垃圾收集 Garbage Collection

前一篇博客介绍了Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域的内存是在线程中分配的;栈帧随着方法的进入和退出执行着入栈和出栈的操作。这几个区域的内存分配和回收都具备确定性,方法结束或者线程结束的时候内存就跟着回收了。而Java堆和方法区则不一样,这部分内存的分配是动态的,我们需要在程序运行时才知道会创建哪些对象,垃圾收集主要关注这部分内存。


1. 对象引用

Java堆里存放着所有的对象实例,垃圾回收器Garbage Collection)在对堆进行回收前,首先要确定哪些对象还被使用,哪些对象等待回收了。

1.1 引用计数

引用计数算法Reference Counting)是给对象增加一个引用计数器,有一处引用到它时,计数器就加1;当引用失效时,计数器就减1;当计数器为0的时候对象就是可以被回收的。这种算法简单高效,但是难以解决对象之间相互引用的问题。

1.2 可达性分析

可达性分析算法Reachability Analysis)就是以一系列的“GC Roots”对象作为起始点,从这些节点开始搜索,搜索所经过的路径称为引用链(Reference Chain),当一个对象到“GC Roots”没有任何引用链相连的话,则该对象是可以被回收的。GC Roots对象包括以下几种。

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

1.3 引用强度

在JDK1.2以前,Java中一个对象只有被引用或者没有被引用两种状态。某些时候我们希望能描述这样一类对象:当内存空间足够时,则能保留在内存中;如果内存空间在进行垃圾回收后还是很紧张,则可以回收这些对象。JDK1.2之后,Java将引用分为强引用Strong Reference)、软引用Soft Reference)、弱引用Weak Reference)、虚引用Phantom Reference),这四种引用强度依次减弱。

  • 强引用就是指在程序代码之中普遍存在的,类似“Object obj = new Object”这类引用,只要对象被强引用着,那垃圾收集器永远不会回收它。
  • 软引用是用来描述一些有用但非必需的对象。被软引用关联着的对象,在系统将要发生OutOfMemory之前,会把这些对象回收。
  • 弱引用也是用来描述非必需对象的,它比软引用要弱一些,弱引用关联的对象只能存活到下一次GC前,无论内存是否足够,都会回收掉该对象。
  • 虚引用是最弱的一种引用关系,一个对象是否有虚引用的存在完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的存在目的是在对象被GC回收的时候收到系统回调。

1.4 Finalize

一个对象被真正回收前至少要经过两次标记过程,如果对象在经过可达性分析后发现没有外部引用了,那它将被第一次标记并且进行一次筛选,筛选的条件是该对象是否有必要执行finalize()方法。当对象没有重写该方法或者该方法已经被虚拟机调用过时,虚拟机将这两种情况视为“没必要执行”。
.
如果这个对象被判定为有必要执行finalize()方法,那么该对象会被放置在一个队列中,稍后由一个虚拟机自动创建的、低优先级的Finalizer线程去执行它,这里的“执行”是指虚拟机会执行finalized()方法,当时不承诺会等待它运行结束。因为,如果一个对象在finalize中堵塞了,那很可能导致该队列其他对象永久等待,甚至整个GC Crash

1.5 回收方法区

方法区的垃圾回收主要包括两部分:废弃常量和无用的类。这和Java堆中的对象回收非常类似。如果常量池中的一个字符串“ohyeah”没有其他地方引用了,有必要的话这个常量就会被清理出常量池。判断一个常量是否“废弃常量”比较简单,而判断一个类是否“无用的类”的条件相对严苛。-Xnoclassgc参数可配置该回收行为。

  • 该类的实例都已经被回收
  • 加载该类的ClassLoader已经被回收
  • 该类的Class对象没有被其他地方引用,无法通过反射访问。

在大量使用反射、动态代理等ByteCode框架,频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。


2. 垃圾收集算法

各个平台的虚拟机操作内存的方法各不相同,这里仅讨论Hotspot虚拟机几种普遍的回收算法。

2.1 标记清除算法

标记清除Mark-Sweep)算法分为两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程在上文中已经介绍过了。这种是最基础的收集算法,它的缺点有两个:一个是效率问题,标记和清楚的两个过程效率都不高;另一个是空间问题,标记清除之后产生大量不连续的内存碎片,内存碎片太多导致在程序需要分配大对象时。无法找到足够的连续内存而导致OutOfMemoryError。

2.2 复制算法

为了解决标记清除算法的效率问题,复制Copying)收集算法出现了,它将内存按容量划分为大小相等的两块,每次只使用其中一块。当这块的内存快用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样是的每次都是对整个半区进行内存回收,也不用考虑内存碎片的复杂情况,只要移动堆顶指针,按顺序分配内存即可。这种算法的代价是将内存缩小为原来的一半。

现在的商业虚拟机大多采用这种算法来回收内存的。实际上新生代中的对象存活时间都是很短的,所以并不需要按1:1的比例来划分内存空间,而是将内存划分为一块较大的Eden区和两块较小的SurvivorFrom SurvivorTo Survivor)区,每次使用Eden和From Survivor(新生代)空间,回收的时候将Eden和From Survivor区中还存活着的对象一次性地复制到To Survivor(老年代)空间上,最后清理掉Eden和From Survivor的空间。HotSpot虚拟机默认Eden和From Survivor的比例为8:1,只有10%的内存会“浪费”。当From Survivor(老年代)空间不够用时需要To Survivor进行分配担保(Handle Promotion)。

分配担保是指如果From Survivor空间没有足够的空间存放上一次新生代手机下来的存活对象时。这些对象将直接通过分配担保机制进入To Survivor。

2.3 标记整理算法

复制收集算法在对象存活率较高的时候就要进行较多的复制操作,这样效率会变低。To Survivor区(老年代)中对象的时间较长,所以又有了标记整理Mark-Compact)算法,标记过程仍然与标记清除算法一样,整理时将所有存活对象都向一端移动,然后直接清理掉边界外的内存。

2.4 分代收集算法

当前商业虚拟机的垃圾收集都采用分代收集(Generational Collection)算法,这种算法根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特年采用最适合的收集算法,新政带中每次垃圾收集都有大批对象被回收,那就算用复制算法,只需要复制少量存货对象就可以完成收集。而老年代中因为对象的存活率高、没有额外空间对它进行分配担保,就必须使用标记清理或者标记整理算法来进行回收。

2.5 HotSpot的算法实现

查找引用链
GC Roots的节点主要在全局性的引用(例如常量或静态属性)与执行上下文(例如栈帧中的本地变量表)中,如果方法区非常大,那查找引用链必然消耗很多时间。
GC停顿
引用分析必需在一个能确保一致性的快照中进行,这里一致性指引用分析期间执行系统就像停止在某事时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,这点不满足的话分析结果的准确性就无法保证。这导致了GC时必需停顿所有Java执行线程( Stop The World)。

由于目前主流Java虚拟机使用的都是准确式GC,所以当执行系统停顿下来后,并不需要一个不漏地检查所有执行上下文和全局的引用位置,虚拟机从一组称为OopMap的数据结构中得知哪些地方存放着对象引用。在JIT编译过程中也会在特定位置记录下栈和寄存器中哪些位置是引用。这样就可以得知这些信息了。

2.5.1 安全点

OopMap的协助下,HotSpot可以快速准确地完成GC Roots枚举,但是如果每一条指令都生成对应的OopMap,那将需要大量的额外空间,这样GC的空间成本会变得很高。

实际上,HotSpt只有在“特定的位置”才生成OopMap,这些位置称为安全点(Safe point),即程序执行时并非所有地方都能停下来GC,只有在到达安全点才能暂停。Safe point必是以“是否具有长时间执行特征”为标准进行选定的,例如方法调用、循环跳转、异常跳转等这些指令才会产生Safe point。

对于Safe point,还有一个需要考虑的问题是如何在GC发生的时候让所有线程跑到最近的Safe point上再停下来。这里有两种方案可选。

抢占式中断( Preemptive Suspension
在GC发生时,把所有的线程中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。
主动式中断( Voluntary Suspension
当GC需要中断线程的时候,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志时就自己中断挂起。

2.5.2 安全区域

Safe point解决了正在执行的线程的进入GC的问题,当线程处于Sleep或者Blocked状态时,线程无法响应JVM的中断请求,这个时候就需要安全区域(Safe Region)来解决。

安全区域是指在一端代码片段中,引用关系不会发生变化。在这个区域中的任何地方GC都是安全的。当线程执行到Safe Region时,首先标识自己已经进入了Safe Region,这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。但是如果线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举,如果完成了那线程继续执行,否则它必须等待直到可以安全离开Safe Region的信号为止。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值