共计 7744 个字符,预计需要花费 20 分钟才能阅读完成。
哈喽哈喽大家猴,我是把代码写成 bug 的大头菜。公众号:大头菜技术 (bigheadit)。原创不易,但欢送转载。
上一篇文章:《JVM- 动静年龄判断》介绍了对象进入老年代的四种形式
- 大对象
- 动静年龄判断
- minor gc 后,survivor 区空间不能包容全副存活对象
- 存活对象达到年龄阈值。比方 15
接下来,咱们将用代码形式来验证这四种形式。
常识回顾
在实战开始之前,咱们先温习一下知识点:
0.134: [GC (Allocation Failure) 0.134: [ParNew: 7444K->685K(9216K), 0.0011650 secs]
7444K->685K(19456K), 0.0012583 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
因为波及到 GC 日志的查看,我就简略介绍一下日志的大略含意:
- 0.134。含意就是在程序启动后多久,产生了垃圾回收。单位: 秒
- GC (Allocation Failure)。GC 就是产生了垃圾回收的意思,这个不用说都分明。而后括号外面的 Allocation Failure,就是产生垃圾回收的起因。这里 Allocation Failure,就是空间调配失败导致产生的垃圾回收。
- [ParNew:ParNew 就是新生代垃圾回收应用的垃圾收集器,同时也代表产生了 minor gc。
- 7444K->685K(9216K), 0.0011650 secs]。接着 7444K,是新生代垃圾回收前已应用的空间。685K,是新生代垃圾回收后已应用的空间。9216K,是新生代的总空间。0.0011650 secs,就是垃圾回收须要消耗的工夫。
- 7444K->685K(19456K), 0.0012583 secs],7444K 是整个堆(新生代 + 老年代)垃圾回收前已应用的空间。685K 是整个堆垃圾回收后已应用的空间。19456K 是整个堆的总空间。
- [Times: user=0.00 sys=0.00, real=0.01 secs]。就是本次 GC 耗费的工夫,因为这里放弃了 2 为小数,四舍五入嘛,因而都是 0。
好了,差不多了,间接开始吧!!!!
大对象
首先,咱们简略回顾一下。
书本,也没阐明,多大的对象才是大对象,比拟形象。
咱们这里间接具体点:
-XX:PretenureSizeThreshold=3m
大于等于 3m 的对象,就是大对象。
-XX:NewSize=10m -XX:MaxNewSize=10m -XX:InitialHeapSize=20m
这里,咱们给新生代 10m, 堆 20m。
也就是说,老年代的空间 =20m-10m=10m
-XX:SurvivorRatio=8
eden:s0:s1 等于 8:1:1
所以 eden 区为 8m,s0 为 1m,s1 为 1m
应用的垃圾收集器为 ParNew 和 CMS
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
打印 GC 的详细信息
-XX:+PrintGCDetails
打印 GC 的工夫戳
-XX:+PrintGCTimeStamps
那这些 GC 日志输入在哪里,当然是文件啦。
-Xloggc:bigobject.log
好了,JVM 的配置差不多了,间接上代码。
JVM 配置参数
-XX:NewSize=10m -XX:MaxNewSize=10m -XX:InitialHeapSize=20m -XX:MaxHeapSize=20m -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3m -XX:MaxTenuringThreshold=15 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:bigobject.log
代码:
byte[] array1 = new byte[2*_1MB];
byte[] array2 = new byte[3*_1MB];
因为 2m 的对象不是大对象,因而调配到 eden 区。而 3m 的对象是大对象,因而调配到 old 区。
下面,这些都是咱们依据实践来揣测进去的。
接下来,咱们运行一下代码,而后查问日志文件 bigobject.log
Java HotSpot(TM) 64-Bit Server VM (25.261-b12) for bsd-amd64 JRE (1.8.0_261-b12), built on Jun 18 2020 06:38:55 by "java_re" with gcc 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.11.45.5)
Memory: 4k page, physical 33554432k(382392k 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
Heap
par new generation total 9216K, used 4849K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
eden space 8192K, 59% used [0x00000007bec00000, 0x00000007bf0bc580, 0x00000007bf400000)
from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
concurrent mark-sweep generation total 10240K, used 3072K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
Metaspace used 3074K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 337K, capacity 388K, committed 512K, reserved 1048576K
当初,咱们剖析一下日志内容,来验证咱们的猜想是否正确。
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
Heap
下面这一堆,就是咱们本人设置的 JVM 参数,也有一些零碎帮咱们增加的。比方:-XX:+UseCompressedClassPointers -XX:+UseCompressedOops
这些不是重点,你理解一下就好。
重点是这个:
Heap
par new generation total 9216K, used 4849K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
eden space 8192K, 59% used [0x00000007bec00000, 0x00000007bf0bc580, 0x00000007bf400000)
from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
concurrent mark-sweep generation total 10240K, used 3072K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
代表的含意是:GC 后堆的状况,留神是 GC 后。这里特别强调一下。
par new generation total 9216K, used 4849K
代表的新生代,当初总的空间大小是 9216K,已应用的空间大小是 4849K。
其实咱们晓得,有 2M 对象是进入了 eden 区的,然而当初 4849K 显著是大于 2048K(2M)的。
那阐明了什么?
阐明,其实 JVM 除了加载咱们本人写的对象外,还会加载一些其余未知对象。未知对象,次要由 JVM 自身产生,这部分大家先疏忽就好
回到次要问题上:大对象是否会间接进入老年代。
咱们看以下这段日志:
concurrent mark-sweep generation total 10240K, used 3072K
老年代的总空间大小是 10240K,目前曾经应用了 3072K。
看到这里,其实置信大家曾经能够明确了。咱们把大对象定义为大于等于 3m 的对象。而日志也通知咱们,目前老年代曾经被占用了 3072K,即 3m。
因而,到这里咱们曾经用代码验证了:大对象会间接进入老年代。
动静年龄判断
首先,咱们还是先理解一下什么是动静年龄判断?
书本的解释如下:
总结一下:就是说 survivor 区中,如果雷同年龄的所有对象大小所占用的空间大于 survivor 空间的一半,年龄大于或等于该年龄对象的,都能够间接进入老年代。
这是书本的说法。。
但其实这个说法是谬误的。
置信看过我另一篇文章《插入链接》的小伙伴,是理解正确的说法应该是: 在 survivor 区中,所有年龄的对象的所占空间的累加和大于 survivor 空间的一半,大于或等于该年龄的对象,都能够进入老年代。
接下来咱们间接上代码和 JVM 配置参数:
JVM 配置参数:
-XX:NewSize=10m -XX:MaxNewSize=10m -XX:InitialHeapSize=20m -XX:MaxHeapSize=20m -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=10m -XX:MaxTenuringThreshold=15 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:dynamicheck.log
代码:
咱们先间接把代码跑起来,而后间接看日志文件:
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(106332k 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=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
0.117: [GC (Allocation Failure) 0.118: [ParNew: 7115K->619K(9216K), 0.0029447 secs] 7115K->619K(19456K), 0.0033789 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.122: [GC (Allocation Failure) 0.122: [ParNew: 7223K->0K(9216K), 0.0020215 secs] 7223K->601K(19456K), 0.0020509 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
Heap
par new generation total 9216K, used 2212K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
eden space 8192K, 27% used [0x00000007bec00000, 0x00000007bee290e0, 0x00000007bf400000)
from space 1024K, 0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
to space 1024K, 0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
concurrent mark-sweep generation total 10240K, used 601K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
Metaspace used 2713K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 291K, capacity 386K, committed 512K, reserved 1048576K
局部代码分析:
当初间断调配了 3 个 2M 的对象,和 1 个 300K 的对象,最初还把 array1 置为 null。此时的堆图应该是这样子的
接下来,还要持续调配一个 2M 的对象,这个时候 eden 区还能持续调配空间吗?
必定不能够,因为 eden 区只有 8M。
那这个时候,只好执行 young gc 来清理空间了。
接着,咱们看一下日志文件:
ParNew: 7115K->619K(9216K)
阐明 GG 前,占用了 7115K,这里大略包含 3 个 2m 对象 +300k 对象 + 几百 K 未知对象。通过 GC 后,只剩下 619K 对象,包含 300K 对象和未知对象。
而后给新创建的 2m 对象调配到 eden 区
此时,咱们得察看一个重点:就是 from 区是 1m, 就是 1024K。当初呢,有 619K 对象曾经来到了 from 区,是超过 from 区的一半的。
接着咱们持续看代码:
执行完这个代码。会在堆里会新增 2 个 2m 对象,1 个 300K 对象,最初 array3 置为 null。
如果要继续执行 byte[] array4 = new byte[2*_1MB];
那么 eden 区就不够空间了。这个时候会触发第二次 young gc 了。
咱们持续看一下第二次 young gc 的日志:
ParNew: 7223K->0K(9216K)
阐明了啥?
GC 前,一共应用了 7223K,包含 eden 区的 3 个 2m 对象 +300K 对象和 from 区的 300K 对象 + 未知对象,GC 后,整个新生代都有空了。
实践上,GC 后,array2 还援用着 300K 对象的。所以,能够必定的是,这 300K 对象,必定不会被回收。
但当初 GC 日志显著通知咱们,新生代在 GC 后的空间使用率为 0。
这是为什么呢?
不焦急,咱们持续看一下老年代的空间日志:
concurrent mark-sweep generation total 10240K, used 601K
你看,老年代居然被应用了 601K。其实这 601K,就是 300K 对象和未知对象的空间。
为什么它们会在老年代?
就是因为触发了动静年龄判断呀。
你想想,首先,300K 不是大对象吧。(-XX:PretenureSizeThreshold=10m)
也没达到 15 岁,因为才 young gc 两次。(-XX:MaxTenuringThreshold=15)
而且当初 survivor 是 1024K 空间,是足以包容 601K 的存活对象的。所以,这些都不是导致对象进入老年代的起因。
当初因为 2 次 young gc,那 600K 对象都存活着,并且占用的空间是超过 survivor 区的空间大小一半。从而触发动静空间判断,进入老年代。
说到这里,置信大家应该都能明确了。如果还不明确,欢送加大头菜微信一起探讨。
纸上得来终觉浅,剩下的 2 种状况:
- minor gc 后,survivor 区空间不能包容全副存活对象
- 存活对象达到年龄阈值。比方 15
这两种状况的代码,你们能够本人试试实战一下。
留一个问题给大家思考一下,欢送大家留言交换:
survivor 空间不足以包容存活对象时,是不是所有对象都会进入老年代?还是会有局部进入老年代,剩下局部留在 survivor 区?
我呢,代码曾经写好了。大家自行在后盾获取即可。
在公众号回复:jvm 代码。能够查看这四种状况的实战代码。
絮叨
原本这种波及到代码实战的技术分享,应该录视频讲比拟好的。
然而切实太难了。大头菜自认为是一个脸皮厚的人,然而昨晚录了 3 个小时,都没录完。
要么就是中途结巴,要么就是中途缓和,总之,害,写文章和录视频,真的两回事。写文章上,我思路算是比拟清晰的。但录视频真的紧脏呜呜呜。。。。。。
我忍不住想吐槽一下本人的声音,录完后,我听了一下本人的声音。
咦惹。。。妈呀。。。。。什么鬼。。辣耳朵
果然,听本人的声音是须要勇气的。。。。。
但,我还是会持续尝试录视频的,剪完后,给大家分享。