性能分析--JVM--应用内存分析

1 简介
JVM(Java Virtual Machine),即Java虚拟机,是整个Java实现跨平台的最核心部分。
所有的Java程序会首先被编译为.class的类文件,然后在虚拟机上执行,即.class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。
JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),即可在多种平台上不加修改地运行。因此,JVM的性能优化对苍穹性能影响至关重要。
小编本期总结了苍穹性能分析实战过程中的一些经验,为大家讲讲和内存相关的性能分析方法。
2 苍穹应用Java内存分析
2.1 JVM基本结构
在进行内存分析前,我们需先了解JVM的内存结构。具体结构如下图所示:

JVM 运行时的数据区主要包括:堆、栈、方法区、程序计数器等。而 JVM 的优化问题主要在于线程共享的数据区:堆、方法区。其中最核心的堆空间结构如下:

如上图所示,Java堆的内存划分为Eden(年轻代)、Old Memory(老年代)、Perm(永久代)。在Jdk1.8中,永久代被移除,使用元数据MetaSpace代替。
Eden(年轻代)
2. 年轻代使用复制清除算法(Copinng算法),原因是年轻代每次GC(Garbage Collection,垃圾回收)都要回收大部分对象。每次GC时只使用Eden和其中一块Survivor空间,然后把存活对象放到未使用的Survivor空间中,清空Eden和刚才使用过的Survivor空间;
3. 内存不足时发生Minor GC。
Old Memory(老年代)
采用标记-整理算法(mark-compact算法),原因是老年代每次GC只会回收少部分对象。
Perm(永久代)
1. Perm的废除:在jdk1.8中,Perm被替换成MetaSpace,MetaSpace存放在本地内存中。原因是永久代进程内存不够用,或者发生内存泄漏;
2. MetaSpace(元空间):元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
2.2 JVM内存使用分析
我们可以借助苍穹的monitor平台和Java的原生命令来查看JVM内存使用情况。在monitor平台中的指标监控如下:

这里主要关注堆内存、Metaspace、GC-Duration三个指标。
堆内存
堆内存是JVM中最大的内存。如果堆内存使用量一直接近max值,说明内存不足或者有内存泄漏,不能通过GC释放内存空间。如果堆内存长时间得不到释放就容易发生堆内存的OOM(Out Of Memory,内存溢出)。
Metaspace
Metaspace元数据区主要是存放类信息和常量信息。和堆内存一样,也会有GC操作来释放内存。如果释放不了也会发生元数据的OOM。
GC-Duration
GC-Duration指标图的横坐标表示时间,纵坐标表示GC使用的时间,下面的标注表示最近一次YongGC和FullGC所花的时间。曲线图出现一次折线表示发生了一次GC操作。我们需重点关注FullGC的次数和时间。
查看堆内存和非堆内存的另一种办法为:
添加JVM选项-XX:NativeMemoryTracking=summary|detail,然后执行命令jcmd <pid> VM.native_memory scale=MB。

如上图所示,上方红框表示堆内存,其中:
“reserved”表示应用可用的内存大小;
“committed”表示应用正在使用的内存大小;
“Java Heap” 部分表示对内存的使用情况;
“committed=4096MB”表示预分配了全部堆内存。如果要看实际使用了多少内存,可以使用“jmap–heap pid”命令查看。
下方红框则表示Java的非堆内存,其中:
“Class”部分表示已经加载的classes个数,占用121M,对应元数据空间 MetaData;
“Thread”部分表示目前有401个线程,占用了404MB,默认1个线程1M;
“Code”部分表示JIT生成的或者缓存的instructions占用了95MB;
“GC”部分表示目前已经占用了95MB的内存空间用于帮助GC;
“Compiler”部分表示compiler生成code的时候占用的内存;
“Internal”部分表示命令行解析、JVMTI等占用了51MB,对应直接内存;
“Symbol”部分表示诸如string table及constant pool等symbol占用了25MB;
“Native Memory Tracking”部分表示该功能自身占用了6M;
“Unknow”部分表示其他未知使用内存。
通过分析堆内存和非堆内存占用,我们可以合理地设置堆大小和容器大小。
以需要2G heap,运行环境为jdk 1.8为例,通过“堆内存 + 线程数*线程栈 + 元空间 + 其他堆外内存”来估算容器大小,即2G+1000*1M+256M+(~2G)=~3.5G(~5.5G)
注:堆外内存=线程数*线程栈+元空间+其他堆外内存
一般情况下,苍穹容器内存规格=JVM堆内存+2G,但对于一些大型项目,如果我们通过分析非堆内存的占用,并根据实际情况调整容器内存规格,苍穹容器内存规格=JVM堆内存+4G也是正常的。
3 Full GC 和OOM
在JVM运行中会产生很多不可达对象,这些对象被称为垃圾对象。JVM需要不断地回收这些垃圾对象以腾出空闲内存。
对年轻代(包括 Eden 和 Survivor 区域)进行GC被称为 Minor GC,对老年代进行GC称为Major GC,而Full GC是对整个堆而言,在最近几个版本的JDK里默认包括了对永久代,即方法区的回收(JDK8中无永久代了)。
出现Full GC的时候经常伴随至少一次的Minor GC,但非绝对。Major GC的速度一般会比Minor GC慢10倍以上,所以我们需重点关注Full GC。一般而言,分析Full GC流程如下。
3.1 查看Full GC日志
配置JVM参数打开GC日志:
-Xloggc:/mservice/logs/gc_%t.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCApplicationStoppedTime默认情况下jdk1.8是使用UseParallelGC垃圾收集器:
/jdk/bin/java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=2147483648
-XX:MaxHeapSize=32210157568
-XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:+UseParallelGC苍穹标准是使用G1垃圾收集器,JVM参数设置如下:
-XX:+UseG1GC
-XX:G1HeapRegionSize=8m
-XX:G1ReservePercent=5查看Full GC日志:
grep -i "full" gc.log

上图发生了6次Full GC,每次GC的时间在16s以上,表示应用程序在这16s中是暂停状态,这对程序的影响是不可忽视的。
Full GC 的3种情况:
1. 偶尔出现Full GC且GC后能够回收大量空间,Full GC时间1-2s说明速度很快,Full GC效果很好,属于正常现象;
2. 出现大量Full GC,会导致Java进程CPU使用率很高,系统很卡顿;
3. 出现大量Full GC出现且GC时间长,回收空间很小,则可能会导致OOM,系统中断。
3.2 分析Full GC日志
可以直接查看GC日志文件进行分析,如下图所示:

"Full GC(Allocation Failure)"表示Full GC失败;Full GC之前堆占用的空间3018.5M,Full GC之后还是3018.5M,表示GC效果不好,没有释放出空闲空间。
为进一步统计暂停时间,可执行以下命令:
grep -i secs gc.log | awk -F '),' '{print $2}' | grep -v "^$" | sort -r | more
该命令可以得到暂停时间的倒序排序:
grep -i "which application" gc.log | awk -F "stopped:" '{print $2}' 性能分析--JVM--应用内存分析
声明:除非特别标注,否则均为本站原创文章,转载时请以链接形式注明文章出处。如若本站内容侵犯了原著者的合法权益,可联系本站删除。



