Java GC 垃圾回收算法 内存分配
垃圾回收(Garbage Collection, GC)是Java不同于c与c++的重要特性之一。
他帮助Java自动清空堆中不再使用的对象。
由于不需要手动释放内存,程序员在编程中也可以减少犯错的机会。
利用垃圾回收,程序员可以避免一些指针和内存泄露相关的bug(这一类bug通常很隐蔽)。
垃圾回收实际上是将原本属于程序员的责任转移给计算机。
GC需要完成的3件事情:
哪些内存需要回收
什么时候回收
如何回收
1 回收那些对象?
在Java中采用可达性分析算法来判定对象是否存活,是否可以被回收。
这个算法通过一系列的被称为”GC Root”的对象作为根节点,
从他们开始向下搜索,搜索走过的路径被称为引用链(Reference Chain).
当一个对象没有一条引用链与GC Root 连接时,
即从GC Root 到这个对象是不可达的,说明这个对象是不可用的。
如图示:
object5 object6 object7 虽然互联互通
但是他们到GC Root是不可达的
所以他们将被判定为可以回收的对象
那么有一个重要的问题是 what is GC Root?
在Java语言中,可以看做是GC Root的是:
虚拟机栈中引用的变量 (可理解为方法中的局部变量)
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中的JNI(native方法)引用的对象
2 垃圾回收算法
2.1 标记-清除算法
顾名思义,该方法分为标记和清除2个过程
标记:将所有需要回收的对象区域进行标记
清除:清除所有配标记的区域里的对象
算法不足之处:
效率问题:标记和清除的效率都不高
空间问题:清除后产生的空间是不连续的碎片
无法满足后续运行中大对象的需求
2.2 复制算法
将整个空间划分2个相等的区域,每次只使用其中一个区域
当一块内存不够时,就将活着的对象复制到另一块内存
然后将第一块的内存全部回收。
这样每次对整个半区回收,就不会有内存碎片的情况,实现简单,运行高效
问题: 该算法的代价就是可以内存大小缩小为原来的一半
解决:现在商用的虚拟机都采取复制算法.
但由于所有的对象是朝生夕死的,所以并不是按照1:1的比例来划分内存的
而是将内存划分为一块较大的Eden区(new一个对象是就是在这里面分配空间)
和2块Survivor区域。每次使用Eden和一块Survivor。
当回收的时候,将Eden和Survivor区中还存活的对象一次性赋值到另一块Survivor中
最后清理掉原来使用过的Eden和Survivor区
这3个区域也被称为新生代。HotSpot的默认新生代各区域比例如下:
每次新生代中可用的空间为整个新生代的90%, 即80% Eden + 10% 1个Survivor
此时只有10% 1个Survivor 会被’浪费’
如果另一块Survivor区域存放不下Eden和Survivor区存活下来的对象
就要依靠其他区域来存放 即老年代
2.3 标记-整理算法
类似于标记-清除算法,先标记所有可以回收的区域,然后不是直接回收,
而是把所有存活的对象都移动到一端,然后直接清理掉端边界以外的区域
标记-整理算法和标记-清除算法常用于老年代的回收
老年代存放的对象存活的时间较长
而且垃圾回收的频率不如新生代的频繁
*对象分配规则
1.对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
2.大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
3.长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
4.动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
5.空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查
HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
*GC 类型
新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具
备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。
老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常
会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里
就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10
倍以上。
*触发Full GC执行的情况
除直接调用System.gc外,触发Full GC执行的情况有如下四种。
1. 旧生代空间不足
旧生代空间只有在新生代对象转入及创建为大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space
为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
2. Permanet Generation空间满
Permanet Generation中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,
Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。
3. CMS GC时出现promotion failed和concurrent mode failure
对于采用CMS进行旧生代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。
应对措施为:增大survivor space、旧生代空间或调低触发并发GC的比率,但在JDK 5.0+、6.0+的版本中有可能会由于JDK的bug29导致CMS在remark完毕后很久才触发sweeping动作。对于这种状况,可通过设置-XX: CMSMaxAbortablePrecleanTime=5(单位为ms)来避免。
4. 统计得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
例如程序第一次触发Minor GC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查旧生代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过- java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。
参考博客:Java垃圾回收机制