java虚拟机--对象死亡判定、垃圾收集算法、HotSpot的算法细节实现

对象已死?
  • 判断对象是否已死(可以被回收)的方法
    • 引用计数法
    1. 实现方式:为每个对象添加一个引用计数器,当一个地方引用了该对象,计数器加1,当引用失效,计数器减法1,,计数器减为0是,该对象就可以被清理。
    2. 存在的问题:难以解决对象的循环引用。故java虚拟机中没有使用引用计数器来管理内存。
    • 可达性分析法
    1. java虚拟机通过可达性分析判定对象是否存活。
    2. 实现过程:从GC Roots开始,根据引用关系搜索(搜索所走的路径成为引用链),如果GC Roots到某个对象不可达,那么说明该对象以不被使用。
    3. 可作为GC Roots的对象有:
      (1)虚拟机栈中的引用。(栈帧中的局部变量表中的引用)–java方法中引用的对象
      (2)本地方法栈中引用的对象
      (3)方法区中静态变量引用的对象。–类变量引用的对象
      (4)方法区中常量引用的对象。如字符串常量池中的引用的对象
      (5)虚拟机内部引用。如Class对·象、异常对象、类加载器
      (6)被synchronized锁定的对象
      (7)在对堆中的某个区域进行垃圾回收时,必须考虑该区域的对象被其他区域引用,故与该区域关联的区域中的对象也应该考虑在GC Roots中。
    • java中的引用分类
      传统的引用的定义,如果reference类型的数据中存储的数值代表订一块内存的起始地址,就称reference数据代表某块内存、某个对象的引用
      • 强引用
        即传统的引用定义,Object o = new Object()(即o是对象new Object()的引用,o是一个reference类型,存储的是对象的地址)。垃圾收集器永远不会回收强引用所引用的象。
      • 软引用
        用来描述一些还有用,但非必须的对象。系统发生内存溢出前,会把软引用所引用的对象进行二次回收,如果这次回收后,还是没有足够内存,才抛出内存溢出异常。可以用SoftReference类来实现软引用。
      • 弱引用
        也是描述那写有用,但是非必须的对象,但是强度比软引用弱一些。它们只能生存到下一次垃圾回收发生,在进行垃圾回收时,都会回收被弱引用所引用的对象。可以通过WeakReference类实现。
      • 虚引用
        最弱的引用。无法通过虚引用获取一个对象。为一个对象设置虚引用的唯一目的是为了能在该对象被回收时收到一个系统通知。可以通过PhantomReference实现。
    • finalize()方法:给对象逃脱死亡的最后一次机会。
      • 只有经过两次标记,才能判定一个对象死亡:
        (1)进行可达性分析时,该对象对于GC Roots不可达(即不存在到达该对象的引用链),那么该对象被第一次标记
        (2)在经过第一次标记后,进行一次筛选,如果该对象的finalize()方法被覆盖并且没有被虚拟机调用过(如果对象没有重写finalize方法或者该方法已经被调用过,则该对象只能被回收,不存在存活的可能),那个该对象会被放置到F-Queue对象中,稍后虚拟机会创建一条低优先级的Finalizer线程去执行对象中的finalize()方法。之后收集器将会对F-Queue中的对象进行第二次小规模标记,如果对象在finalize()方法中,与引用链上的任何对象建立了关联(如将this赋值给类变量或者成员变量),那么该对象会被移出“即将回收的集合”。
    • 回收方法区
      • 方法区中可以不进行垃圾回收,尽管进行垃圾回收,回收率也比较低。
      • 方法区中主要对两部分内容进行回收:
        (1)废弃的常量(字面量、符号引用)
        回收方法区的常量与回收堆中的对象非常相似。当常量池中的常量,没有任何被任何地方引用时,垃圾回收器可以回收该常量。
        以常量池中字面量的回收为例,若字符串“abc”已经进入常量池中,但当前系统没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用该字面量,若发生内存回收,且必要的话,该“abc”就会被系统清理出常量池。常量池中其他的类(接口)、方法、字段的符号引用与此类似。
        (2)不再使用的类型(即class对象,类卸载)。判定一个类型不再被使用必须具备一下三个条件:(a)该类的所有实例都已经被回收,即堆中不存在该类及其派生子类的实例。(b)该类的类加载器被回收。(c)该类的Class对象没有在任何地方被使用,即无法通过反射方位该类的方法。
垃圾收集器算法
  • 分代收集理论

    • 基于(1)绝大多数对象都是朝生夕灭、(2)熬过越多垃圾收集过程的对象就越难以消灭,这两个理论基础,垃圾收集器将java堆划分为不同的区域,然后将回收对象依据年龄(年龄就是对象熬过收集过程的次数)分配到不同的区域之中存储。(即将容易被回收的对象放在同一个区域,将不容易回收的对象放在另外一个区域,这样只需要对前一个区域的回收频率高一些,后一个区域频率低一些就行了,垃圾回收器也就可以一次只回收一个区域)
    • 一般将堆划分为新生代、老年代两个区域。即在新生代存放的就是朝生夕灭的对象,每次对该区域进行垃圾回收都能回收大批对象,只有少部分对象存活。新生代中每次经过垃圾回收存活下来的对象,年龄增加1,之后到达一定年龄,将存储到老年代。
    • 对新生代进行垃圾回收时,并不能只对新生代进行可达性分析,因为有可能新生代的对象被老年代引用,但是毕竟跨代引用占少数,不用为了少数的跨代引用去扫描整个老年代,采用的做法是:在新生代建立一个全局数据结构(记忆集),该结构将老年代划分为若干小块,标识出老年代中的哪一块乃村存在跨代引用。之后对新生代进行垃圾回收时,查看记忆集,只有被标识的老年代块里的对象,才被加入到GC roots中进行可达性分析。
  • 垃圾收集算法

    • 标记-清除算法
    1. 实现过程:该算法分为标记、清除两个阶段。首先通过可达性分析标记出所有需要回收的对象,标记完成后,统一清除掉所有被标记的对象。
    2. 该算法缺点:(1)效率收到对象数量的影响。如果堆中存在大量对象,其中大部分可以被回收,此时必须进行大量标记和清除工作,即标记和清除两个过程执行效率随着对象数量的增加而降低。(2)标记、清除两个过程后会产生大量的不连续的内存碎片,这样当为大对象分配内存时,堆中没有足够大的连续内存时,会出发一次垃圾回收。
      在这里插入图片描述
    • 标记-复制法(新生带采用)
    1. 实现过程:将可用内存划分为大小相等的两块,每次只使用一块,当这一块用完了,就将活着的对象复制到另外一块内存上,然后把用完的那块内存空间的对象一次性都回收。
    2. 缺点:可用内存缩小了原来的一半,浪费空间。
    3. 目前商用虚拟机有限采用这种回收算法回收新生代,新生代中98%对象会在第一次垃圾收集时被回收,因此也不用把新生代按照1:1划分。
    4. 目前新生代采用的回收算法(Appel式回收算法在标记-复制算法基础上进行优化):把新生代划分为一个Eden空间和两个Survivor空间(Eden较大,Surivor较小),每次分配内存只使用Eden和其中一块Survivor。发生垃圾回收时,对Eden和Survivor进行垃圾回收,并且把存活的对象一次性复制到另外一块Survivor空间上(但是有可能存活的对象需要的空间大于一个Survivor空间,导致没法完全复制到Survivor,那么这些存活的对象将直接进入老年代(分配担保)),之后直接清理到Eden和用过的Survivor空间。HotSpot虚拟机中,默认的Eden:Survivor=8:1,这样新生代中可用的内存空间为90%。因为要进行分配担保,因此老年代一般不采用标记-复制算法。
      在这里插入图片描述
  • 标记-整理算法

    • 实现过程:首先在内存中进行可达性分析,标记需要被清除的对象,标记完成后,让存活的对象向内存空间一段移动,然后直接清理掉边界以外的内存。
    • 缺点:在老年代类,每次回收都会存活大量的的对象,采用标记整理算法,那么要移动大量的对象,而且这种移动过程必须全程暂停用户应用程序磁能进行(Stop the World)。
      在这里插入图片描述
HotSpot算法实现细节
  • 根节点枚举

    • 判断独享死亡的可达性分析分为两个步骤,(1)枚举根节点(即GC roots)(2)从根节点开始查找引用链。所有的垃圾收集器在进行第一步骤的时候,都必须暂停用户线程(Stop the word);第二步骤是耗时最长的,这一步可以与用户线程并发。
    • 在枚举根节点时,只需要从OopMap数据结构中找根节点。在一组OopMap数据结构中记录了哪些地方存放着对象的引用。
    • 根节点的枚举始终必须在一个能保障一致性快照中进行,这样不会出现在枚举过程中,根节点集合的对象引用关系还在不断变化,保证分析结果的准确性。
  • 安全点

    • HotSpot只有为“具有让程序长时间执行特征”的指令生成OopMap(如果为每个指令都生成OopMap需要大量额外空间),生成OopMap的指令处称为安全点(因为只有到了这个地方,才能有OopMap,这样才能枚举到Gc Roots),通常具有让程序长时间执行特征的指令有:方法调用、循环跳转、异常跳转等指令,故安全点通常在这些地方。
    • 在垃圾收集时,所有线程必须到最近的安全点,然后停顿下来。采取的方案:(1)抢先式中断:垃圾收集发生时,系统把所有线程中断,如果发现有线程不再安全点上,就恢复这条线程,让他运行到安全点上,然后再中单。(2)主动式中单:垃圾收集发生时,设置一个标志位,每个线程在执行过程中不停的轮询该标志,发现标志为真则到最近的安全点上主动中断。(线程轮询该标志为的地方就是安全点,另外再加上所有创建和需要在堆上分配内存的地方,因为这些地方可能发生垃圾收集)
  • 安全区域

    • 当线程没有执行(没有被分到处理器,可能处于Sleep或者blocked状态),那么就不可能执行到安全点,这样就引入了安全区域来解决。
    • 安全区域是指:在一段代码中,引用关系不会发生变化。因此在这个区域任何地方发生垃圾回收都是安全的。
    • 用户线程执行到安全区域,会标识自己已经进入安全区域,这样虚拟机发起垃圾收集,即不用管这些线程。当线程要离开安全区域,要检查虚拟机时候完成了Gc roots的枚举,如果完成,就继续执行,若没有完成,则继续等待,知道收到可以离开安全区域的标志。
  • 记忆集与卡表

    • 由于存在跨代引用,因此垃圾收集器在新生代中建立了记忆集,用来避免把整个老年代加入GC roots的扫描范围。
    • 记忆集:是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
    • 记忆集只是一个抽象的数据结构,而卡表是记忆集的具体实现:

CARD_TABLE[this address >> 9] = 0
CARD_TABLE数组的每一个元素都对应着其标识的内存区域中一块特定大小的内存块(卡页),一般来说卡页大小都是以2的N次幂的字节数(通过以上代码,可以看出HotSpot中使用的是2的9次幂即512字节),这样内存区域就被划分为若干卡页,如果一个卡页中至少一个对象的字段存在跨代指针,那么对应卡表的数组元素的值表示为1(称为这个元素变脏),否则表识为0.这样垃圾收集时,只要筛选出CARD_TABOLE数组(卡表)中值为1的元素,就能得出对应的哪些内存中存在跨代引用。

  • 写屏障
    • 在HotSpot虚拟机里通过写屏障技术维护卡表状态。
    • 写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形通知,也就是说在赋值前后都在写屏障范畴内,赋值前部分的写屏障叫写前屏障,赋值后的叫写后屏障。写后屏障逻辑如下:
void oop_filed_store(oop* field, Oop new_value){
//引用字段赋值操作
*field = new_value;
//写后屏障,在这里完成卡表状态更新,老年代对象中的字段引用了新生代的对象,
//那么老年代中的这个==对象所在的内存区域对应的卡表元素就会变脏(为1)
post_write_barrier(field, new_value);
}

卡表在高并发场景下还面临着“伪共享”问题。现代中央处理器的缓存系统中是以为缓存行为单位存储,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、无效化、或者同步)而导致性能降低,这就是伪共享问题。为了解决伪共享问题,可以采用先检查卡表的标记,当卡表元素未被标记过时,才将其标记为变脏:逻辑如下:

if(CARD_TABLE[this address >> 9] != 0){
	CARD_TABLE[this address >> 9] = 0;
}
  • 并发的可达性分析(即在进行可达性分析时,用户线程和垃圾收集器线程可以并发执行)
    在并发的进行可达性分析过程中,灰色对象的一个引用被切断了,但是被切断的对象被黑色对象引用了,浙江导致被切断的这个对象不能到达GC Roots,将会被误回收
    在并发的进行可达性分析过程中,灰色对象的一个引用被切断了,但是被切断的对象被黑色对象引用了,浙江导致被切断的这个对象不能到达GC Roots,将会被误回收
    • 解决并发标记过程中对象消失(即有的对象漏标记)的方法
      (1)增量更新
      黑色对象(该对象实例数据中的所有引用都已经被扫描过)一旦新插入了指向白色对象(该对象还没有被收集器访问过)的引用,那么它就变为灰色(该对象已经被访问过,但是存在至少有一个引用没有被扫描)了,之后要需要进行扫描。
      (2)原始快照
      当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值