最近在看《深入理解Java虚拟机:JVM高级特性与最佳实践》这本书,觉得有必要记录一下. 如无说明,则图片是我用Google Drawings制作的, under CC BY-NC-SA 3.0 CN License.

Java运行时内存

先上图

java_runtime_memory

虽然Java中没有直接(明显)的指针操作,但是在内部的实现里用的还是指针的.在访问对象的过程中,有两种方式可以实现:句柄访问,直接指针.对象实际上是一个reference类型的数据,其中存储的是他自己的地址,通过句柄访问则是句柄的地址.

tow-method-to1

two-method-to2

VM options

IDEA设置

书中需要设置各种虚拟机参数,书里用的是Eclipse,这里介绍一下IDEA的怎么设置.

点击导航栏 Run -> Edit Configurations -> VM options.这样就可以对不同的main方法设置不同的参数了.

vmoptions

参数介绍

名称 描述
-XX:+PrintGC 打印 GC 信息
-XX:+PrintGCDetails 打印 GC 信息…
-XX:+PrintGCTimeStamps 打印 GC 信息..
-XX:+UseConcMarkSweepGC 使用 ParNew + CMS + Serial Old(备用)进行内存收集
-XX:+UseParallelGC 在 Server 模式下的默认值,使用 Parallel Scavenge + Serial Old.
-XX:+UseSerialGC 在 Client 模式下的默认值,使用 Serial Old + Serial 的组合收集器
-XX:SurvivorRatio= Eden 区与 Survivor 区的大小比值
-Xmn 新生代大小
-Xms 初始堆大小
-Xmx 最大堆大小

垃圾回收算法

根搜索算法

根搜索算法应该不算是垃圾回收算法,而是用来判断对象是否存活的算法.我为了方便就放到这里了.

基本思路就是通过一系列 GC Roots 的对象作为起点,从这些节点开始进行搜索.如果有对象是 GC Roots 不能到达的,就将对象标记为可清理的.

gc_roots

那么什么样的对象可以成为GC Roots呢?

  • 虚拟机栈中局部变量引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈(Native Method Stack)中的引用的对象.

分代算法

虚拟机大多采用这种分代收集的算法,也就是根据对象存活的周期将内存划分几个区域.一把把Java Heap 分为新生代和老年代.对不同的年代,使用不同的回收方法.

新生代一般采用复制算法.

老年代一般采用标记-清理 或 标记-整理 算法.

标记-清理

图片来自CSDN

顾名思义,这个算法分成两部分:标记和清理.标记部分就是用的根搜索算法.在标记后,统一回收被标记的对象.从图中可以看出,清理后,留下了大量的不连续的内存碎片.这可能会导致无法分配大对象,而触发另一次的GC.

复制

图片来自CSDN

复制(Copying)可以解决标记-清理算法产生大量内存碎片的问题.

它将可用内存分为等大的两块,每次使用其中的一块,当这一块内存用完时,将其中还存活的对象移到另一块内存中,然后一次性就把已使用的一块内存清理掉.

新生代一般采用这种算法,只不过,虚拟机里的并不是按1:1来分配内存的,而是分为一块大的Eden,和两块小的Survivor.每次使用Eden和一块Survivor,回收时将Eden中和已使用的一块 Survivor中的存活对象,移到另一块Survivor中(当然,在对象存活率高的时候,这一块Survivor可能不够用,那么就会依赖老年代了,(下面将讨论这个问题)),最后进行清理.

一般的,Eden和Survivor的比例是8:1;也就是说新生代的可用内存是整个新生代容量的90%(=80% + 10%).可以通过 -XX:SurvivorRatio 来设置比例.

标记-整理

图片来自CSDN

那么还是顾名思义…

标记-整理(Mark-Compact)分为两个部分:标记和整理,标记部分和"标记-清理"算法一样,整理就是将存活的对象都整理到一边,而后清理.

内存回收策略

图片来自网络 `

  • 对象优先在Eden分配

  • 大对象直接进老年代

  • 长期存活的对象将进入老年代

垃圾收集器

Minor GC 和 Full GC

  • 新生代GC(Minor GC)

  • 老年代GC(Full GC / Major GC)

这样一标注应该就清楚多了.

下面主要通过比较两种参数情况下的垃圾回收机制进行分析.

测试代码:

1
2
3
4
5
6
7
8
9
public static final int ONEMB = 1024 * 1024;

public static void test() {
    byte[] allocation1, allocation2, allocation3, allocation4, allocation5;
    allocation1 = new byte[2 * ONEMB];
    allocation2 = new byte[2 * ONEMB];
    allocation3 = new byte[2 * ONEMB];
    allocation4 = new byte[4 * ONEMB];
    }

UseSerialGC

参数设置为:

1
2
3
4
5
6
7
8
-Xms20m
-Xmx20m
-Xmn10m
-XX:+UseSerialGC
-XX:SurvivorRatio=8
-XX:+PrintGC
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails

此时的GC日志:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
0.103: [GC (Allocation Failure) 0.103: [DefNew: 8027K->358K(9216K), 0.0034545 secs] 8027K->6502K(19456K), 0.0034934 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation   total 9216K, used 4620K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff029918, 0x00000000ff400000)
  from space 1024K,  34% used [0x00000000ff500000, 0x00000000ff5598c8, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
 Metaspace       used 3235K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 360K, capacity 388K, committed 512K, reserved 1048576K

注意到,新生代占用了差不多(大概其他的使用吧)4MB,而老年代却是6MB.这是为什么呢?

在给 allocation4 分配内存的时候,Eden(8MB)已经使用了6MB,因此无法分配.发生了一次Minor GC,而三个2MB的对象(都是存活的)无法一次性放入另一个Survivor(1MB)里.所以就要依赖老年代了.把这三个对象移到了老年代.

这次的GC结束后,4MB的 allocation4 就可以分配到Eden里了.

UseParallelGC

参数设置为:

1
2
3
4
5
6
7
8
-Xms20m
-Xmx20m
-Xmn10m
-XX:+UseParallelGC
-XX:SurvivorRatio=8
-XX:+PrintGC
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails

GC日志为:

1
2
3
4
5
6
7
8
9
Heap
 PSYoungGen      total 9216K, used 8028K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 97% used [0x00000000ff600000,0x00000000ffdd7090,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 4096K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff000010,0x00000000ff600000)
 Metaspace       used 3251K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 362K, capacity 388K, committed 512K, reserved 1048576K

通过日志可以看到,老年代的内存占用为 4096kb=1024*4 .可以推测4MB的对象直接进入了老年代.因此也没有触发GC.(对不对就不知道了(:3」∠❀)_)

不过新生代占用了8028kb(7.8MB差不多),加起来11.8MB比对象总的10MB大啊(逃)…