垃圾回收机制

所谓的垃圾就是指内存空间中不再被使用到的内存空间,例如在new完某个对象之后,没有任何指针指向这个对象,那么这块内存区域就是垃圾。

1、如何判断一块内存空间是否为垃圾

1.1 引用计数法

给对象添加一个引用计数器,每访问一次就加1,引用失效就减1

优点:实现简单,效率高;

缺点:需要单独的内存空间去实现这个算法,若两个对象之间相互引用,该算法无法进行识别

1.2 根搜索算法(可达性分析算法)

从根节点(GC Roots)向下搜索对象节点,搜索走过的路径称为引用链(Reference Chain),当一个对象到根节点之间没有连通的话,则该对象不可用。可见下图示例:

1.2.1 可以作为根节点的对象
  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等;
  2. 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量;
  3. 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用;
  4. 在本地方法栈中JNI(即通常所说的Native方法)引用的对象;
  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器;
  6. 所有被同步锁(synchronized关键字)持有的对象;
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

HotSpot使用了一组叫做OopMap的数据结构达到准确式GC的目的,OopMap中记录了各个对象之间的引用关系,这样在进行GC回收的时候不用从每个根节点往下去寻找引用链,直接扫描OopMap就可以了;

但是JVM并没有对每一条指令都生成一个OopMap,只有在一些“特定位置”才能记录OopMap的信息,这些“特定位置”被称为“安全点(SafePoint)”,当前线程只有执行到安全点后才允许暂停进行GC

1.3 引用关系

1.3.1 强引用

类似于Object a = new A()这样的,不会被回收

1.3.2 软引用

软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。

1.3.3 弱引用

弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2版之后提供了WeakReference类来实现弱引用

1.3.4 虚引用

虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。只要发生垃圾回收时一定会被回收掉一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK1.2版之后提供了 PhantomReference类来实现虚引用

2、垃圾回收相关概念

2.1 跨代引用

就是一个代中的对象引用另一个代中的对象

2.2 跨代引用假说

跨代引用相对于同代引用来说只是极少数

2.3 隐含推论

存在互相引用关系的两个对象,应该是倾向于同时生存或同时消亡的

当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”(Generational Collection)的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

(1)弱分代假说(WeakGenerational Hypothesis):绝大多数对象都是朝生夕灭的

(2)强分代假说(StrongGenerational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡

2.4 记忆集(Remembered Set)

一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,通俗的说,就是用一部分内存记录了存在跨代引用的情况,这部分内存就称为记忆集,这样就不用扫描整个新生代和老年代了,只需要扫描记忆集中的内容即可。在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就可以了,并不需要了解这些跨代指针的全部细节

2.4.1 字长精度

每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的32位或64位,这个精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针

2.4.2 对象精度

每个记录精确到一个对象,该对象里有字段含有跨代指针

2.4.3 卡精度(最常用)

每个记录精确到一块内存区域,该区域内有对象含有跨代指针

第三种“卡精度”所指的是用一种称为“卡表”(Card Table)的方式去实现记忆集,卡表就是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。关于卡表与记忆集的关系,可以按照Java语言中HashMap与Map的关系来类比理解,卡表的每个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块称为“卡页(Card Page)”

2.5 写屏障

在HotSpot虚拟机里是通过写屏障(Write Barrier)技术维护卡表状态的。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP切面,在引用对象赋值时会产生一个环形(Around)通知,供程序执行额外的动作(指写屏障),也就是说赋值的前后都在写屏障的覆盖范畴内。在赋值前的部分的写屏障叫作写前屏障(Pre-Write Barrier),在赋值后的则叫作写后屏障(Post-Write Barrier)。通过写屏障实现当对象状态改变之后,维护卡表状态

2.6 判断是否为垃圾的步骤

(1)根搜索算法不可用

(2)看是否有必要执行finalize算法(如果有必要执行finalize方法,那么该对象就有自救的可能,不一定会被回收)

(3)上述两个步骤完成后对象仍然没有人使用的话就属于垃圾

2.7 几种GC类型

2.7.1 MinorGC/YoungGC

发生在新生代的收集动作

2.7.2 MajorGC/OldGC

发生在老年代的GC,目前只有CMS收集器会有单独收集老年代的行为

2.7.3 MixedGC

收集整个新生代以及部分老年代,目前只有G1收集器会有这种行为

2.7.4 FullGC

收集整个Java堆方法区的GC

2.8 Stop-The-World

STW是Java中一种全局暂停的现象,多半由于GC引起。所谓全局暂停,就是所有的Java代码停止运行,native代码可以执行,但是不能与JVM交互,这种情况要极力避免

3、垃圾回收算法

3.1 标记清除法(Mark-Sweep)

算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象;也可以反过来,标记存活的对象,统一回收所有未被标记的对象。标记过程就是对象是否属于垃圾的判定过程

优点:实现思路简单

缺点:

  1. 标记和清除的效率都不高
  2. 标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

3.2 标记复制算法(Copying)

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。(多在新生代中使用)

优点:实现简单,运行高效,不用考虑内存碎片化的问题

缺点:内存浪费

JVM实际实现中,是将内存分为一块较大的Eden区和两块较小的Survivor区,每次使用Eden区和其中一块Survivor区,回收时,把存活的对象复制到另外一块Survivor区内。HotSpot默认Eden区和Survivor比为8:1,即每次都能使用90%的新生代空间;如果Survivor空间不够,就需要依赖老年代进行分配担保,把放不下的对象直接放入老年代中

分配担保步骤:

(1)在发生MinorGC前,JVM会检查老年代的最大可用连续空间,是佛大于新生代所有对象的总空间,如果大于,可以确保MinorGC是安全的;

(2)如果小于,那么JVM会检查是否设置了允许担保失败,如果允许,则继续检查老年代最大可用的连续空间,是否大于历次晋升到老年代对象的平均大小

(3)如果老年代最大可用的连续空间大于历次晋升到老年代对象的平均大小,则尝试进行一次MinorGC,否则就改做一次FullGC

3.3 标记整理算法(Mark-Compact)

标记整理算法的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存(多用于老年代中)

4、几种垃圾收集器

4.1 新生代、老年代中的收集器及他们的组合关系

4.2 串行收集器(Serial收集器、Serial Old收集器)

GC单线程内存回收,会暂停所有用户的进程,即发生STW现象,如Serial、Serial Old。串行收集器运行示意图如下:

对于串行收集器来说,新生代中采用复制算法,老年代采用标记整理算法,垃圾回收时时单线程进行的,进行GC的时候暂停所有的用户线程

使用-XX:+UseSerialGC来开启,此时使用Serial、Serial Old的收集器组合。

优点:简单,对于单CPU来说,由于没有多线程的交互开销,可能更高效,是默认的Client模式下的新生代收集器

4.3并行收集器(ParNew收集器、Parallel收集器、Parallel Old收集器)

4.3.1 ParNew收集器

ParNew收集器是应用于新生代中的收集器,在老年代中的工作主要是配合CMS收集器,其运行示意图如下:

多个GC线程并发工作,此时用户线程是暂停的,ParNew收集器可以看作是串行收集器的多线程并行版,它和Serial收集器一样可以和CMS收集器配合工作,在并发能力好的CPU环境里,它停顿的时间要比串行收集器短,单对于单CPU或者并发能力较弱的CPU,由于多线程的交互开销,它的性能可能比串行回收期更差。

4.3.2 Parallel Scavenge收集器

Parallel Scavenge收集器(新生代)/Parallel Old收集器和ParNew类似,是使用复制算法、并行的收集器,但是它更关注吞吐量,能最高效率地利用CPU,适合运行后台应用。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:

Parallel Scavenge收集器使用-XX:+UseParallelGC来开启,并提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

-XX:MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

-XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。

Parallel Scavenge收集器(新生代)/Parallel Old收集器运行示意图如下:

4.4 CMS收集器(Concurrent Mark Sweep)

并发收集:用户线程和GC线程同时执行(不一定是并行,可能交替执行),不需要暂停用户线程,如CMS收集器(用于老年代)

并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。

并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。

CMS工作的过程分为四个步骤:

(1)初试标记:只标记GC Roots能够直接关联到的对象,速度很快,过程中仍然会发生STW;

(2)并发标记:是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;

(3)重新标记:由于第二阶段并发标记没有暂停用户线程,因此重新标记阶段会修正并发标记期间因为程序运行而导致发生变化的那一部分对象,该阶段仍然会发生STW;

(4)并发清除:并发回收垃圾对象

CMS收集器运行示意图如下:

最后的重置线程,指的是清空跟收集相关的数据并重置,为下一次的收集做准备

优点:低停顿,并发执行

缺点:

  1. 并发执行对CPU资源压力大
  2. 由于垃圾回收线程与用户程序并发执行,无法处理在用户程序运的过程中产生的垃圾,可能会导致FullGC
  3. 标记清除算法导致的产生大量内存碎片,缺少足够大的连续的内存空间,可能触发FullGC

使用-XX:UseConcMarkSweepGC开启CMS收集器,使用ParNew+CMS+Serial Old的收集器组合,Serial Old将作为CMS出现错误的后备收集器

4.5 G1收集器

G1收集器是一款面向服务端应用的收集器,与其他收集器相比,具有如下特点:

(1)G1把内存划分为多个大小相等独立的区域(Region),Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待

(2)G1仍采用分代思想,保留新生代和老年代,但他们不再是物理隔离的,而是一部分Region的集合,且不需要Region是连续的(一块Region区可能作为新生代也可能作为老年代),如下图所示:

(3)G1能充分利用多CPU、多核环境硬件优势,尽量缩短STW

(4)G1整体上采用标记-整理算法,剧不是通过标记复制算法,不会产生内存碎片

(5)G1的停顿是可以预测的,可以通过设置参数来明确指定在一个时间段内,消耗在垃圾收集上的时间不能超过多少

(6)G1跟踪各个Region里面垃圾堆价值大小,在后台维护一个优先列表,每次根据允许的收集停顿时间来优先回收价值最大的区域,从而保证在有限时间内的高效收集

Java堆分成多个独立Region后,Region里面存在的跨Region引用对象如何解决呢?解决的思路我们已经知道:使用记忆集避免全堆作为GC Roots扫描。但是在G1收集器上记忆集的应用其实要复杂很多,它的每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更复杂,同时由于Region数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。

在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?这里首先要解决的是用户线程改变对象引用关系时,必须保证其不能打破原本的对象图结构,导致标记结果出现错误,CMS收集器采用增量更新算法实现,而G1收集器则是通过原始快照(SATB)算法来实现的。此外,垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设计了两个名为TAMS(Top at Mark Start) 的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。与CMS中的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”。

怎样建立起可靠的停顿预测模型?用户通过-XX:MaxGCPauseMillis参数指定的停顿时间只意味着垃圾收集发生之前的期望值,但G1收集器要怎么做才能满足用户的期望呢?G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。这里强调的“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。换句话说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。

如果我们不去计算用户线程运行过程中的动作(如使用写屏障维护记忆集的操作),G1收集器的运作过程大致可划分为以下四个步骤:

4.5.1 初始标记

仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。

4.5.2 并发标记

从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。

4.5.3 最终标记

对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。

4.5.4 筛选回收

负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

G1收集器运行过程见下图:

(1)使用和配置G1:-XX:+UseG1GC:开启G1收集器,JDK13默认就是G1收集器

(2)-XX:MaxGCPauseMills=n,最大GC停顿时间,这个是软目标,JVM将尽可能(但不保证)停顿小于这个时间n

(3)-XX:InitiatingHeapOccupancyPercent=n,堆内存占用多少的时候就触发GC,默认为45

4.6 ZGC收集器(了解)

JDK11加入的具有实验性质的低延迟收集器,ZGC的设计目标是支持TB级的内存容量,暂停时间低(<10ms),对整个程序吞吐量的影响小于15%

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值