头图
哈喽哈喽大家猴,我是把代码写成bug的大头菜。公众号:大头菜技术(bigheadit)。原创不易,但欢迎转载。

这周的技术周报主要内容:JVM相关知识点

JVM相关知识点

这周,还是主要研究JVM的相关知识,主要研究一些真实的JVM生产案例。尝试通过案例去总结一些优化JVM的方法论:

比如:

频繁发生FULL GC,应该如何分析?

首先可能原因有:

  • survivor空间太小,不足以容纳所有存活对象,从而导致对象过早进入老年代。使得老年代很快没内存,导致频繁发生FULL GC。
  • survivor空间太小,存活对象的空间大小超过survivor空间的一半,触发动态年龄判断,使得对象过早进入老年代,使得老年代很快没内存,导致频繁发生FULL GC。
  • 发生内存泄露,有大对象一直在老年代,得不到回收。当每一次young gc后,都会有少量对象进入老年代,从而触发FULL GC
  • 代码中显示调用System.gc(),在高并发时,会频繁触发FULL GC
  • 频繁发生young gc,存活对象的大小大于进入老年代对象的平均大小,且没开启空间担保分配,从而触发FULL GCC
  • 在元数据区,不断有新的类被加载到元数据区,从而触发FULL GC

上面这些是一些发生FULL GC可能的原因。

我们可以通过jstat命令来查看堆的情况来进行判断。如果有监控视图,也可以通过监控视图来判断。

定位到具体原因后,进行对应的优化即可。

总体的核心思路就是:尽可能让对象在young gc中被回收,防止对象过快进入老年代,进行一些不必要的FULL GC。说白了,就是减少FULL GC的频率。一般把FULL GC发生的次数优化到一天一次,或者几天一次即可。

大家也不要被网上的一些文章观点误导:说老年代是新生代空间的1.5倍。

哪有这样子通用的优化模板的,不同的业务,肯定需要制定不同的优化策略的。只要不变应万变,不变就是降低FULL GC的频率,变化的就是各种内存的分配策略+垃圾回收器的选择+垃圾回收器参数配置。

比如:一些要求延时低的系统,可以采用CMS+ParNew策略;或者采用G1策略。

如果对应一些需要计算大量数据的系统,这时需要大内存,此时选择G1会比较合适。

尝试解答一些问题:

问题:发生young gc时,存活对象的空间大小大于survivor空间,此时存活对象会去哪?
  • 说法一:所有存活对象会全部进入老年代。
  • 说法二:部分对象进入survivor区,部分对象进入老年代。

先说结论:说法一和说法二都正确。理由如下:

  • 如果发生full gc,会将新生代存活的对象都放入老年代。
  • 如果发生young gc,会将新生代存活的对象一部分放survivor区,一部分放老年代

那如何来证明呢?

我直接上代码:

先运行代码,拿到日志文件:

Java HotSpot(TM) 64-Bit Server VM (25.281-b09) for bsd-amd64 JRE (1.8.0_281-b09), built on Dec  9 2020 12:44:49 by "java_re" with gcc 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.11.45.5)
Memory: 4k page, physical 16777216k(126540k free)

/proc/meminfo:

CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=3145728 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 
0.115: [GC (Allocation Failure) 0.115: [ParNew (promotion failed): 6943K->7407K(9216K), 0.0040643 secs]0.119: [CMS: 8194K->6562K(10240K), 0.0016383 secs] 11039K->6562K(19456K), [Metaspace: 2706K->2706K(1056768K)], 0.0061605 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Heap
 par new generation   total 9216K, used 2130K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  26% used [0x00000007bec00000, 0x00000007bee14930, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
  to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
 concurrent mark-sweep generation total 10240K, used 6562K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
 Metaspace       used 2713K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 291K, capacity 386K, committed 512K, reserved 1048576K

首先,我们直接看from区和to区,这两个区,使用率都为零。根据这里,可以得到证明,存活对象没去survivor区。

[ParNew (promotion failed): 6943K->7407K(9216K)

首先看,eden区,GC前6943K,GC后7407K。好家伙,不降反升。

因为上面这几个对象都被引用着,无法回收。

当执行代码: byte[] array6 = new byte[2*_1MB];

此时eden区有存活对象有3个2M的对象和1个128K的对象。

因为eden区没有足够的空间,此时会发生young gc。

新生代6M+128K大于老年代的可用连续内存空间,

但是老年代可用连续内存空间大于历年新生代进入老年代对象的平均大小0。

所以,空间分配担保成功,不需要触发full gc。

但是因为6m+128k对象都存活,且因为survivor空间只有1m空间

导致存活对象直接进入老年代

看一下日志:

[CMS: 8194K->6562K(10240K)

年轻代现将2个2m对象,放入老年代,此时老年代有1个4m+2个2m的对象。大约8194k

此时还需要放入1个2m对象+1个128k对象。

但老年代空间不足,此时需要触发full gc。

回收4m对象,然后再把1个2m对象+1个128k对象放入老年代。即6562K。

这里发生了full gc。结合from 区和to区的使用率都是0

可见,发生full gc时,全部存活对象,都会进入老年代。

那如何证明:

如果发生young gc,会将新生代存活的对象一部分放survivor区,一部分放老年代

让它只发生young gc即可。

代码如下:

运行一下代码,日志如下:

Java HotSpot(TM) 64-Bit Server VM (25.281-b09) for bsd-amd64 JRE (1.8.0_281-b09), built on Dec  9 2020 12:44:49 by "java_re" with gcc 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.11.45.5)
Memory: 4k page, physical 16777216k(125452k free)

/proc/meminfo:

CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=3145728 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 
0.115: [GC (Allocation Failure) 0.116: [ParNew: 6943K->462K(9216K), 0.0040400 secs] 6943K->6608K(19456K), 0.0044398 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
Heap
 par new generation   total 9216K, used 2592K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  26% used [0x00000007bec00000, 0x00000007bee14930, 0x00000007bf400000)
  from space 1024K,  45% used [0x00000007bf500000, 0x00000007bf573ab0, 0x00000007bf600000)
  to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
 concurrent mark-sweep generation total 10240K, used 6146K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
 Metaspace       used 2713K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 291K, capacity 386K, committed 512K, reserved 1048576K

我们把焦点放在:from space 1024K, 45%

此时,from 区被使用了45%,可以证明有存活对象

再看,concurrent mark-sweep generation total 10240K, used 6146K

这里是有6M(6*1024=6144k),就是6146。肯定不包括array5的128K对象。

过程我就不描述了。

看到这里,你应该懂了吧。如果你还看不懂,欢迎加我微信和我交流讨论。

这周的技术日报就到这吧。。。。

下周见。。。。

相关代码和JVM配置参数:公众号回复:对象去哪里 获取相应代码


大头菜
41 声望8 粉丝