/ Afred's Blog / JAVA GC学习笔记

JAVA GC学习笔记

2015-10-17 posted in [编程之旅]

Java 垃圾回收概况

Java垃圾回收要做的是将那些消亡对象所引用的内存回收掉,主要做三件事情:

  1. 确定哪些内存需要回收
  2. 确定什么时候需要执行GC
  3. 如何执行GC

Hotspot认为没有引用的对象是消亡对象(引用可以分为四种),需要注意的是,Java Garbage Collector不是只决定内存回收,还决定了内存分配。

Java 内存区域划分

学习Java的垃圾回收之前,需要了解Java的内存区域划分,Java运行时的内存区域可以划分为如下几块: 程序计数器、虚拟机栈、本地方法区、堆区、方法区和直接内存。

程序计数器

程序计数器纪录当前线程的虚拟机字节码指令地址,是线程私有的,字节码解释器工作时会根据程序计数器的值获取下一条指令的地址。在上下文切换时,需要保存当前的程序计数器地址。

虚拟机栈

每一个线程都对应一个虚拟机栈,栈中存储的有局部变量表、动态链接、方法出口等。局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。可以用-Xss参数制定大小。

本地方法栈

本地方法栈和虚拟机栈的区别在于,前者是用来执行native方法的,后者执行Java方法,两者都是线程私有。在Hotspot中,本地方法栈和虚拟机栈时同一个,因此也可使用-Xss控制。

堆区

终于说到最重要的区域了,堆区时Java 垃圾回收的主要区域,堆区的空间是该JVM所有的线程共享,堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。堆区的内存大小可以通过-Xms-Xmx参数控制,其中的年轻代可以通过-Xmn控制。

方法区

方法区用于存储类信息、final常量、静态变量和编译器编译的代码等等。这块区域在垃圾回收机制中被划分为永久代,但是在方法区上执行的垃圾回收很少。运行时常量池也是方法区的一部分。在虚拟机规范中,方法区被划分为堆区的逻辑部分。方法区可以通过-XX:PermSize-XX:MaxPermSize参数控制。

直接内存

直接内存是JVM之外的内存,不受JVM管理。DirectByteBufferUnsafe.allocateMemeory,但不推荐。

垃圾回收

垃圾回收针对的是堆内存,Java内存管理和垃圾回收的原则可以概括为:分代分配和分代回收。一般情况下,应用的大部分对象存活时间都很短,采取这种方式可以提高垃圾回收的效率。在JVM中,内存区域被三个世代:年轻代、老年代和永久代,Java对象根据存活时间被放入不同的世代。

内存分配

年轻代

对象被创建时,内存分配首先在年轻代,如果这时年轻代空间不足或者该对象是大对象,也可能直接分配在年老代。在年轻代上执行的GC叫Minor GC或者Young GC,YGC并不代表年轻代空间不足,只是表示在Eden区进行的GC。

年轻代分为三个部分:Eden区和两个Survivor区(From和To,也叫Survivor0和Survivor1)。分配过程如下:

  1. 很大部分对象都是在Eden区上分配,由于Eden区是连续的空间,所有分配内存速度很快;
  2. 当Eden区满时,执行一次YGC,将消亡的对象清理掉,并将存活对象复制到Survivor0中,Survivor0和Survivor1中总有一个空间的利用率为0%;
  3. 之后每次Eden区满,都会执行步骤2;
  4. 如果Survivor0空间也满了,将仍然存活的对象复制到Survivor1,并将Survivor0中的所有对象清除,以后Eden区满,执行YGC时,会将存活对象复制到Survivor1;
  5. 当两个存活区切换几次之后(这个参数可以通过-XX:MaxTenuringThreshold配置,默认15次,也对应对象的存活年龄),仍然存活的对象将被复制到老年代。

年轻代的这种垃圾回收算法称为“停止-复制(stop-and-copy)”。年轻代的大小可以通过-Xmn指定,同时-XX:SurivorRatio配置Eden区域Survivor区的容量比值。

年老代

如果年轻代的对象年龄大于MaxTenuringThreshold,就会被分配到年老代,同时,大内存对象也可能直接分配到年老代(用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。),年老代的空间一般比年轻代空间大,能存放更多的对象,同时年老代垃圾回收的频率也会比年轻代低,当年老代空间不足时执行的GC,称为Major GC,JAVA规范并没有明确区分Major GC和Full GC,由于Major GC除了并发GC外均需对整个堆以及永久代进行扫描和回收,所以很多人视其为Full GC。

可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。

年老代的垃圾回收采用“标记-整理”算法,基本上是在空间已满时执行,具体的执行策略根据GC类型不同而不同。

方法区(永久代)

永久代的回收有两种:常量池中的常量,无用的类信息。 常量的回收很简单,没有引用了就可以被回收。对于无用的类进行回收,必须保证3点:

  1. 类的所有实例都已经被回收;
  2. 加载类的ClassLoader已经被回收;
  3. 类对象的Class对象没有被引用(即没有通过反射引用该类的地方)。

可以通过jstat命令观察堆中各个内存区间的变化以及GC的大概状态,该命令展示的是瞬时状态,通过参数可以指定输出频率,一般和GC log配合。在进程启动时,添加-XX:PrintGCDetails -Xloggc:<file>查看GC的状态,尤其是回收前后内存区间的占用情况。 另外还可以通过jmap -heap查看当前进程各个区间的大小。

调优基础

性能属性
  1. 吞吐量
    指不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支持应用程序达到的最高性能指标。
  2. 延迟
    衡量标准是缩短由于垃圾收集引起的停顿时间或完全消除因垃圾收集引起的停顿,避免应用程序执行过程中的抖动。
  3. 内存占用
    垃圾收集器运行流畅所需要的内存数量。
    以上三个指标中,提高其中的任何一个,都是以其他一个或者两个属性损失作为代价。

活跃数据大小:应用程序稳定运行时长期存活对象所占用的Java堆内存量。换句话说,它是应用程序运行于稳定态时,Full GC之后Java堆所占用的空间大小。 包括下面两块的内容:

  1. 应用程序运行于稳定态时,老年代占用的堆大小
  2. 应用程序运行于稳定态时,永久代占用的堆大小 为了更多好度量活跃数据的大小,需要在多次Full GC之后观察堆的大小。#
初始堆大小推荐
  1. 将堆的初始值-Xms和最大值-Xmx设置为老年代活跃数据大小的3~4倍。
  2. 永久代的初始值-XX:PermSize和最大值-XX:MaxPermSize应该比永久代活跃数据大1.2~1.5倍。
  3. 新生代空间应该为老年代活跃数据的1~1.5倍。
三个基本原则

垃圾收集器回收有三个基本原则:

  1. Minor GC回收原则
    每次YGC(Minor GC)都应该尽可能多的回收垃圾对象,以此减少应用程序发生Full GC的频率。
  2. GC内存最大化原则
    处理吞吐量和延迟时, 应用程序的内存越大,即堆越大,垃圾收集的效果越好,应用程序运行也流畅。
  3. GC调优的3选2原则
    在三个性能属性中选择任意两个进行调优。

晋升阈值

晋升阈值就是对象的年龄,一个对象的年龄就是它经历的Minor GC次数,当对象首次分配时,它的年龄为0。晋升阈值决定了对象在新生代保存的时间。

阈值计算的依据是Minor GC之后新生代要容纳的可达对象的空间大小以及目标Survivor空间占用的大小。 监控晋升阈值对避免Survivor空间溢出很重要。

实际的晋升阈值是内部计算的结果,而最大晋升阈值可以通过参数-XX:MaxTeuringThreshold=<N>设置,适当调整最大晋升阈值,可以减少不必要的开销,比如From SurvivorTo Surivivor不必要的对象复制。另外,可以通过-XX:+PrintTeuringDistribution观察各个年龄段的分布情况。示例如下:

2015-10-25T08:32:44.057+0800: 754776.883: [GC2015-10-25T08:32:44.057+0800: 754776.883: [ParNew
Desired survivor size 89456640 bytes, new threshold 15 (max 15)
- age   1:       2728 bytes,       2728 total
- age   2:     732632 bytes,     735360 total
- age   3:     109120 bytes,     844480 total
- age   4:      64760 bytes,     909240 total
- age   5:      29384 bytes,     938624 total
- age   6:    1260088 bytes,    2198712 total
- age   7:      40504 bytes,    2239216 total
- age   8:      36000 bytes,    2275216 total
- age   9:      56544 bytes,    2331760 total
- age  10:       1016 bytes,    2332776 total
- age  11:      16368 bytes,    2349144 total
- age  12:      17064 bytes,    2366208 total
- age  13:       3536 bytes,    2369744 total
- age  14:      20664 bytes,    2390408 total
- age  15:      25896 bytes,    2416304 total
: 5353K->3477K(349568K), 0.3519480 secs] 1210163K->1209377K(1922432K), 0.3525660 secs] [Times: user=1.92 sys=0.02, real=0.35 secs]

Desired survivor size是Survivor的空间大小乘以目标存活率得到的空间大小。max 15表示最大晋升阈值,new threshold 15表示内部计算的阈值。每个年龄的数据分布:第一个字段表示年龄,第二个字段表示当前年龄占用的空间大小,第三个表示总大小,是该年龄及其之前所有行对象大小的年龄之和。

通常来说,如果观察到的晋升阈值持续小于最大晋升阈值,或者Survivor的空间大小小于总的存活对象大小(最后最右列的值),都表明Survivor空间过小。

调整Survivor空间

调整Survivor空间时,需要注意的原则是:如果新生代大小不变,增加Survivor空间的大小,会缩小Eden区大小;而缩小Eden区会增加Minor GC的频率。

垃圾回收器

todo

GC 命令

todo

comments powered by Disqus