• JVM学习笔记二:垃圾收集算法


    垃圾回收要解决的问题:

    1. 哪些内存需要回收?

    线程私有区域不需要回收,如PC、Stack、Native Stack;Java 堆和方法区需要
    2. 什么时候回收?

    以后的文章解答
    3. 如何回收?

    首先进行对象存活性的分析,然后利用GC回收算法进行回收,具体算法请看下文。

    如何判断对象是否可以回收?

    有两种方式:引用计数算法和可达性分析算法,目前主流商业JVM普遍采用可达性分析算法

    引用计数算法

    引用计数算法顾名思义,为对象的引用计数,每当有一地方引用它时,计数器加1,引用失效(离开作用域时)减1,当计数器值为0时,对象就可以被回收了。引用计数算法优点是判定效率高,缺点是无法解决对象互相引用的问题,使用此技术进行内存管理的技术有微软的COM、Flash、Python等

    可达性分析算法

    可达性分析算法,基本思路是以GC Roots为根、向下搜索,所走过的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链可达时,就成为此对象不可用,即可以被回收。注意再注意:该算法的本质是通过找出所有活对象来把其余空间认定为“无用”,而不是找出所有死掉的对象并回收它们占用的空间。分代式GC是一种部分收集(partial collection)的做法。在执行部分收集时,从GC堆的非收集部分指向收集部分的引用,也必须作为GC roots的一部分。具体到分两代的分代式GC来说,如果第0代叫做young gen,第1代叫做old gen,那么如果有minor GC / young GC只收集young gen里的垃圾,则young gen属于“收集部分”,而old gen属于“非收集部分”,那么从old gen指向young gen的引用就必须作为minor GC / young GC的GC roots的一部分。
    jinjiprojectnaotu

    可以作为GC Roots的对象包括:

    1. 虚拟机栈 VM Stack 中局部变量表(Local Variables)中引用的对象
    2. 方法区中类静态属性引用的对象
    3. 方法区中常量引用的对象
    4. 所有当前被加载的Java类
    5. Java类的运行时常量池里的引用类型常量(String或Class类型)
    6. String常量池(StringTable)里的引用
    7. VM的一些静态数据结构里指向GC堆里的对象的引用,例如说HotSpot VM里的Universe里有很多这样的引用。
    8. 本地方法栈中JNI(Native方法)引用的对象

    对象引用类型的不同对存活周期的影响:

    JDK1.2之后,引用概念得到了扩展,分为强引用Strong、软引用Soft、弱引用Weak、虚引用Phantom

    1. 强引用:一般未指定的,都是强引用,包括Object o = new Object(),只要满足存活算法,就不会被回收
    2. 软引用:表示有用但非必须的对象,这些对象在系统将要发生溢出异常之前,进行一次回收,如果回收到还没有足够的空间,再抛出异常。
    3. 弱引用:其关联的对象只能生存到下一次垃圾回收之前。
    4. 虚引用:不会影响对象的生存时间,也就是说生存时间与强引用一样,设置虚引用的目的是得到垃圾回收的通知

    生存还是死亡?

    存活算法之后,不可达的对象被标记,并进行一次筛选,筛选的条件是“是否有必要执行finalize”,当对象没有覆盖此方法或此方法已经被调用过,都将视为没有必要执行,如果有必要执行,该对象将被放在一个称为F--Queue的队列中,并被一个虚拟机建立的线程去执行finalize方法,如果在方法中该对象成功建立了与GC Roots的引用链,他就可以从待回收列表中移除,并继续存活。

    回收方法区

    方法区采用永久代实现,相比堆,尤其是新生代,方法区回收的性价比极低,永久代的垃圾回收主要回收废弃常量和无用的类,参数-verbose:class可以查看类加载和卸载情况

    垃圾回收算法

    标记-清除算法Mark Sweep

    先标记,再删除,缺点是空间利用率低,清除之后会产生大量不连续的内存碎片,浪费内存空间。

    复制算法

    将内存分为使用和待用两部分,当使用部分快满时,将还存活的对象复制到另外待用部分,然后堆整个使用区域进行回收,优点是回收不会出现内存碎片、缺点是浪费了待用区域的内存空间。商业虚拟机以这种算法为基础进行了优化,以HotSpot为例,将内存分为较大的Eden空间和两个较小的Survivor空间,比例默认是8:1:1,可以通过参数调节,每次使用Eden和一块Survivor空间,回收时,将这两个空间存活的对象复制到剩余的一块Survivor空间中,并清理掉Eden和第一块Survivor空间,也就是说可用空间达到了90%,只浪费了10%,这里有个问题,如果存活对象的超过Survivor空间怎么办,这时要依赖一种被称为分配担保的机制(Handle Promotion),将多出的对象分配到老年代。

    标记整理算法

    适用于老年代这种存活率高的空间,相比标记清除算法,标记阶段一致,只不过在标记后,会对可回收对象进行整理,让存活对象向内存一个方向聚集,然后直接清理掉编辑额外的内存。

    分代收集算法

    根据存活周期的不同将内存划分为几块,一般把java堆分为新生代和老年代,然后根据不同年代的特点选择最适合的算法,比如新生代中的对象朝生夕灭,只有少量存活,比较适合复制算法,而老年代中对象存活率高,没有额外空间提供分配担保,必须使用标记-清除或标记-整理算法进行回收。

    HotSpot算法实现

    枚举根节点

    程序的执行是动态的,与之相关的引用关系也是动态的,所以GC roots的枚举和分析必须在一个能保证一致性的快照下进行,JVM是依赖被称为GC停顿的机制实现,在这个时间点会停顿所有的Java执行线程,即俗称的”Stop The World“,GC Roots的枚举过程并不是在内存中便利所有对象和执行上下文,那样效率无疑会很低,其实在Class加载和解析的工作中,HotSpot已经使用一组称为OopMap的结构对GC Roots进行标记,这样在GC过程中即可直接获得Roots信息。

    安全点 SafePoint

    那么GC停顿是如何让线程暂停的呢?有一个关键的概念叫安全点,顾名思义是指一些特定的位置,当线程运行到这些位置时,线程的一些状态可以被确定(the thread's representation of it's Java machine state is well described),比如记录OopMap的状态,从而确定GC Root的信息,使JVM可以安全的进行一些操作,比如开始GC。
    另一个问题是GC如何让线程在安全点上停顿下来,有两种方式可选,一种是抢先式中断,GC主动中断所有线程,发现有线程中断的地方不在安全点上,就恢复线程,让它继续跑到安全点上,一种是主动式中断,GC不主动中断线程,而是设置一个标志,线程执行时主动去轮询这个标志,发现标志为真就自动挂起,轮询标志和安全点是重合的。

    safepoint指的特定位置主要有:

    1. 循环的末尾 (防止大循环的时候一直不进入safepoint,而其他线程在等待它进入safepoint)
    2. 方法返回前
    3. 调用方法的call之后
    4. 抛出异常的位置

    安全区域 SafeRegion

    安全点没有解决一个问题,就是当线程不执行的时候(如进入Sleep或Blocked状态),这时候线程无法响应JVM的中断请求,这就需要安全区域这种机制来解决。安全区域是指一段代码范围,这个范围内任何位置引用关系都不会发生变化,也就是说在这些位置进行GC是安全的,你可以把安全区域看成被扩展了的安全点。
    线程在进入安全区域后会标识自己已经进入了安全区域,这样GC就会忽略这个线程,同样离开安全区域时,线程要检查GC是否完成了根节点枚举,如果完成,线程继续进行,否则继续等待。

    参考资料

    本文参考:《深入理解Java虚拟机》

  • 相关阅读:
    Foundation框架中一些类的使用
    Objective-C知识总结(5)
    Javascript 严格模式详解
    JS-数组冒泡排序
    JS--垒房子
    JS-小球碰撞反弹
    Js制作的文字游戏
    JS产生随机一注彩票
    JS编写背景图切换
    JS编写全选,复选按钮
  • 原文地址:https://www.cnblogs.com/MinnieChang/p/7271915.html
Copyright © 2020-2023  润新知