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

时间:2023-03-09 00:55:43
JVM学习笔记二:垃圾收集算法

垃圾回收要解决的问题:

  1. 哪些内存需要回收?

线程私有区域不需要回收,如PC、Stack、Native Stack;Java 堆和方法区需要

  1. 什么时候回收?

以后的文章解答

  1. 如何回收?

首先进行对象存活性的分析,然后利用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的一部分。
JVM学习笔记二:垃圾收集算法

可以作为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虚拟机》