文章目录
- 一、背景分析
- 二、查看垃圾收集器
- 三、理解的偏差
- 四、深挖 demo 代码
- 五、总结
一、背景分析
无论讨论对象的内存分配,还是讨论内存的回收策略或是算法,都需要明确指出是基于哪种垃圾收集器进行的讨论。所以,很自然地就想到这个问题:如何查看 JVM 使用了哪种垃圾收集器呢?
二、查看垃圾收集器
方法有很多,可以通过jconsole、VisualVM等工具间接地查看垃圾收集器的版本,但是比较简单的方式还是使用命令。
在cmd中执行下面的命令:
java -XX:+PrintCommandLineFlags -version
然后输出了如下内容:
从图中可以看出:
- JDK版本:1.8.0
- JVM是64位的Server版本
- 结合下一张图的 参数说明,可知使用的垃圾收集器是 :新生代(Parallel Scavenge),老年代(PS MarkSweep)。这是虚拟机Server模式下的默认值。
图片来源:《深入理解Java虚拟机:JVM高级特性与最佳实践》
下面看一下jconsole的截图:
启动jconsole的方法也很简单,只需要在命令行中输入: jconsole
,就可以启动该工具。
三、理解的偏差
但是,后来看到这位大哥的文章:
https://www.cnblogs.com/grey-wolf/p/9217497.html
才发现自己的理解是有偏差的。为什么这么说呢?
因为我写了一个验证GC的小demo,发现打印出的日志信息如下:
HeapPSYoungGen total 9216K, used 3378K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)eden space 8192K, 41% used [0x00000000ff600000,0x00000000ff94c880,0x00000000ffe00000)from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)ParOldGen total 10240K, used 10239K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)object space 10240K, 99% used [0x00000000fec00000,0x00000000ff5ffc10,0x00000000ff600000)Metaspace used 3036K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 328K, capacity 388K, committed 512K, reserved 1048576K
相关代码如下:
public class MemoryAllocationDemo {/*** VM:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8*/public static void main(String[] args) {//对象直接分配到老年代byte[] alloc = new byte[10239*1024];}
}
虚拟机参数如下:
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
参数说明:
- 新生代:10M
- 老年代:10M
- 初始堆内存和最大堆内存:20M
- Eden和Survivor的比例是:8:1(即Eden是8M,单个survivor是1M)
据说在jdk8中,默认启用了 ParallelOldGC
,相关代码如下:
--- a/src/share/vm/runtime/arguments.cpp Mon Jan 30 15:21:57 2012 +0100
+++ b/src/share/vm/runtime/arguments.cpp Thu Feb 02 16:05:17 2012 -0800
@@ -1400,10 +1400,11 @@void Arguments::set_parallel_gc_flags() {assert(UseParallelGC || UseParallelOldGC, "Error");
- // If parallel old was requested, automatically enable parallel scavenge.
- if (UseParallelOldGC && !UseParallelGC && FLAG_IS_DEFAULT(UseParallelGC)) {
- FLAG_SET_DEFAULT(UseParallelGC, true);
+ // Enable ParallelOld unless it was explicitly disabled (cmd line or rc file).
+ if (FLAG_IS_DEFAULT(UseParallelOldGC)) {
+ FLAG_SET_DEFAULT(UseParallelOldGC, true);}
+ FLAG_SET_DEFAULT(UseParallelGC, true);// If no heap maximum was requested explicitly, use some reasonable fraction// of the physical memory, up to a maximum of 1GB.
相关修改来源:
https://hg.openjdk.org/jdk8u/jdk8u/hotspot/rev/24cae3e4cbaa
出于好奇,我在虚拟机参数中增加了如下内容:
-XX:+UseParallelOldGC -XX:-UseParallelGC
参数说明:
- 启用 ParallelOldGC
- 同时禁用 ParallelGC
打印出来的堆信息如下:
HeapPSYoungGen total 9216K, used 3544K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)eden space 8192K, 43% used [0x00000000ff600000,0x00000000ff976108,0x00000000ffe00000)from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)ParOldGen total 10240K, used 10239K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)object space 10240K, 99% used [0x00000000fec00000,0x00000000ff5ffc10,0x00000000ff600000)Metaspace used 3312K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 359K, capacity 388K, committed 512K, reserved 1048576K
可以看出,堆信息几乎一样。所以单纯从堆信息看,是无法区分到底是使用了哪种GC收集器。
使用表格比较下两者的区别:
参数 | 新生代-垃圾收集器 | 老年代-垃圾收集器 | Old-线程 | Old-算法 |
---|---|---|---|---|
UseParallelOldGC | Parallel Scavenge | Parallel Old | 多线程 | 标记整理 |
UseParallelGC | Parallel Scavenge | PS MarkSweep(即Serial Old ) | 单线程 | 标记整理 |
在 吞吐量优先
的场景下,建议使用第一个垃圾收集器组合。
结论:既然jdk源码都说默认启用了 ParallelOldGC
,那就记住这个结论吧。
四、深挖 demo 代码
现在回过头聊一聊demo中的代码意图。
代码:
byte[] alloc = new byte[10239*1024];
是创建了一个大的数组。其中的数组元素的个数是:10239k
。
然后我们看到其全部放到了老年代中:
ParOldGen total 10240K, used 10239K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)object space 10240K, 99% used [0x00000000fec00000,0x00000000ff5ffc10,0x00000000ff600000)
这些数组元素占用的空间:10239k
,所以可以看出每个数组元素占了1个byte
(字节)。
那么,这里的对象为什么会直接进入了老年代了呢?
首先,这里的byte数组是一个大对象,并且它需要一段连续的空间(因为是数组)。
其次,这个大对象的占用空间有点特殊,是 10239k
,恰好比 10M
少 1k
。
那么,新生代可以容纳得下它吗?
前面我们说过新生代的总大小是10M
,但是因为这10M
包含了1个 8M
的Eden区,还有2个 1M
的 Survivor区,所以实际可用空间是 9M
(即 9216K
)。很显然 9216K
是小于 10239k
的。
那么接下来需要再检查下老年代空间是否足够。老年代的空间是 10M
,前面讲过这个值比 10239k
正好大 1k
。所以,老年代是能容纳下这些数据的,根据 内存的分配策略,大对象会直接进入老年代。
因为jdk1.8使用的是 Parallel Scavenge
收集器,所以下面的虚拟机参数是不生效的:
# 单位是byte,1048576=1024k=1M
-XX:PretenureSizeThreshold=1048576
五、总结
本文介绍了如何查看JVM使用的垃圾收集器。在网友的启示下,获知jdk1.8.0中默认使用的是 Parallel Scavenge + Parallel Old
收集器。通过修改虚拟机参数还得知:Parallel Scavenge+PS MarkSweep
与 Parallel Scavenge + Parallel Old
两种收集器的GC日志是一样的。
然后又通过代码验证了一下大对象的内存分配策略:了解了什么情况下大对象会直接进入老年代。
参考:
-
《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》
-
https://github.com/zlserver/jvm_code
-
https://gitee.com/tanglongan/understanding-the-jvm
-
https://github.com/TangBean/understanding-the-jvm/tree/master