Java垃圾回收机制理解一:垃圾回收算法

概叙:

之前描述了Java内存区域的分布,我们知道了虚拟机栈,本地方法栈,程序计数器3个内存区域是各自线程私有的,和线程同生同死。而虚拟机栈中栈帧中分配的内存的区域是随着栈帧的出栈也会跟着消亡。所以这几块区域的内存我们不需要过多的考虑的问题。但是java中堆中和方法区中内存是共享的,在堆中创建的实例对象可能会被多个线程所共有,这时候某一个线程的消亡并不能决定该实例对象是否需要回收。那么我们就会产生几个疑问,那么这个实例对象的内存什么时候需要被回收?怎么样被回收?java中的垃圾回收机制关注重点就是这部分内存。

1:如何判定一个实例对象存活(需要被回收)?

对于判定对象是否存活,主流有两种算法:引用计数法,可达性分析算法。

1.1 引用计数法

引用计算法其实原理很简单:给对象添加一个计数器,每当一个地方需要引用(java中引用的概念)该对象,那么该计数器相应就会加一,当引用失效则计数器减一。任意时刻如果计数器为0,则该对象已不可用。这种算法实现简单而且效率也很高,不少语言都是通过该算法来管理内存的例如python语言,FlashPlayer等。但是该算法会存在一个问题,它很难去解决对象之间相互引用的问题。

例如:


  class Test1 {public Test2 test2;}

  class Test2 {public Test1 test1;}

    public static void main(String[] args) throws Exception {

        Test1 test1 = new Test1();

        Test2 test2 = new Test2();

        //互相引用
        test1.test2=test2;
        test2.test1=test1;
        //置null 这两个对象不可能被访问,按道理这两对象所在内存应该被释放了
        //但是实际因为对象互相引用导致两者引用计数都不为0 导致使用引用计数算法无法使这两对象所在
        //内存释放
        test1 = null;
        test2 = null;
}

1.2 可达性分析算法

对于主流的开发语言(Java,C#)都是可达性分析算法(Reachability Analysis)来判断对象是否存活的。算法的核心:以一系列的名为"GC Root"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径被称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连。则证明此对象是不可用的。

在Java中被称为GC Root的对象包括以下几种:

  1. 虚拟机栈中引用的对象, 如局部变量表中局部变量和入参的参数。
  2. 本地方法栈中Native Method的局部变量和入参的参数
  3. 全局性引用如静态变量和常量

2:有哪些流行的垃圾回收算法?

2.1:标记-清除算法

标记清除是最基础的算法,从字面上就可以知道,该算法有两个过程标记与清除。标记:如果一个对象被认定为不可用,则标记为可进行回收。清除:回收该对象内存空间。该算法存在两个问题,一是效率问题标记与清除两个过程的效率都不高;二是空间问题,使用标记清除算法会产生内存碎片。如果内存碎片过多就会导致在分配大的内存对象时候无法分配,从而导致下一轮gc,最终导致系统不可用。

2.2:复制算法

为了解决内存碎片和效率的问题,新的算法被提出来-复制算法,该算法的核心将内存分为两快,每次只是用一块,当需要进行gc时,将未被标为回收的对象复制到另外一快内存,然后清空第一块内存。这样就不会再产生内存碎片的问题了实现也很简单,不过次算法有个缺点在于内存浪费,因为每次都有一半的空间处于浪费之中。 

 

 2.3:标记-整理算法

与标记清除算法相比标记整理算法就就多了一个整理的过程,该整理的过程实际是让所有存活的对象都向一段进行移动,然后直接清除掉端边界以外的内存。标记清除算法中的弊端-内存碎片的问题就解决了。

2.4:分代搜集算法

目前所有主流的垃圾回收器都采用了分代搜集算法,该算法是按照对象的成活周期的不同设置不同区域,不同区域使用不同的垃圾回收算法。大部分(包括主流Sun HotSpot)JVM对堆中内存进行分区,分为新生代,老年代,永久代(HotSport 1.7之前包含其它类型JVM不一定)。对于新生代中的对象每次gc都会有大量的对象死亡,只有少量的对象存活,那么我们采用复制算法达到快速清理过程。老年代中都是已经存活一定时间的对象,也没有额外空间进行担保(后续会描述为啥有空间担保这一说法),所以采用标记清除或标记整理算法。

以上说了这么多,那么我们最关注的其实就是Sun HotSpot中的使用什么垃圾回收算法?

3:HotSpot中的垃圾回收(以下是JDK1.8) 

HotSpot将堆中分为新生代与老年代,且两者之间内存比例为1:2。对于新生代使用复制算法来进行gc,但是由于新生代中的对象是朝生夕死,所以对于新生代的分区没必要使用1:1来进行分配,而是将内存分为一块大的Eden区和两块相同的Survivor空间。其中3者的比例默认为8:1:1(可通过配置SurvivorRatio来进行设置)。这样jvm进行gc的时候最多浪费10%的内存而不是50%。但是这其中会存在一定的风险。如果说Eden区进行gc之后对象内存超过了10%,此时survivor区已经无法存放这些对象,此时就需要依赖于其它内存(担保人)用于分配担保(类似于在银行贷款买房这种情况,如果银行评估买房者没有充足的还款能力时,这么时候银行就要买房子提供一个担保人信息如果当买房子不能还款时,就从担保者账号中扣除)策略了。这里老年代就充当担保人这个角色。

以下是HotSpot整个GC过程

3.1 Minor GC与Major GC/Full GC

在整个垃圾回收器工作过程中会存在两种不同的gc动作:Minor GC与Full GC,那么这两种GC有什么不同的地方吗?

Minor GC:发生在新生代的垃圾收集动作,因为新生代对象的特点(朝生夕死),所以Minor GC发生的频率非常高,速度也非常快。哪些情况会触发Minor GC: 

  1. 当新生代中Eden区满了,此时就会触发MinorGC,但是Survivor区满了不会触发
  2. Eden区剩余内存无法对新创建的对象分配内存空间将触发

在Minor GC时整个系统会处于一个停止的状态(stop-the-world),除了gc的线程处于执行状态,其它线程都处于停止状态。整个MInor GC过程中应用停止的状态一般持续都很短暂(微秒级别),只有Eden中存在大量的不符合gc条件的,时间才会延长。

Major GC/Full GC:对于Major GC/Full GC两者我查了很多资料都没很精确的概念的定义(在深入理解Java虚拟机这一本书有这么一句描述:Major GC/Full GC指发生在老年代的GC,出现了Major GC,经常会伴有至少一次的Minor GC(但非绝对的,在Parallel Sca venge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。)。对于此段话结合一些资料,个人对Major GC/Full GC的理解为:Major GC与Full GC都是发生在老年代GC,在Major GC之前触发了Minor GC,两次gc的总称为Full GC。相对于Minor GC来说Full GC发生的频率较低(如果Full GC发生的频率较高,说明系统存在性能问题需要进行优化)

哪些情况会触发Major GC/Full GC: 

  1. 从新生代晋级(虚拟机会对每一个对象定义一个对象年龄(Age)计数器,如果对象从第一次Minor GC开始存活下并被移动到Survivor中,设置年龄为1。在Survivor中每经历过一次Minor GC年龄加一,只要达到一定值(默认为15-可以通过-XX:MaxTenuringThreshold=15设置)就可以达到晋升条件。但上述条件不是唯一是晋升条件,如果在Survivor中相同年龄的所有对象大小的和大于Survivor的空间一般,那么所有年龄大于或者等于该年龄的对象都达到晋升条件)到老年代的对象所需内存空间大于老年代的剩余空间时会触发Full GC
  2. 元空间空间不足的时会触发Full GC
  3. 当老年代剩余空间不足以存放因担保机制新生代的复制过来的对象会触发Full GC
  4. System.gc() 但可以通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc 会触发Full GC
  5. 大对象(多大的对象才算是大对象,可以根据-XX:PretenureSizeThreshold=1024*1024参数设置,这个参数只对Serial和ParNew两中垃圾收集器有效)会直接分配到老年代中所以在大对象的所需的内存空间大于老年代剩余空间会触发Full GC

4:理解GC日志

理解GC日志是处理Java虚拟机内存问题的基本技能,通过观察GC日志可以查看系统的运行过程

GC Demo日志

GC日志开头的[GC 其实有两种 第一种为[GC 第二种为[FULL GC 这两个的意思不是区别新生代GC还是老年代GC而是说明这次GC停顿的类型。如果开头是FULL GC代表此次GC是发生了Stop-The-World的。

具体时间数据(有些垃圾回收器是不会给出具体时间):

  1. user :用户态消耗CPU时间
  2. sys:内核态消耗CPU时间
  3. real:操作从开始到结束所经历的墙钟时间(Wall Clock Time)

其中CPU时间与墙钟时间的区别是墙钟时间包括各种非运算的等待时间例如io等待。CPU时间不包括这些,但是如果系统是多核或多CPU时,多线程操作会叠加CPU时间,所以user或sys时间可能会超过real时间

GC之后新生代,老年代,元空间内存分布情况:

测试demo1:对象在老年代分配担保 


/**
 * 查看gc日志
 * 设置堆最大内存为30m -Xms30m -Xmx30m
 * 那么年轻代内存大小为10m(Eden为8m 两个Survivor区分别为1m) 老年代为20m
 * @author fangyuan
 */
public class GCDetailsTest {

    private static final int _1MB = 1024 *1024;

    public static void main(String[] args) {
        //1
        byte[] memoryTow1,memoryTow2,memoryTow3,memoryFour;
        //2
        memoryTow1 = new  byte[2*_1MB];
        //3
        memoryTow2 = new  byte[2*_1MB];
        //4
        memoryTow3 = new  byte[2*_1MB];
        //5
        memoryFour = new  byte[4*_1MB];
        //查看gc日志
    }
}

第一步:新生代内存分布:

第二步:创建第一个大小为2m的对象memoryTow1

 

第三步:创建大小为2m的对象memoryTow2

第四步:创建大小为2m的对象memoryTow3 此时Eden区剩余大小不足以存放memoryTow3对象,所以触发第一次Minor GC,

因为Survivor区大小不足以存放这些对象,通过分配担保机制这些对象提前从新生代直接转移到老年代中

第五步:再创建4m的memoryFour时

测试Ddemo2:大对象直接进行老年代(使用ParNew垃圾回收器) 

/**
 * 设置大对象直接分配到老年代中
 * 设置堆最大内存为30m -Xms30m -Xmx30m -XX:+PrintGCDetails
 * -XX:PretenureSizeThreshold=3145728
 * -XX:+UseParNewGC
 *
 * @author fangyuan
 */
public class BigDataToOldThreshold {

    private static final int _1MB = 1024 *1024;

    public static void main(String[] args) {
        //直接分配
        byte[] memory = new byte[4*_1MB];

    }
}

内存日志

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值