JVM调优
JVM内存
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiIXZ05WZj91YpB3IwczX0xiRGZkRGZ0Xy9GbvNGL2EzXlpXazxib1clW0x2VlFTMywEMW1mY1RzRapnTtxkb5ckYplTeMZTTINGMShUYfRHelRHLwEzX39GZhh2css2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xyayFWbyVGdhd3LcV2Zh1Wa9M3clN2byBXLzN3btg3Pn5GcuITMyUjNycTM5ETMwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
JVM脑图
垃圾回收算法
- 标记-复制算法
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
- 标记-清除算法
算法分为“标记”和“清除”阶段:标记存活的对象, 统一回收所有未被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。但是会带来两个问题:
- 效率问题:如果需要标记的对象太多,效率不高
- 空间问题:标记清除后会产生大量不连续的碎片
- 标记-整理算法
根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
栈
- 指针压缩
通过指针压缩来减小对内存的消耗,来提高内存的利用率。
- 对象内存分配-栈上分配
栈上内存的特点为线程所有,不存在共享数据,所以就可以做到用完就销毁,就不需要垃圾回收,基于这个特点,JVM会对对象进行是否可以在栈上分配的判断-“逃逸分析”和 “标量替换”。
堆(Heap)
JVM调优基本上都是围绕着堆的调整来优化垃圾回收时机。首先我们来看一下垃圾回收算法都有哪些。
堆的划分
分带收集理论
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象(近99%)死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可 以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。
虚拟机一般把堆分为两部分 年轻代 和老年代。
年轻代
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。年轻代采用的垃圾回收算法一般都是标记-复制算法,所以需要把年轻代再次划分为多个区域。
Eden与Survivor区默认8:1:1
大量的对象被分配在eden区,eden区满了后会触发yang gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可,JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy。
老年代
进入老年代的对象只能被Full GC收集,所以我们要控制进入老年代的对象,尽量减少Full GC的次数。对象进入老年代的途径:
- 大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效。
- 长期存活的对象晋升到老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
- 对象动态年龄判断
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在yang gc之后触发的。
触发Full GC的时机
1. System.gc()方法的调用
在代码中调用System.gc()方法会建议JVM进行Full GC,但是注意这只是建议,JVM执行不执行是另外一回事儿,不过在大多数情况下会增加Full GC的次数,导致系统性能下降,一般建议不要手动进行此方法的调用,可以通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
2. 老年代(Tenured Gen)空间不足
在Survivor区域的对象满足晋升到老年代的条件时,晋升进入老年代的对象大小大于老年代的可用内存,这个时候会触发Full GC。
3. Metaspace区内存达到阈值
Metaspace使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关。-XX:MetaspaceSize=21810376B(约为20.8MB)超过这个值就会引发Full GC,这个值不是固定的,是会随着JVM的运行进行动态调整的。
4. 老年代空间担保分配机制
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间,如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象) 就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次yang gc后进入老年代的对象的**平均大小。**如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,
如果回收完还是没有足够空间存放新的对象就会发生"OOM"当然,如果yang gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放yang gc之后的存活对象,则也会发生“OOM”。
JVM调优
相关命令
前置启动程序 jps
事先启动一个web应用程序,用jps查看其进程id,接着用各种jdk自带命令优化应用
Jmap
此命令可以用来查看内存信息,实例个数以及占用内存大小
jmap -histo 17296 > ./test.txt
num:序号 instances:实例数量 bytes:占用空间大小
class name:类名称,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][]
堆信息 jmap -heap 18564
jmap -heap 18564
E:\gitee\vastmoon\sparrow>jmap -heap 18564
Attaching to process ID 18564, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.231-b11
using thread-local object allocation.
Parallel GC with 8 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4263510016 (4066.0MB)
NewSize = 89128960 (85.0MB)
MaxNewSize = 1420820480 (1355.0MB)
OldSize = 179306496 (171.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 246939648 (235.5MB)
used = 139866816 (133.38739013671875MB)
free = 107072832 (102.11260986328125MB)
56.64008073746019% used
From Space:
capacity = 12582912 (12.0MB)
used = 0 (0.0MB)
free = 12582912 (12.0MB)
0.0% used
To Space:
capacity = 14680064 (14.0MB)
used = 0 (0.0MB)
free = 14680064 (14.0MB)
0.0% used
PS Old Generation
capacity = 117440512 (112.0MB)
used = 18069168 (17.232101440429688MB)
free = 99371344 (94.76789855957031MB)
15.385804857526507% used
18745 interned Strings occupying 1746872 bytes.
堆内存dump
jmap -dump:format=b,file=bootstrapApplication.hprof 18564
也可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=./ (路径)
Jstack
用jstack加进程id查找死锁
Jinfo
查看正在运行的Java应用程序的扩展参数
jinfo -flags 18564
查看jvm的参数
jinfo -sysprops 18564
查看java系统参数
Jstat
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:
jstat [-命令选项] [vmid] [间隔时间(毫秒)] [查询次数]
垃圾回收统计
jstat -gc pid
最常用,可以评估程序内存使用及GC压力整体情况
S0C:第一个幸存区的大小,单位KB, S1C:第二个幸存区的大小 S0U:第一个幸存区的使用大小S1U:第二个幸存区的使用大小 EC:伊甸园区的大小 ,EU:伊甸园区的使用大小 OC:老年代大小 OU:老年代使用大小
MC:方法区大小(元空间) MU:方法区使用大小 CCSC:压缩类空间大小 CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间,单位s FGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间,单位s GCT:垃圾回收消耗总时间,单位s
jstat -gccapacity 18564
堆内存统计
NGCMN:新生代最小容量 NGCMX:新生代最大容量 NGC:当前新生代容量 S0C:第一个幸存区大小
S1C:第二个幸存区的大小 EC:伊甸园区的大小 OGCMN:老年代最小容量 OGCMX:老年代最大容量
OGC:当前老年代大小 OC:当前老年代大小 MCMN:最小元数据容量 MCMX:最大元数据容量 MC:当前元数据空间大小 CCSMN:最小压缩类空间大小 CCSMX:最大压缩类空间大小 CCSC:当前压缩类空间大小 YGC:年轻代gc次数 FGC:老年代GC次数
jstat -gcnew 18564
新生代垃圾回收统计
S0C:第一个幸存区的大小 S1C:第二个幸存区的大小 S0U:第一个幸存区的使用大小 S1U:第二个幸存区的使用大小 TT:对象在新生代存活的次数 MTT:对象在新生代存活的最大次数 DSS:期望的幸存区大小 EC:伊甸园区的大小 EU:伊甸园区的使用大小 YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间
jstat -gcnewcapacity 18564
新生代内存统计
NGCMN:新生代最小容量 NGCMX:新生代最大容量 NGC:当前新生代容量 S0CMX:最大幸存1区大小
S0C:当前幸存1区大小 S1CMX:最大幸存2区大小 S1C:当前幸存2区大小 ECMX:最大伊甸园区大小
EC:当前伊甸园区大小 YGC:年轻代垃圾回收次数 FGC:老年代回收次数
jstat -gcold 18564
老年代垃圾回收统计
MC:方法区大小 MU:方法区使用大小 CCSC:压缩类空间大小 CCSU:压缩类空间使用大小 OC:老年代大小
OU:老年代使用大小 YGC:年轻代垃圾回收次数 FGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
jstat -gcoldcapacity 18564
老年代内存统计
OGCMN:老年代最小容量 OGCMX:老年代最大容量 OGC:当前老年代大小 OC:老年代大小 YGC:年轻代垃圾回收次数 FGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间 GCT:垃圾回收消耗总时间
jstat -gcmetacapacity 18564
元数据空间统计
MCMN:最小元数据容量 MCMX:最大元数据容量 MC:当前元数据空间大小 CCSMN:最小压缩类空间大小
CCSMX:最大压缩类空间大小 CCSC:当前压缩类空间大小 YGC:年轻代垃圾回收次数 FGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间 GCT:垃圾回收消耗总时间
jstat -gcutil 18564
辅助信息
S0:幸存1区当前使用比例 S1:幸存2区当前使用比例 E:伊甸园区使用比例 O:老年代使用比例 M:元数据区使用比例 CCS:压缩使用比例 YGC:年轻代垃圾回收次数 FGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间 GCT:垃圾回收消耗总时间
JVM运行情况预估
用 jstat gc -pid 命令可以计算出如下一些关键数据,有了这些数据就可以采用之前介绍过的优化思路,先给自己的系统设置一些初始性的JVM参数,比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。
年轻代对象增长的速率
可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。
Young GC的触发频率和每次耗时
知道年轻代对象增长速率我们就能推根据eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC 公式算出,根据结果我们大概就能知道系统大概多久会因为Young GC的执行而卡顿多久。
每次Young GC后有多少对象存活和进入老年代
这个因为之前已经大概知道Young GC的频率,假设是每5分钟一次,那么可以执行命令 jstat -gc pid 300000 10 ,观察每次结果eden,survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率。
Full GC的触发频率和每次耗时
知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。
优化思路
其实简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。