文章目录
- 一、背景分析
- 二、查看垃圾收集器
- 三、理解的偏差
- 四、深挖 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