垃圾回收主要针对堆区

程序计数器,本地方法栈,虚拟机栈是随着线程生死的,在线程结束之后内存就会被回收了,所以不需要特殊的垃圾收集。
一个栈帧,存的局部变量表在类结构确定之后就确定了(编译期已知)。
栈帧在方法结束或线程结束之后就内存回收了。
而堆区的内存分配和回收全是动态的,所以需要有一个收集策略。

如何判断对象已死

1、引用计数
有一个引用指向这个对象,引用加一。减少一个引用指向这个对象,引用减一。当引用计数为0时,回收对象。
这样有一个问题,加入两个对象的熟悉属性分别引用着彼此,那么这两个对象永远不会回收。

obj1.instance = obj2;
obj2.instance = obj1;

2、可达性算法分析
通过一系列“GC-Roots”的对象作为起始点,向下遍历树,走过的路经就是引用链。当从“GC-Roots”无法达到这个对象时,则证明此对象不可用,需要回收。
可以作为GC-Roots中的对象有:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(native方法)引用的对象
    这里写图片描述

3、引用分类
上面两种方法都和引用相关,我们需要对引用进行分类。定义出这样的对象,在内存足够时,这个对象可以被留下;内存紧张时,这个内存应该被舍弃。

  • 强引用
Object obj = new Object()

普遍存在,永远不会被收集

  • 软引用

描述一些还有用但非必须的对象。在内存溢出异常之前,这些对象将会被垃圾回收(第二次,之前第一次不会被回收)。如果内存还是不够,抛出内存溢出异常。

  • 弱引用

比软引用更弱,当被标记成弱引用之后,下次垃圾回收一定会被收
集。

  • 虚引用

最弱的引用,立即被收集。系统设置虚引用的目的只是为了在垃圾回收的时候,能通知到系统。

4、生存还是死亡
真正收集一个对象,至少有两次标记。如果对象不可达,标记对象,筛选。筛选是指,判断是否需要执行对象的finalize方法,如果对象没有覆盖finalize或者finalize已经被调用过,就是不需要执行。
如果要执行finalize,把对象放入F-Queue队列。之后由finalizer线程执行,会执行,但是并不承诺执行结束(比如有死循环的,超时自动就退出了)。
一个对象的finalize只会被执行一次,在这次执行时,对象可以把自己和一个可达的引用关联上,以此拯救自己(书上说不推荐)。

5、回收方法区
HotSpot会回收永久代。
回收两部分:废弃的常量,没用的类。

  • 废弃常量

和回收堆中的对象很类似,比如常量池中有一个”abc”,但是在GC-Root中没有任何一个引用指向这个String,那么需要回收这个”abc”。

  • 无用的类

这个类的全部实例都已经被回收
加载这个类的ClassLoader已经被回收(ClassLoader是啥
这个类对应的java.lang.Class对象没有任何一个地方被引用,无法通过反射生成这个类,访问这个类的方法

这里无用的类只是可以被回收,并不是一定被回收。
在大量使用动态代理,cglib,反射,动态生成JSP这类频繁定义ClassLoader的场景都需要虚拟机有类卸载的功能,以保证永久代不会溢出。

垃圾收集算法

1、标记清除
垃圾回收前:
这里写图片描述
垃圾回收后:
这里写图片描述
绿色:存活对象 红色:可回收对象 白色:未使用空间

不足:
标记和清除效率不高
碎片太多,导致在大对象分配前,必须GC

2、复制算法
这里写图片描述
这里写图片描述
将可用的内存按照容量划分成大约相等两块(8:1:1),一块内存用完之后,复制到另一块,再把原来使用过的内存一次清理掉。这样每次GC只对整个半区进行回收。
HotSpot的分配比例是,Eden:Survivor:Survivor = 8 : 1: 1

3、标记整理算法
适合用于老年代的算法(存活对象多于垃圾对象)。
标记后不复制,而是将存活对象压缩到内存的一端,然后清理边界外的所有对象。
这里写图片描述

4、分代收集
新生代:因为有大批的死亡,所以复制的代价小,也不用整理,采用复制算法。
老年代:对象基本长时间存活,复制成本高,没有额外空间担保,使用标记清理或者标记整理算法。

HotSpot的算法实现

1、枚举根节点

采用准确式GC,从外部记录下类型信息,存成映射表,称作OopMap。
在类加载完成后,使用OopMap来记录对象中哪些是引用。
所以,在类加载完成后,其实就知道GC-Roots相关的树结构了。

2、安全点
因为java程序指令非常多,如果为每条指令都生成OopMap,会占用太多空间。
HotSpot并没有为每条指令生成OopMap,只是对处于某些位置的指令生成了OopMap。
这些位置叫安全点,也是垃圾收集触发的地方,只在安全点上才会触发GC。
安全点的选取特征:“指令长时间执行”,如方法调用,循环,异常跳转。这些指令会产生安全点,执行到这些指令时,读取对应的OopMap,触发GC。
之所以这么选安全点,是为了让程序执行过程中用户感觉不到垃圾收集。比如在一个循环中加入垃圾收集,因为循环本来就很耗时,所以时间再长些也没什么。而如果在一个用户交互中选取安全点,那么用户就会明显感觉到程序的停顿了。所以有安全点的选取。

那如何让所有线程都在安全点呢?
抢先式:GC抢先发生,然后让没到安全点的线程继续执行到安全点,所有线程暂停,执行GC。
主动式:GC需要中断时,设置一个标志,各个线程执行时主动轮询这个标志,发现标志为真的时候就中断。标志的位置就是安全点。
现在没有使用抢先式来中断线程了。

3、安全区域
安全点解决的是线程执行时的中断,但是在线程不执行时怎么办?比如block和sleep,线程无法响应JVM中断,无法走到安全点。
安全区域是指在一段代码中,引用关系不会发生变化,在这个区域任何地方GC都是安全的,是扩展的安全点。
在线程执行到Safe Region中后,会标识自身为Safe Region。GC不会收集Safe Region标识的线程。
在线程要离开Safe Region时,需要检查是否完成了根节点枚举(或者GC收集整个过程)。
如果完成了,就继续执行,没完成,需要等待完成才能离开Safe Region。

垃圾收集器

实现了垃圾收集算法的垃圾收集器,HotSpot中是这样的:
这里写图片描述

  • 新生代的垃圾收集器

Serial收集器、ParNew收集器、Parallel Scavenge收集器

  • 老年代的垃圾收集

Serial Old收集器、Parallel Old收集器、CMS收集器、G1收集器
Serial收集器/Serial Old收集器

  • Serial 收集器

是一个单线程的收集器,只能使用一个CPU或一条线程去完成垃圾收集;在进行垃圾收集时,必须暂停所有其他工作线程,直到收集完成。

Serial/Serial Old 收集器运行示意图:
这里写图片描述
Serial_gc_runtime

缺点:Stop-The-World。

优势:简单。对于但CPU的情况,由于没有多线程交互开销,反而可以更高效。

是Client模式下默认的新生代收集器。

  • ParNew 收集器

是Serial收集器的多线程版本。

ParNew/Serial Old 收集器运行示意图:
这里写图片描述
ParNew_gc_runtime

垃圾收集语境下的并发与并行概念

并行(Parallel):指多条垃圾收集线程并行工作,用户线程仍然处于等待状态。
并发(Concurrent):用户线程与垃圾收集线程同时执行。
Parallel Scavenge 收集器

新生代收集器,使用复制算法、并行的多线程收集器。

Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量(Throughput)。这里的吞吐量是指CPU用于运行用户代码的时间与CPU总消耗时间的比值。主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge 收集器允许采用GC自适应的调节策略,也就是让虚拟机根据收集到的运行时数据自行决定各个分代的大小等与垃圾回收有关的配置。

  • Serial Old 收集器

用于老年代的Serial收集器,单线程,使用“标记-整理”算法。
这里写图片描述
SerialOld_gc_runtime

主要在Client模式下使用。

  • Parallel Old 收集器

Parallel Scavenge的老年代版本,多线程,使用“标记-整理”算法。
这里写图片描述
ParallelOld_gc_runtime

  • CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法。
这里写图片描述
CMS_gc_runtime

运作过程分为4个阶段:

初始标记(CMS initial mark):值标记GC Roots能直接关联到的对象。
并发标记(CMS concurrent mark):进行GC RootsTracing的过程。
重新标记(CMS remark):修正并发标记期间因用户程序继续运行而导致标记发生改变的那一部分对象的标记。
并发清除(CMS concurrent sweep):
其中标记和重新标记两个阶段仍然需要Stop-The-World,整个过程中耗时最长的并发标记和并发清除过程中收集器都可以和用户线程一起工作。

CMS收集器对CPU资源非常敏感。

浮动垃圾(Floating Garbage):由于CMS并发清理阶段用户线程还在运行着,自然会有新的垃圾产生,而这些垃圾是在标记过程之后,CMS只能在下次GC时回收它们,这些垃圾就称为浮动垃圾。

CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。

在垃圾收集阶段用户线程还在运行,因此需要预留足够的内存给用户线程使用。如果预留内存不能满足用户线程,会出现“Concurrent Mode Failure”,这时虚拟机将启动临时后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集。

由于CMS使用的是清除算法,会导致内存碎片问题,因此提供了参数用于控制是否在进行FullGC后进行内存整理,还提供了参数用于控制在多少次FullGC时才进行内存整理。内存整理是不能并发的,也就是要暂停所有用户线程。

  • G1 收集器

G1(Garbage First):是一款面向服务端应用的垃圾收集器,用于替换CMS收集器。
这里写图片描述
G1_gc_runtime

G1将整个Java堆划分为大小相等的独立区域(Region);新生代和老年代不再是物理隔离的,都由一组不连续的Region组成。

G1的特点:

并行与并发:充分利用多CPU缩短Stop-The-World停顿时间,在收集过程中用并发的方式让Java线程继续执行。
分代收集:仍然有分代的概念,不需要其他收集器配合,独立管理整个GC堆。
空间整合:从整体看,是基于“标记-整理”算法实现的,从局部(两个Region之间)看是基于“复制”算法的。在运行期间不会产生内存碎片。
可预测的停顿:G1跟踪各个Region里垃圾堆积值的价值大小,维护一个优先级队列,每次根据允许的时间,优先回收价值最大的Region。(这也是Garbage First的由来)
Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。

G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序对引用类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查引用的对象是否处于不同的Region中(在其他收集器中就是检查是否老年代中的对象引用了新生代中对象),如果是,通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

G1垃圾回收主要有4个阶段:

初始标记:只标记GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。此阶段需要暂停用户线程。
并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象;耗时较长,可与用户线程并发执行。
最终标记:修正在并发标记期间有变动的标记记录。
刷选回收:对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间制定回收计划,进行垃圾回收。

JVM中的G1垃圾回收器

内存分配与回收规则

内存分配与回收规则由垃圾回收器和内存有关参数决定,不是固定的。

两个概念:

MinorGC,次收集:在新生代发生的垃圾收集,速度快,发生频繁。
FullGC,MajorGC,主收集:发生在年老代的垃圾收集。一般伴随一次MinorGC。
一般的规则:

对象优先在Eden分配。当Eden没有足够的空间时,虚拟机将发起一次MinorGC。

大对象直接进入老年代。大对象是指需要连续内存空间的Java对象。目的是避免在Eden区及两个Survivor区之间大量的内存复制(新生代采用复制算法收集内存)。

长期存活对象将进入老年代。虚拟机给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过一次Minor GC后仍然存活,并且能够被Survivor容纳,将被移动到Survivor空间,并且对象年龄设为1。对象在Survivor区中每“熬过”一次MinorGC,年龄就加1,达到某个阀值(parNew中默认是15)就晋升到年老代。

动态对象年龄判定。如果surivior空间中相同年龄所有对象大小总和大于Survivor的一半,大于等于这个年龄的对象直接进入老年代,不用达到MaxTenuringThrehold中的年龄(15)。

空间分配担保。在发生Minor GC之前,虚拟机会先检查年老代最大可用的连续空间算法大于新生代所有对象总空间,如果是,那么Minor GC可以确保是安全的。如果否,虚拟机会查看HandlePromotionFailure设置是否允许担保失败。如果允许继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,这是有风险的(存活对象占用的内存大于平均大小,将导致HandlePromotionFailure失败,重新发起一次Full GC);如果小于或者HandlePromotionFailure设置不允许冒险,将改为Full GC。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐