JVM的垃圾回收

一、简述

GC(Garbage Collection):Java/.NET 中的垃圾收集器。Java 是由 C++ 发展来的。它摈弃了 C++ 中一些繁琐容易出错的东西,其中有一条就是这个 GC。

在老式的 C/C++ 程序中,程序员定义了一个变量,就是在内存中开辟了一段相应的空间来存值。由于内存是有限的,所以当程序不再需要使用该变量的时候,就需要销毁该对象并释放其所占用的内存资源,好重新利用这段空间。在 C/C++ 中,释放无用变量内存空间的事情需要由程序员来处理,就是说当程序员认为变量没用了,就手动释放其占用的内存。但是这样非常繁琐,如果有所遗漏,就可能造成资源浪费甚至内存泄露。当软件系统比较复杂,变量多的时候程序员往往就忘记释放内存或者在不该释放的时候释放内存了。

有了 GC,程序员就不再需要手动的去控制内存的释放。当 JVM 或 .NET CLR 发觉内存资源紧张的时候,就会自动地去清理无用对象(没有被引用到的对象)所占用的内存空间(这里的说法略显粗略,事实上何时清理内存是个复杂的策略)。如果需要,可以在程序中显式地使用System.gc()来强制进行一次立即的内存清理。Java 提供的 GC 功能可以自动监测对象是否超过了作用域,从而达到自动回收内存的目的,Java 的 GC 会自动进行管理,调用方法:System.gc()或者Runtime.getRuntime().gc()

二、堆内存划分及GC类型

1️⃣堆内存根据对象存活的生命周期被划分为两块,一块是新生代(Young Generation),另一块是老年代(Tenured Generation),大概比例是:新生代:老年代 == 1:2。新生代又分为 Eden 和 Survivor,二者空间大小比例默认为Eden:Survivor == 8:2。幸存区又分为S0和S1,二者空间大小是一模一样的。在堆区之外还有一个元空间(MetaqSpace)。JDK8 HotSpot 中移除了永久代(Permanet Generation),使用 MetaSpace 来代替,MetaSpace 是使用本地内存来存储类元数据信息。内存容量取决于操作系统虚拟内存大小,通过参数 -MaxMetaspaceSize 来限制 MetaSpace 的大小。

2️⃣“对象进入到老年代”的四种情况

  1. Minor GC/Young GC 时,To Survivor 区不足以存放存活的对象,对象会直接进入到老年代。
  2. 经过多次 Minor GC/Young GC 后,如果存活对象的年龄达到了设定阈值,则会晋升到老年代中。
  3. 动态年龄判定规则,To Survivor 区中相同年龄的对象,如果其大小之和占到了 To Survivor 区一半以上的空间,那么大于此年龄的对象会直接进入老年代,而不需要达到默认的分代年龄。
  4. 大对象:由-XX:PretenureSizeThreshold启动参数控制,若对象大小大于此值,就会绕过新生代,直接在老年代中分配。HotSpot 虚拟机提供该参数,指定大于其值的对象直接在老年代分配的意义在于,避免大对象在 Eden 区及两个 Survivor 区之间来回复制,产生大量的内存复制操作。

3️⃣GC类型

  1. 部分收集(Partial GC):指收集的目标不是整个 Java 堆的垃圾收集。其中又分为:

①新生代收集(Minor GC/Young GC):【一般采用复制-回收算法】
指目标只是新生代的垃圾收集,主要是由于 Eden 不够分配了。这个区,大多数 Java 对象都是朝生夕灭,所以 Minor GC 是非常频繁的,回收速度也比较快。

②老年代收集(Major GC/Old GC):【标记-清除算法】
指目标只是老年代的垃圾收集。目前只有 CMS 收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。

③混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行为。

  1. 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。

因为 Full GC 要对整个堆进行回收,包括 Young、Tenured 和 MetaqSpace。所以比 Partial GC 要慢,因此应该尽可能减少 Full GC 的次数。在对 JVM 调优的过程中,很大一部分工作就是对于 Full GC 的调节。

三、堆内存垃圾回收过程

在新生代中,由于对象生存期短,每次垃圾回收时都有大量的对象需要被回收,这时采用复制算法。老年代里的对象存活率较高,每次垃圾回收时只有少量对象需要被回收,没有额外的空间进行分配担保,这时采用标记-整理或者标记-清除算法。可以根据不同代的特点采取最适合的收集算法。

分代收集算法

1️⃣Minor GC/Young GC 的回收过程

  1. 大多数情况下,对象直接在年轻代中的 Eden 区进行分配,如果 Eden 区域没有足够的空间,那么就会触发 Minor GC,处理的区域只有新生代。因为大部分对象在短时间内都可以回收掉,因此 Minor GC/Young GC 后只有极少数的对象能存活下来,而被移动到S0区(采用的是复制算法),并且对象的年龄设为 1。

  2. 当触发下一次 Minor GC/Young GC 时,会将 Eden 区和S0区的存活对象移动到S1区,同时清空 Eden 区和S0区。当再次触发 Minor GC/Young GC 时,这时候处理的区域就变成了 Eden 区和S1区(即S0和S1进行角色交换)。如此反复,对象在 Survivor 区中每“熬过”一次 Minor GC/Young GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就将会被晋升到老年代中。对象晋升老年代的阈值,可以通过参数-XX:MaxTenuringThreshold 设置。

2️⃣Full GC 的触发时机

当晋升到老年代的对象大于了老年代的剩余空间时,就会触发 Full GC。除此之外,还有以下四种情况也会触发:

  1. 老年代的内存使用率达到了一定阈值(可通过参数调整),直接触发 Full GC。

  2. 空间分配担保
    在发生 Minor GC/Young GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。

①如果大于,那这一次 Minor GC/Young GC 可以确保是安全的。
②如果小于,则虚拟机会先查看- XX:HandlePromotionFailure的设置值是否允许担保失败(Handle Promotion Failure):

  • 如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次 Minor GC/Young GC,尽管这次 Minor GC/Young GC 是有风险的;
  • 如果小于,或者-XX: HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次 Full GC。
  1. Metaspace(元空间)达到-XX:MetaspaceSize【元空间初始值,以字节为单位】的指定值时,也会触发 Full GC。同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放 了很少的空间,那么在不超过-XX:MaxMetaspaceSize【元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。】(如果设置了的话)的情况下,适当提高该值。

  2. System.gc()或者Runtime.gc()被显式调用时,触发 Full GC。

四、GC只回收堆内存和方法区内的对象

JVM 内存区域中程序计数器、虚拟机栈、本地方法栈三个区域随线程而生、随线程而灭。因此这三个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。而 Java 堆区和方法区则不一样,这部分内存的分配和回收是动态的,正是垃圾收集器所需关注的部分。

垃圾收集器在对堆区和方法区进行回收前,首先要确定这些区域的对象哪些可以被回收,哪些暂时还不能回收,这就要用到判断对象是否存活的算法:
①引用计数算法
②可达性分析算法
③对象死亡(被回收)前的最后一次挣扎
④方法区如何判断是否需要回收

1️⃣引用计数算法

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为 1。当任何其它变量被赋值为这个对象的引用时,计数 +1(a = b,则 b 引用的对象实例的计数器 +1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器 -1。任何引用计数器为 0 的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器 -1。

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,它们的引用计数永远不可能为 0。

public class ReferenceFindTest {
  public static void main(String[] args) {
     MyObject object1 = new MyObject();
     MyObject object2 = new MyObject();

     object1.object = object2;
     object2.object = object1;

     object1 = null;
     object2 = null;
  }
}

这段代码是用来验证引用计数算法不能检测出循环引用。最后两句将 object1 和 object2 赋值为 null,也就是说 object1 和 object2 指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为 0,那么垃圾收集器就永远不会回收它们。

2️⃣可达性分析算法

该算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点 GC ROOT 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
引用节点图论

在 Java 中,可作为 GC Roots 的对象包括下面几种:
①虚拟机栈中引用的对象(栈帧中的本地变量表);
②方法区中类静态属性引用的对象;
③方法区中常量引用的对象;
④本地方法栈中 JNI(Native方法) 引用的对象。

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在 Java 中,将引用又分为强引用、软引用、弱引用、虚引用四种,这四种引用强度依次逐渐减弱。无论引用计数算法还是可达性分析算法都是基于强引用而言的。

3️⃣对象死亡(被回收)前的最后一次挣扎

即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。

第①次标记:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记;
第②次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行 finalize()。在 finalize() 中没有重新与引用链建立关联关系的,将被进行第二次标记。

第二次标记成功的对象将真的会被回收,如果对象在 finalize() 中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。

4️⃣方法区如何判断是否需要回收

方法区存储内容是否需要回收的判断不同于上。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断,但是对于无用的类则需要同时满足下面三个条件:
①该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例;
②加载该类的 ClassLoader 已经被回收;
③该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

五、常用的垃圾收集算法

1️⃣标记-清除算法

标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如图。该算法不涉及对象的移动,只处理不存活的对象,在存活对象比较多的情况下相对高效。但是会有两个主要问题:①效率不高,标记和清除的效率都很低;②会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次 GC 动作。
标记-清除算法

2️⃣复制算法【参见第三项】

复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成一个对象面和多个空闲面,程序从对象面为对象分配空间,当对象满了,基于复制算法的垃圾收集就从根集合(GC Roots)中扫描还活动的对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。然后一次性清除完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一半的内存。于是将该算法进行了改进,内存区域不再是按照 1:1 去划分,而是将内存划分为 8:1:1 三部分,较大那份内存叫 Eden 区,其余是两块较小的内存区叫 Survior 区。每次都会优先使用 Eden 区,若 Eden 区满,就将对象复制到第二块内存区上,然后清除 Eden 区,如果此时存活的对象太多,以至于 Survivor 不够时,会将这些对象通过分配担保机制复制到老年代中。
复制算法

3️⃣标记-整理算法

该算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。该算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。当对象存活率较高时,也解决了复制算法的效率问题。具体流程如图:
标记-整理算法

六、垃圾收集器

如图是 HotSpot 虚拟机包含的所有收集器:

新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
新生代和老年代的收集器之间进行连线,说明它们之间可以搭配使用。

七、新生代垃圾收集器

1️⃣Serial 垃圾收集器---串行收集器【复制算法】
Serial 收集器是最基本的、发展历史最悠久的收集器。

特点:
串行收集器是指使用单线程进行垃圾回收的收集器。每次回收时,串行收集器只有一个工作线程,暂停其他线程。对于并行能力较弱的单 CPU 计算机来说,串行收集器的专注性和独占性往往有更好的性能表现。它存在 Stop The World 问题,即垃圾回收时,要停止程序的运行。使用-XX:+UseSerialGC参数可以设置新生代使用这个串行收集器。

2️⃣默认ParNew 垃圾收集器---并行收集器【复制算法】
ParNew 其实就是 Serial 的多线程版本,除了使用多线程之外,其余参数和 Serial 一模一样。

特点:
ParNew 默认开启的线程数与 CPU 数量相同,在 CPU 核数很多的机器上,可以通过参数-XX:ParallelGCThreads来设置线程数。它是目前新生代首选的垃圾收集器,因为除了 ParNew 之外,它是唯一一个能与老年代 CMS 配合工作的。它同样存在 Stop The World 问题。使用-XX:+UseParNewGC参数可以设置新生代使用这个并行收集器。

3️⃣Parallel Scavenge 收集器
Parallel Scavenge 使用复制算法回收垃圾,也是多线程的。

特点:
就是非常关注系统的吞吐量,吞吐量=代码运行时间/(代码运行时间+垃圾收集时间)

-XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间,可用把虚拟机在GC停顿的时间控制在 MaxGCPauseMillis 范围内,如果希望减少 GC 停顿时间可以将 MaxGCPauseMillis 设置的很小,但是会导致 GC 频繁,从而增加了 GC 的总时间,降低了吞吐量。所以需要根据实际情况设置该值。

-Xx:GCTimeRatio:设置吞吐量大小,它是一个0到100之间的整数,默认情况下他的取值是99,那么系统将花费不超过1/(1+n)的时间用于垃圾回收,也就是1/(1+99)=1%的时间。

另外还可以指定-XX:+UseAdaptiveSizePolicy打开自适应模式,在这种模式下,新生代的大小、eden、from/to的比例,以及晋升老年代的对象年龄参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。

使用-XX:+UseParallelGC参数可以设置新生代使用这个并行收集器。

八、老年代垃圾收集器

1️⃣SerialOld 垃圾收集器---串行收集器【标记 - 整理算法】
SerialOld 是 Serial 收集器的老年代收集器版本,它同样是一个单线程收集器。

用途:
一个是在JDK1.5及之前的版本中与 Parallel Scavenge 收集器搭配使用。另一个就是作为 CMS 收集器的后备预案,如果 CMS 出现 Concurrent Mode Failure,则 SerialOld 将作为后备收集器。

2️⃣Parallel Old 垃圾收集器---并行收集器【标记压缩算法】
老年代 Parallel Old 垃圾收集器也是一种多线程的收集器,和新生代的 Parallel Scavenge 收集器一样,也是一种关注吞吐量的收集器。使用-XX:+UseParallelOldGc设置老年代使用该收集器。使用-XX:+ParallelGCThreads设置垃圾收集时的线程数量。

3️⃣CMS 垃圾收集器【标记清除法】
CMS 全称 Concurrent Mark Sweep 意为并发标记清除。主要关注系统停顿时间。使用-XX:+UseConcMarkSweepGC设置老年代使用该收集器。使用-XX:ConcGCThreads设置垃圾收集时的线程数量。

特点:
CMS 并不是独占的收集器,也就说 CMS 回收的过程中,应用程序仍然在不停的工作,又会有新的垃圾不断的产生,所以在使用 CMS 的过程中应该确保应用程序的内存足够可用。

CMS 不会等到应用程序饱和的时候才去回收垃圾,而是在某一阀值的时候开始回收,回收阀值可用指定的参数进行配置:-XX:CMSInitiatingoccupancyFraction来指定,默认为 68,也就是说当老年代的空间使用率达到 68% 的时候,会执行 CMS。

如果内存使用率增长的很快,在 CMS 执行的过程中,已经出现了内存不足的情况,此时 CMS 回收就会失败,虚拟机将启动老年代串行收集器 SerialOld GC 进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作。这个过程 GC 的停顿时间可能较长,所以-XX:CMSInitiatingoccupancyFraction的设置要根据实际的情况。

标记清除法有个缺点就是存在内存碎片的问题,那么 CMS 有个参数设置-XX:+UseCMSCompactAtFullCollecion可以使 CMS 回收完成之后进行一次碎片整理。使用-XX:CMSFullGCsBeforeCompaction设置进行多少次 CMS 回收之后,对内存进行一次压缩。

在 CMS GC 过程中,由于老年代剩余空间无法存放需要分配的对象,会产生concurrent-mode-failure。

九、堆内存常见参数配置

十、TLAB 内存

TLAB 全称是 Thread Local Allocation Buffer 即线程本地分配缓存,从名字上看是一个线程专用的内存分配区域,是为了加速对象分配而生的。每一个线程都会产生一个 TLAB,该线程独享的工作区域,JVM 使用这种 TLAB 区来避免多线程冲突问题,提高了对象分配的效率。TLAB 空间一般不会太大,当大对象无法在 TLAB 分配时,则会直接分配到堆上。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容