代码编织梦想

JVM垃圾回收机制

垃圾回收机制是JVM的重要特点,简单介绍一下JVM垃圾回收

什么算垃圾

JVM垃圾回收时要标记出“垃圾”并清理,那什么算垃圾呢?

垃圾可以理解为内存中没用了的对象,那哪些是没用的对象呢?

有两个方法评价对象是否是垃圾:

引用计数法

为每个对象设置一个引用计数器,每当有人引用这个对象则这个对象的引用计数器加一,当计数器为0时认为这个对象是垃圾,可被回收。

  • 优点:实现简单
  • 缺点:计数器占空间且无法检测循环引用(你我互相引用,计数器都是1)

可达性分析

定义了一个集合GCRoots根集,根集的元素是对象!对象!对象!

如果对象是GCRoots的对象或者被GCRoots对象直接或间接引用就算可达,像树状图一样从根往下找出所有存活对象。

GCRoots包括:虚拟机栈中引用的对象、方法区中的常量引用的对象、方法区中类静态属性引用的对象、本地方法栈中JNI(Native方法)的引用对象、活跃线程。

  • 优点:能解决循环引用的问题
  • 缺点:在多线程的情况下,工作线程有可能更新已经访问过的对象的引用

其实被标记到了也不一定会被清理,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize`方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,那对象就死了。

几种引用

  • 强引用(Strong Reference):如Object obj = new Object(),这类引用是 Java 程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用(Soft Reference):它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2 之后提供了SoftReference类来实现软引用。
  • 弱引用(Weak Reference):它也是用来描述非必须对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在 JDK1.2 之后,提供了WeakReference类来实现弱引用。
  • 虚引用(Phantom Reference):也称为幻引用,最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。

虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列ReferenceQueue联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

垃圾回收算法

知道什么是垃圾之后,我们来看怎么清理垃圾

标记清理

标记出存活的对象,回收死亡对象的内存。

  • 优点是:清理后不用移动存活的对象

  • 缺点是:会留下很多不连续的内存碎片,需要另外维护一个列表来记录空闲内存的地址和大小

标记整理

标记出存活的对象,整理移动存活对象到内存的一端,清理存活边界外的内存。标记整理一般依赖于句柄和句柄表。

  • 优点:整理后没有内存碎片,空闲内存的地址可知
  • 缺点:GC暂停时间长,需要更新对象地址

复制

把内存分成两片一样大的区域(from,to是相对而言的),每次回收时就标记并复制存活对象到to内存,再把from内存回收。

  • 优点:也可以避免内存碎片
  • 缺点:可用的内存减半了

分代回收

分代回收将堆内存分成了新生代、老年代和永久代(永久代JDK1.8后在元空间)。

新生代被分为几个区域:伊甸园(Eden)和两个存活区(Survivor0/Survivor1),用来存放刚创建的“年轻”对象

老年代用来存放逃过多次GC的“老”对象

永久代在JDK1.8之前指JVM内存中的方法区,JDK1.8之后指直接内存中的元空间

GC的分类

针对 HotSpot VM 的实现,它里面的 GC 其实准确分类有两大种:

部分收集 (Partial GC):

  • 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
  • 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
  • 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。

整堆收集 (Full GC):收集整个 Java 堆和方法区。

分代回收流程

一般流程如下:

  1. 对象创建优先在Eden分配内存。
  2. 当Eden内存不够用时发起minor gc,回收新生代垃圾,Eden和Survivor0中的存活的对象复制到Survivor1中然后交换Survival0和1,每当存活对象躲过一次GC则年龄加1,默认年龄达到15的对象会从存活区晋升到老年代。
  3. 当老年代内存不足时触发full gc,回收新生代和老年代中的垃圾。 如果分配了直接内存,若直接内存中的对象不被引用则也会被回收。

新生代晋升老年代的方式

  1. Eden区满时,进行Minor GC,当Eden和一个Survivor区中依然存活的对象无法放入到Survivor中,则提前转移到老年代中。
  2. 若对象体积太大,就会绕过新生代, 直接在老年代分配, 此参数只对Serial及ParNew两款收集器有效。参数-XX:PretenureSizeThreshold用来设置这个门限值。
  3. 对象头的Mark Word中包含对象的年龄。当年龄增加到一定的临界值时,就会晋升到老年代中。 该临界值由参数:-XX:MaxTenuringThreshold来设置,默认为15,即对象在经历15次minor gc后会晋升到老年代。
  4. 如果在Survivor区中相同年龄的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

空间分配担保

空间分配担保是为了确保在 Minor GC 之前老年代里还能容纳即将晋升的新生代对象。避免Full GC过于频繁。

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(HandlePromotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次FullGC。

在这里插入图片描述
借用一下图,原文链接:空间分配担保

JDK 6 Update 24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。

垃圾回收器

有几种垃圾回收器,简单解释一下

Serial回收器

Serial回收器是单线程的回收器,回收时会STW(Stop The World),暂停其他工作线程,新生代回收器,采用复制算法。

在这里插入图片描述

ParNew回收器

ParNew回收器是Serial的多线程版本,回收时会STW,新生代回收器,采用复制算法。

在这里插入图片描述

Parallel Scavenge回收器

Parallel Scavenge是使用复制算法的多线程回收器和ParNew相似,区别在于Parallel Scavenge是“吞吐量优先”的回收器,吞吐量即CPU用于用户代码的运行时间/CPU运行总时间,适合在注重CPU使用效率的应用上使用。

JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old。

Serial Old回收器

Serial回收器的老年代版本,采用标记整理算法的单线程回收器。

Parallel Old回收器

Parallel Scavenge的老年代版本,采用标记整理的注重吞吐量的多线程回收器。

CMS回收器

CMS(Concurrent Mark Sweep)回收器是一款使用标记清理的注重回收停顿时间的回收器,工作在老年代,它非常符合在注重用户体验的应用上使用。CMS是一款真正意义上的并发回收器,它让垃圾回收线程和用户工作线程并发工作。

CMS工作流程

  1. 初始标记:需要STW暂停所有工作线程,记录下CGRoots能直接关联到的对象,速度很快。

  2. 并发标记:该阶段进行GC ROOT TRACING,在第一个阶段被暂停的线程重新开始运行。由前阶段标记过的对象出发,所有可到达的对象都在本阶段中标记。

    并发标记后有可能有并发预处理,并发预清理的步骤,并发预处理有可能在重新标记之前对新生代进行MinorGC,减少重新标记的STW时间,对这里不展开说,CMS详细可参考:CMS详解

  3. 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿(STW)时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。

  4. 并发清除:开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

  5. 并发重置:CMS清除内部状态,为下次回收做准备。

在这里插入图片描述

CMS的优缺点

优点:并发收集、低停顿

缺点:

  • 对CPU资源敏感(与用户进程抢CPU资源)
  • 无法处理浮动垃圾(并发标记回收时用户进程产生的垃圾有可能无法回收)
  • 会产生内存碎片(标记清理的算法导致的,解决方案是设置偶尔进行整理)

CMS详细可参考:CMS详解

G1回收器

G1(Garbage First)重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成,即 G1 提供了接近实时的收集特性。
在这里插入图片描述

Humongous是巨型对象区域,用于存放占连续内存巨大的对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。

G1有如下特点

  1. 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU 来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
  2. 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
  3. 空间整合:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
  4. 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。

G1的回收流程

有些陌生的词后面会解释(TAMS,SATB,RSet。。。)

  1. 初始标记:需要STW暂停所有用户工作线程,记录下CGRoots能直接关联到的对象,并且修改Region的TAMS指针,速度很快
  2. 并发标记:以GC Roots直接关联的对象对整个堆进行可达性分析,不断从扫描栈取出引用递归扫描整个堆的对象图,将扫描到的对象进行标记,并将字段压入扫描栈,直到扫描栈被清空。这个过程也将扫描一部分SATB write barrier记录的引用和更新一部分Region的Rset。与用户工作线程并发并行运行。
  3. 最终标记:在并发标记之后又会进入一个短暂的暂停期,用于处理并发阶段遗留的少量SATB记录。
  4. 筛选回收:G1在清除时与传统的mark-compact(标记-复制算法)是不同的,他并不是必须依赖global concurrent marking的结果,而是采用CSet作为回收集合,对堆进行清理。CSet会根据统计模型选定收益最高、开销不超过用户指定的期望停顿时间以内的若干个Region。即不一定回收所有Region而是看情况回收一部分。

G1的两种GC

  • Young GC:在分配一般对象(非巨型对象)时,当所有 Eden 区域使用达到最大阀值并且无法申请足够内存时,会触发一次 YoungGC。每次 Young GC 会回收所有 Eden 以及 Survivor 区,并且将存活对象复制到 Old 区以及另一部分的 Survivor 区。
  • Mixed GC:当越来越多的对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,该算法并不是一个 Old GC,除了回收整个新生代,还会回收一部分的老年代,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些 Old 区域进行收集,从而可以对垃圾回收的耗时时间进行控制。G1 没有 Full GC概念,需要 Full GC 时,调用 Serial Old GC 进行全堆扫描。

G1详细可参考:G1详解

两种Remembered Set实现的说明

垃圾收集器在Partial GC时(局部收集)都会面临跨界引用的问题。例如在回收新生代的过程中,新生代和老年代有可能存在跨代跨界引用,即有可能老年代对象引用了新生代对象,若只扫描新生代则有可能漏标这部分对象导致回收了存活对象,为了避免这种情况则需要扫描整个新生代和老年代,但是这样代价又太大了,那怎么办?

CMS脏卡Card Table

CMS使用了脏卡(卡表)机制。

脏卡简单来说就是属于老年代的一张表,把老年代分成多个区域,只要区域内有对象引用了新生代对象则标记这个区域对应的页表,也就是脏了。脏卡由代表老年代区域的页表构成,一个页表代表一个区域,页表储存了区域内对象指向新生代对象的跨界指针,只要页表内有一个或以上的跨界指针则标记为脏。这种形式被称为 points-out,可以理解为“我指向谁”。

在这里插入图片描述

G1对RSet的实现

G1的内存分代和CMS不同,G1把内存分为了多个区域(Region),所有Region都有可能存在跨界引用,所以RSet比普通脏卡复杂。

G1在points-out的卡表的基础上加了一层结构来构成另外一种 points-into(“谁指向我”)的Rset:即每个region会记录下到底哪些别的region有指向自己的指针,每个Region有一个RSet,这个Rset是一种哈希表,key是指向本Region的其他Region的起始地址,value则是可以理解为指向本Region的其他Region对应的卡表

Rset记录的是points-into的关系(谁指向我),而card table 仍在记录了points-out的关系(我指向谁),是一种双向的结构。

在这里插入图片描述

三色标记

CMS和G1都是从GCRoots开始进行可达性分析并标记存活的对象,我们简单介绍一下标记过程——三色标记。

三色指黑色、灰色、白色,分别代表:

  • 黑色:对象被遍历过且其引用的对象也全被遍历过
  • 灰色:对象被遍历过但其引用的对象还没全被遍历
  • 白色:还没被遍历的对象

标记流程

一开始所有对象都是白色,从GCRoots对象开始遍历,遍历到的对象变成灰色,然后继续遍历灰色对象引用的对象,把灰色变成黑色,白色变成灰色,继续遍历灰色对象引用的对象直到遍历完所有可达的对象,此时黑色即为存活,白色即为垃圾。

在这里插入图片描述

三色标记的缺陷

  1. 浮动垃圾:如果在并发标记时,黑色的对象变成了垃圾,则在这一次GC无法清除这个垃圾,需要留到下次GC。
  2. 漏标(存活对象被回收):在某种条件下,有可能存活对象最终不会被标记为黑色导致存活对象被回收,接下来细说漏标问题。

漏标问题及其解决

漏标的条件:

  1. 黑色对象A引用了白色对象B
  2. 灰色对象断开所有直接或间接的对白色对象B的引用

当以上两个条件同时发生就会发生漏标的情况,只要我们破坏其中一个条件就可以解决这个问题,所以有两种解决方案:增量更新(Incremental Update)和原始快照(STAB——Snapshot-at-begging),分别破坏了1、2条件。

增量更新:CMS采用增量更新,当黑色对象引用白色对象时,通过写屏障把黑色对象变成灰色并记录起来,之后重新遍历标记一次就可以避免漏标。(增量更新破坏第一个条件,黑色对象引用白色对象是吧?给你黑变灰=-=)

原始快照:G1采用原始快照,当灰色对象断开对白色对象的引用时,通过写屏障把这个白色对象变成灰色并记录起来,之后重新遍历标记一次可以避免漏标。原始快照即标记开始时活的,标记结束后一定还是活的,但是有可能标记过程中死亡的对象最终被保留下来。(原始快照破坏第二个条件,灰色对象断开引用是吧?断就断,我不管你干嘛,我全活=-=)

那就有人要问了,那如果你用原始快照的情况下,当正在标记时分配了新的对象给黑色对象怎么办?原始快照也没照到,新对象岂不是要被回收?解决方案是:TAMS(Top At Mark Start)

TAMS(Top At Mark Start)

在并发标记时,新对象的分配并不是随意的,G1在Region中划分了一块专门用来分配新对象的内存,这块内存由两个指针prevTAMS和nextTAMS指定。在TAMS的内存里分配的新对象则认定为存活不会被回收。
灰色对象断开引用是吧?断就断,我不管你干嘛,我全活=-=)

那就有人要问了,那如果你用原始快照的情况下,当正在标记时分配了新的对象给黑色对象怎么办?原始快照也没照到,新对象岂不是要被回收?解决方案是:TAMS(Top At Mark Start)

TAMS(Top At Mark Start)

在并发标记时,新对象的分配并不是随意的,G1在Region中划分了一块专门用来分配新对象的内存,这块内存由两个指针prevTAMS和nextTAMS指定。在TAMS的内存里分配的新对象则认定为存活不会被回收。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_47110792/article/details/127864779

jvm垃圾回收机制【简单介绍】-爱代码爱编程

Java虚拟机(JVM)垃圾回收机制 JVM中的垃圾回收计数是采用的一种自适应的技术(可以通过其工作方式将它“啰嗦地”称为:自适的、分代的、停止-复制、标记-清扫式垃圾回收器) 在讲Java虚拟机的自适应回收机制前,有必

jvm垃圾回收机制(GC)-爱代码爱编程

JVM的垃圾回收机制 有两个混淆的概念特别要注意:GC的对象死活判断算法(用于确认这个对象还有没有用)和GC的对象清除算法(进行对象清理的算法) jvm数据分区 垃圾回收机制简称GC,GC主要用于Java堆的管理。Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。 什么是垃圾回收机制? 程序在运行过程中,会产生大

JVM垃圾回收机制-爱代码爱编程

为什么要有垃圾回收机制? 因为会有垃圾产生啊,可垃圾什么时候会产生呢?这就是涉及到运行时数据区的工作流程了: 当线程A在执行如下代码时: public void say(){ A a = new A(); } 此时对应于JVM运行时数据区发生的事情便是:a对象的引用放在了当前线程栈中的say()方法对应的栈帧中的局部变量表中,然后a对象本身

JVM垃圾回收机制详解-爱代码爱编程

JVM垃圾回收机制详解 1. 为什么垃圾回收 如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此。所以,垃圾回收是必须的。 2. 如何判断对象已经死亡 2.1 引用计数法 引用计数法:给每个对象维护一个引用计数器,每当该对象被应用一次,计数器就加1;当引

JVM垃圾回收机制(收集器、收集算法、卡表)-爱代码爱编程

目录 JVM垃圾回收机制 HotSpot垃圾分代回收算法 HotSpot经典垃圾收集器 CMS G1  跨代引用、卡表、写屏障 各种收集器对比         在java开发中,我们不需要过度的关注对象的回收和释放。因为JVM的垃圾回收机制可以帮助我们自动对内存中已经死亡或者长时间没有使用的对象进行清楚和回收来实现内存空间的有效利用,但是完

JVM垃圾回收机制与算法详解-爱代码爱编程

目录 一、概述 二、垃圾回收相关算法 三、垃圾回收相关概念 四、垃圾回收器(7种) 一、概述 想要学习垃圾回收机制(Garbage Collection),首先我们需要了解以下几点: 【什么是垃圾呢??】 垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存

简述jvm垃圾回收机制_燕麦牛奶小米粥的博客-爱代码爱编程

目录 一、四种垃圾回收方法 (1)标记清除 (2)标记整理 (3)复制算法 (4)分代收集 二、垃圾回收机制 jvm内存结构 垃圾回收有两种类型:Minor GC 和 Full GC 1.Minor GC 2.Full GC 一、四种垃圾回收方法 (1)标记清除 标记阶段 清除阶段 缺点: 可能产生

javase入门篇——类和对象(实例理解)_我不是大叔丶的博客-爱代码爱编程

文章目录 一、面向对象简述二、类与对象的基本概念三、类的定义与使用四、this引用五、对象的构造及初始化六、static成员七、 代码块 一、面向对象简述 面向对象是一种现在最为流行的程序设计方法,几乎现在的

自我实现tcmalloc的项目简化版本_捕获一只小肚皮的博客-爱代码爱编程

项目介绍 该项目是基于现代多核多线程的开发环境和谷歌项目原型tcmalloc的自我实现的简化版本,相比于本身就比较优秀的malloc来说,能够略胜一筹,因为其考虑了 性能,多线程环境,锁竞争和内存碎片的问题,主要利用了

浅谈jvm_array_new的博客-爱代码爱编程

内存结构: 程序计数器(寄存器) 作用:记住下一条jvm指令的执行地址 特点: 是线程私有的 不会出现内存溢出 虚拟机栈 虚拟机栈:是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈