1. 示例程序代码

  1. public class Demo1 {
  2. public static void main(String[] args) {
  3. byte[] array1 = new byte[1024*1024];
  4. array1 = new byte[1024*1024];
  5. array1 = new byte[1024*1024];
  6. array1 = null;
  7. byte[] array2 = new byte[2*1024*1024];
  8. }
  9. }

2. JVM 参数

-XX:NewSize=5242880 -XX:MaxNewSize=5242880 -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=10485760 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
  • 上述参数基于 JDK1.8 来设置;
  • -XX:InitialHeapSize”和“-XX:MaxHeapSize”就是初始堆大小和最大堆大小,堆内存 10MB;
  • -XX:NewSize”和“-XX:MaxNewSize”是初始新生代大小和最大新生代大小,新生代 5MB;
  • -XX:SurvivorRatio=8”是新生代的空间分配比例,Eden 4MB,两个 Survivor 都是 0.5MB;
  • -XX:PretenureSizeThreshold=10485760”指定了大对象阈值是 10MB;
  • -XX:+UseParNewGC -XX:+UseConcMarkSweepGC”指定新生代使用 ParNew 回收器,老年代使用 CMS 回收器;
  • -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log”:
    • 打印详细的 GC 日志;
    • 打印每次 GC 发生的时间;
    • 设置将 GC 日志写入一个磁盘文件;


3. 图解 GC 执行过程

image.png
image.png

3. GC 日志分析

Java HotSpot(TM) 64-Bit Server VM (25.121-b13) for windows-amd64 JRE (1.8.0_121-b13), built on Dec 12 2016 18:21:36 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 20883216k(9240900k free), swap 29555520k(4575992k free)

CommandLine flags: -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:MaxNewSize=5242880 -XX:NewSize=5242880 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC 

0.151: [GC (Allocation Failure) 0.151: [ParNew: 4057K->510K(4608K), 0.0017559 secs] 4057K->1693K(9728K), 0.0019789 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

Heap
 par new generation   total 4608K, used 3744K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000)
  eden space 4096K,  78% used [0x00000000ff600000, 0x00000000ff9286c0, 0x00000000ffa00000)
  from space 512K,  99% used [0x00000000ffa80000, 0x00000000ffaffbd0, 0x00000000ffb00000)
  to   space 512K,   0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)
 concurrent mark-sweep generation total 5120K, used 1183K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 3470K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 384K, capacity 388K, committed 512K, reserved 1048576K
  • CommandLine flags: -XX:InitialHeapSize=10485760……”
    • 这里可以看到所有我们设置的 JVM 参数,也有默认就给设置的 JVM 参数;
  • “0.151: [GC (Allocation Failure) 0.151: [ParNew: 4057K->510K(4608K), 0.0017559 secs] 4057K->1693K(9728K), 0.0019789 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]”
    • 一次 GC 的概要说明
      • GC (Allocation Failure)“,因为 Eden 内存不够,2M的数组对象分配失败,导致出现 “Allocation Failure”,所以发生一次 Young GC;
      • 0.151“,系统运行 0.151秒之后发生了本次的 Young GC,大概系统运行150ms后发生 GC;
      • ParNew: 4057K->510K(4608K), 0.0017559 secs“:
        • ParNew 执行了垃圾回收;
        • 年轻代的可用空间大小是4608KB,即 Eden 4M + Survivor 0.5M = 4.5MB;
        • 新生代里的 4057K 的对象,经过 Young GC 后,有 510K 的存活对象;
          • 创建一个1M大小的数组对象,实际占用的内存是大于1M的,因为还需要附带一些额外的信息,会创建一些未知对象,所以 GC 之前,三个 1M 的数组和一些额外的未知对象加起来占据 4057K;
          • GC 之后,有 510K 的对象存活,将其转移到了 Survivor From 区域;
        • 本次 GC 耗费 1.7ms,仅仅是回收大概 3M 的对象;
      • 4057K->1693K(9728K), 0.0019789 secs“,整个堆内存的情况:
        • 堆内存总的可用内存 9728K,即 Eden 4M + Survivor 0.5M + 老年代 5M = 9.5M;
        • Young GC 后,整个堆内存的占用空间从 4M,恢复到了 1.5M;
      • [Times: user=0.00 sys=0.00, real=0.00 secs]“,单位是秒,本次 gc 耗费几毫秒,所以以秒为单位,几乎是0;
  • Heap par new generation total 4608K……”
    • GC 过后的堆内存使用情况
      • par new generation total 4608K, used 3744K”,ParNew 垃圾回收器负责的新生代总共有 4608K(4.5M)的可用空间,目前使用了 3744K(3.5M);
        • 2M的数组对象 + 510K的额外未知对象 = 2.5M,而年轻代使用了 3.5M,这多出来的 1M 可能是其他原因占用的(比如 IDEA工具等);
      • eden space 4096K, 78% used“,Eden 区此时 4M 的内存使用了 78%;
      • from space 512K, 99% used“,From Survivor 区,512KB 是100%的使用率,此时被之前 gc 后存活下来的 512KB 的未知对象给占据了;
      • to space 512K, 0% used“,空闲的 to Survivor 区;
      • concurrent mark-sweep generation total 5120K, used 1183K“,CMS(Concurrent Mark-Sweep)垃圾回收器管理的老年代内存空间一共是 5MB,此时使用了 1183KB 的空间,这个是啥也先不用管了,可以先忽略不计,以后有内存分析工具了,都能看到;
      • Metaspace used 3470K, capacity 4496K, committed 4864K, reserved 1056768K“:
        • Jdk8 开始把类的元数据放到本地内存(native heap),称之为 MetaSpace;
        • 理论上本地内存剩余多少,MetaSpace 就有多大,需要用 -XX:MaxMetaSpaceSize 来指定 MetaSpace 区域大小;
        • 关于used capacity commited 和reserved,在stackoverflow找到个比较靠谱的答案,我尝试翻译一下:MetaSpace由一个或多个Virtual Space(虚拟空间)组成。虚拟空间是操作系统的连续存储空间,虚拟空间是按需分配的。当被分配时,虚拟空间会向操作系统预留(reserve)空间,但还没有被提交(committed)。MetaSpace的预留空间(reserved)是全部虚拟空间的大小。 虚拟空间的最小分配单元是MetaChunk(也可以说是Chunk)。当新的Chunk被分配至虚拟空间时,与Chunk相关的内存空间被提交了(committed)。MetaSpace的committed指的是所有Chunk占有的空间。 每个Chunk占据空间不同,当一个类加载器(Class Loader)被gc时,所有与之关联的Chunk被释放(freed)。这些被释放的Chunk被维护在一个全局的释放数组里。MetaSpace的capacity指的是所有未被释放的Chunk占据的空间。 这么看gc日志发现自己committed是4864K,capacity4486K。有一部分的Chunk已经被释放了,代表有类加载器被回收了。
      • class space used 384K, capacity 388K, committed 512K, reserved 1048576K“,