Java虚拟机—堆内存分代和GC垃圾收集算法

2019-07-03 00:00:00 算法 虚拟机 收集

前言:

上一篇文章我们说到了「对象」这个在java中无比重要的概念,也讨论了对象在堆内存里的创建、布局和访问定位,本篇文章我们就要讨论下「对象」的「死亡」和垃圾收集。

不同的JVM实现采用了不同的垃圾收集器,不同的垃圾收集器的工作原理也是不同的,本文主要以介绍HotSpot虚拟机的垃圾收集器及其实现。由于现在主流的垃圾收集器都采用分代式垃圾回收算法,所以我们会重点介绍相关算法以及对于的Java堆中分代的概念。

所以本文的主要内容有:

  • 1.对象存活判断
  • 2.Java堆分代的概念
  • 3.GC垃圾收集算法
  • 4.常用垃圾收集器介绍(G1、CMS)
  • 5.内存分配和回收策略

0.概述

垃圾收集(Garbage Collection),也叫GC,学Java的童鞋大部分人都听过这个概念,而且知道这是为了回收无用的Java堆内存上的「对象」而产生的。其实GC的历史比Java久远,1960年诞生的Lisp是第一门使用内存动态分配和垃圾收集的编程语言。

正如上一篇文章中提到的:

Java堆中内存的排列是否规整取决于堆中垃圾收集器,如果JVM中的垃圾收集器带有空间压缩整理功能,则内存规整;否则内存不规整。

在Java堆内存上,垃圾收集器往往和内存分配紧密相关,因为不同的垃圾收集器可能采用不同的内存分配方式、不同的收集算法和不同的内存压缩整理方式。

下面请思考一个小问题——GC垃圾收集为什么只回收Java堆内存和方法区?(HotSpot VM中)那Java虚拟机栈、本地方法栈、程序计数器这些呢?

答案:Java虚拟机栈、本地方法栈、程序计数器这三者是线程私有的,随线程而生随线程而灭。栈中的栈帧随着方法的进入和退出有条不紊的出栈入栈,每个栈帧需要分配多少内存,在类结构确定下来时就是已知的(尽管运行期间会有JIT编译器进行一些优化,但在基于概念模型的讨论中,大体可以认为是编译器可知的)。因此上述这些区域的内存分配和回收都具备确定性,故不需要过多考虑垃圾回收的问题。而Java堆和方法区则不一样:一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支所需的内存也不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的,所以垃圾回收器所关注的重点是位于Java堆和方法区上的内存。

1.对象存活判断

在Java堆中存放着Java世界中几乎所有的对象实例,在垃圾回收器回收内存前,首先要做的事情就是判断这些对象哪些还「活着」哪些已经「死去」。何为活着?活着就表示该对象可以被发现和使用,死去则表示该对象已经无法被任何途径所使用,将要等待着被当作垃圾收集。

对象存活性的判断,有两类比较常用的算法:

  • 引用计数算法
  • 可达性算法

1.1引用计数算法

引用计数算法可以这样实现:给每个创建的对象添加一个引用计数器,每当此对象被某个地方引用时,计数值+1,引用失效时-1,所以当计数值为0时表示对象已经不能被使用。引用计数算法大多数情况下是个比较不错的算法,简单直接,也有一些著名的应用案例但是对于Java虚拟机来说,并不是一个好的选择,因为它很难解决对象直接相互循环引用的问题。

譬如有A和B两个对象,他们都互相引用,除此之外都没有任何对外的引用,那么理论上A和B都可以被作为垃圾回收掉,但实际如果采用引用计数算法,则A、B的引用计数都是1,并不满足被回收的条件,如果A和B之间的引用一直存在,那么就永远无法被回收了

1.2可达性算法

在主流的商用程序语言如Java、C#等的主流实现中,都是通过可达性分析(Reachability Analysis)来判断对象是否存活的。此算法的基本思路就是通过一系列的“GC Roots”的对象作为起始点,从起始点开始向下搜索到对象的路径。搜索所经过的路径称为引用链(Reference Chain),当一个对象到任何GC Roots都没有引用链时,则表明对象“不可达”,即该对象是不可用的。

《Java虚拟机—堆内存分代和GC垃圾收集算法》
《Java虚拟机—堆内存分代和GC垃圾收集算法》

在Java语言中,可作为GC Roots的对象包括下面几种:

  • 栈帧中的局部变量表中的reference引用所引用的对象
  • 方法区中类static静态引用的对象
  • 方法区中final常量引用的对象
  • 本地方法栈中JNI(Native方法)引用的对象

1.3对象的引用

无论是引用计数算法或者是可达性分析算法,判断对象是否“存活”都和「引用」相关。在JDK1.2以后,「引用」分为4种类型:

  • 强引用-Strong Reference
  • 软引用-Soft Reference
  • 弱引用-Weak Reference:
  • 虚引用-Phantom Reference

强引用就是在代码中普遍存在的,类似Object obj = new Object()这类的引用,只要强引用存在,则垃圾回收器永远不会回收掉它。

软引用用来描述一些还有用,但是非必须的对象。这些对象通常不会被回收。在虚拟机内存即将溢出之前,垃圾回收器会回收这部分软引用的内存,如果还是内存不够,则抛出内存溢出异常。存在软引用的对象只在内存即将溢出时被回收。

弱引用也是用来描述非必须的对象,且它的强度比弱引用更浅。它的生命只能存活到下一次垃圾收集之前。当下一次垃圾收集发生时,无论内存是否足够,都会回收弱引用的内存。

虚引用是最弱的一类引用,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过一个弱引用来获取一个对象实例(即:无论是否存在此引用,不会影响一个对象的回收)。例如,为一个对象设置若引用,则该对象被垃圾收集器回收后能收到一个系统通知。

1.4对象生死大逃亡

对象经过可达性算法分析后,判断为不可达,那么对象就「必死无疑」了么?不一定,对象在面临垃圾回收器的处理时,还有最后一次求生的机会。

要kill掉一个对象,至少要经过垃圾回收器的2次标记过程,不可达的对象被第一次标记后会进行一次筛选,筛选的条件是「此对象是否有必要执行finalize()方法」,当对象没有覆盖finalize方法或者已经执行过finalize方法时,会被判断为:没必要执行。如果被判断为有必要执行,则该对象会被放置在一个F-Queue队列,并在稍后虚拟机建立的Finalizer线程中执行finalize()来kill掉对象。在回收前垃圾回收器会对F-Queue队列中的对象进行第二次标记,如果在标记前,对象成功与引用链上的任意对象建立了关联,则会在第二次标记时被移出F-Queue,从而实现「自救」

1.5方法区的垃圾回收

首先,要明确一个概念——方法区,是一个「概念」,是Java虚拟机规范中定义的概念,一个「非堆」的运行时数据区域,用于存放被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,运行时常量池也是存放于方法区中。逻辑上的「非堆」表示和Java堆独立,那物理上呢?

Java虚拟机规范中定义了方法区这个概念,但是并没有规定此区域的是否需要垃圾收集。

在Java7以前,HotSpot虚拟机中,方法区也被称为“永久代”,因为在物理上,方法区使用的是由JVM开辟的堆内存,由于和Java堆共享内存且内存空间由垃圾收集器统一分配和管理,自然的垃圾收集也拓展到了方法区上。此时,Java堆中分区为青年代Young Generation和老年代Old Generation,而方法区自然地被称为永久代Permanent Generation 。

(JVM虚拟机有不同的实现,比较主流的是sun公司的HotSpot虚拟机,其他虚拟机不存在“永久代”这个概念)

在Java8中,HotSpot虚拟机改变了原有方法区的物理实现,将原本由JVM管理内存的方法区的内存移到了虚拟机以外的计算机本地内存,并将其称为元空间(Metaspace)。这样一来,现在的方法区实际存储在于元空间,再也不用和Java堆共享内存了,“永久代”也就永久地被撤销了。由于元空间Metaspace用的是计算机本地内存,所以理论上来说只要计算机内存足够大,元空间就能有多大。

尽管永久代撤销了,方法区这个逻辑上的空间一直是存在的,实际上Metaspace的大小是可以通过参数设定的,如果Metaspace的空间占用达到了设定的最大值,那么就会触发GC来收集死亡对象和类的加载器。常用的G1和CMS垃圾收集器都能很好地回收Metaspace区。所以在java8以后,方法区的垃圾回收在物理上就是对元空间的垃圾回收。

2.Java堆分代的概念

Java堆是垃圾收集器管理的主要内存,由于主流的虚拟机实现中,垃圾收集器大多采用分代式垃圾回收算法(Generational Garbage Collection),所以会将垃圾收集器所管理的堆内存划分为不同的代。

Java7以前Hotspot虚拟机中将Java堆内存分为3个部分:

  • 青年代Young Generation
  • 老年代Old Generation
  • 永久代Permanent Generation

《Java虚拟机—堆内存分代和GC垃圾收集算法》
《Java虚拟机—堆内存分代和GC垃圾收集算法》

在Java8以后,由于方法区的内存不在分配在Java堆上,而是存储于本地内存元空间Metaspace中,所以永久代就不存在了,在几天前(2018年9约25日)Java11正式发布以后,我从官网上找到了关于Java11中垃圾收集器的官方文档,文档中没有提到“永久代”,而只有青年代和老年代。以官网给出的垃圾收集器分代图片为例,堆分代情况如下:

《Java虚拟机—堆内存分代和GC垃圾收集算法》
《Java虚拟机—堆内存分代和GC垃圾收集算法》 Serial垃圾收集器中分代的默认排列
《Java虚拟机—堆内存分代和GC垃圾收集算法》
《Java虚拟机—堆内存分代和GC垃圾收集算法》 Parallel垃圾收集器中分代的默认排列

可见,无论是串行垃圾收集器(Serial Collector)还是并行垃圾收集器(Parallel Collector),目前的分代情况只有两种:

  • 青年代Young Generation
  • 老年代Old Generation

而在G1垃圾收集器(G1 garbage collector)上,对Java堆内存的划分和上述两种垃圾收集器有较大区别,但是还是保留了青年代和老年代的概念。其堆内存布局如下:

《Java虚拟机—堆内存分代和GC垃圾收集算法》
《Java虚拟机—堆内存分代和GC垃圾收集算法》 https://docs.oracle.com/en/java/javase/11/gctuning/garbage-first-garbage-collector.html#GUID-15921907-B297-43A4-8C48-DC88035BC7CF

我们将在下面的小节中介绍这个G1收集器。

3.GC垃圾收集算法

3.1标记-清除算法(Mark-Sweep)

正如其名,此算法主要分为“标记”和“清除”两个阶段,首先标记出需要收集/回收的对象,在标记完成后将标记过的对象统一回收。标记清除算法是最基础的收集算法。

《Java虚拟机—堆内存分代和GC垃圾收集算法》
《Java虚拟机—堆内存分代和GC垃圾收集算法》

标记清除算法有两处劣势:

  • 效率较低,因为标记和清除这两个过程效率都比较低
  • 空间问题,标记清除后会产生大量不联系的内存空间(碎片),导致如果有大内存的对象,那么就无法找到足够大的连续内存空间以供分配。

3.2复制算法

复制算法,将完整内存区域分为大小相等的2块,每次只使用其中的一块,当这块内存满了(用完),则将此块内存上的对象都「复制」到另一块空内存上去,然后将用完的那块内存进行垃圾回收。这样的好处是将对象复制到空内存空间时由于是按顺序分配,只需要移动堆顶指针,实现起来简单高效,无内存碎片。劣势:空间消耗比较大,一半的内存空间得不到利用。

《Java虚拟机—堆内存分代和GC垃圾收集算法》
《Java虚拟机—堆内存分代和GC垃圾收集算法》

IBM公司的研究表明,98%的对象是“朝生夕死”的,即存活时间很短,所以在“复制算法”中没必要按1:1来划分内存空间,而是将整个内存划分为一块Eden区域和两块Survivor区域,每次使用Eden和其中的一块Survivor。HotSpot虚拟机默认Edan和Survivor的比值为8:10:10。这样分配内存空间时,只浪费了1个Survivor的空间也就是10%。

商用虚拟机的垃圾收集器实现多采用复制算法来完成青年代Young Generation的垃圾回收。

3.3标记-压缩算法(Mark-Compact)

在青年代采用复制算法是非常合适的,因为青年代的特点是对象数量多,生存时间短,所以空间利用率比较重要,而复制算法对于老年代Old Generation则不太适合,因为老年代的对象数量虽少,但比较稳定存活率高这样会有较多的复制开销,针对这种情况,出现了标记-压缩算法。

《Java虚拟机—堆内存分代和GC垃圾收集算法》
《Java虚拟机—堆内存分代和GC垃圾收集算法》

标记-压缩算法和标记-清除算法类似,先通过标记找出等待回收的对象,然后在清除之前将存活的对象都整理整齐放到一边,然后再清除掉边界以外的内存。

3.4分代收集算法(Generational Collectjion)
目前的商用虚拟机的垃圾收集器实现大多采用分代收集算法。分代收集算法是前几种算法的集合体。Java堆分为年轻代和老年代,分代收集算法是指对不同的代采取不同的算法实现,在年轻代中选择复制算法,而老年代中由于对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清除或标记-压缩算法。

此算法的具体实现稍后在第5小节——内存分配和回收策略中具体介绍。

4.常用垃圾收集器(Garbage Collector)介绍

  • Serial收集器
  • Parnew收集器
  • Parellel Scavenge收集器
  • Serial Old收集器
  • Parellel Old收集器
  • CMS收集器
  • G1收集器

1.Serial收集器

Serial收集器是最基本、发展历史最悠久的垃圾收集器在JDK1.3以前是青年代垃圾收集的唯一选择。Serial翻译为串行,看名字就知道这个收集器采用单线程“串行”工作,它在进行垃圾回收的工作时,必须暂停JVM中的其他工作线程,直到垃圾收集介绍,所以这段时间就称为——“Stop The World”,虽然名字很酷,但是由于它会暂停用户的所有线程,造成停滞,所以带来了很不好的用户体验。尤其是Stop The World时间过长时,会让人怀疑人生。虽然很古老,但是Serial经过不断地优化还是一个简单而高效的单线程下的垃圾收集器。

2.Parnew收集器

Parnew收集器其实就是Serial收集器的多线程版本。由于是多线程版本,所以在单CPU环境下的效率并不如传统的单线程Serial收集器。可以和CMS收集器配合一起工作,因此是虚拟机中青年代常用的垃圾收集器。

3.Parellel Scavenge收集器

Parellel Scavenge收集器也是一个青年代,采用复制算法的收集器。和CMS收集器尽可能缩短垃圾收集时Stop The World停顿时间不同,Parellel Scavenge收集器的主要关注点在于达到一个可控制的吞吐量(Throughtput)。

吞吐量就是CPU用于运行用户代码的时间/CPU总消耗时间,即吞吐量 = 运行用户代码的时间/(运行用户代码+垃圾收集时间)

低停顿时间的关注点在于以良好的响应速度和低延迟来提升用户体验,适合需要和用户有较多交互的场景;而高吞吐量的关注点在于可以高效率地利用CPU时间以尽快完成运算任务,此场景主要适合较少用户交互多后台计算任务的场景。

4.Serial Old收集器

Serial Old收集器是Serial收集器在老年代上的版本,同样是采用复制算法的单线程收集器。

5.Parellel Old收集器

是Parellel Scavenge的老年代版本,采用多线程和标记-压缩算法。

6.CMS收集器

CMS收集器全名(Concurent Mark Sweep),从名字可以看出这款收集器是一款比较优秀的基于标记-清除算法的并发收集器。之前也提到过,此收集器的目标在于尽量小的Stop The World间隔时间,用于用户交互比较多的场景。它的收集过程分为4步:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

其中初始标记和重新标记两个步骤仍需要Stop The World间隔。初始标记仅仅是标记一下GC Roots能直接关联到的对象,速度很快。并发标记阶段就是进行GC Roots追踪的过程,而重新标记则是为了修正并发标记期间由于用户程序继续执行可能产生变动的那部分对象的标记记录,此阶段会比初始标记长一些,但远小于并发标记的时间。

整个阶段并发标记和并发清除是耗时最长的两个阶段。但是由于CMS收集器是并发执行的,故可以和用户线程一起工作,所以从整体上CMS收集器的工作过程是和用户线程并发执行的。

优点:

  • GC收集间隔时间短,多线程并发。

缺点:

  • 1.并发时对CPU资源占用多,不适合CPU核心数较少的情况。
  • 2.且由于采用标记清除算法,所以会产生内存碎片。
  • 3.无法处理浮动垃圾。

浮动垃圾:CMS并发清除阶段由于用户线程还可以继续执行,所以可能会产生新的垃圾)

以上垃圾收集器的内容摘自《深入理解Java虚拟机-周志明》-第二版,由于Java更新速度很快,最新的Java11已经出来了😳,所以书中部分垃圾收集器可能已经被HotSpot Vm弃用了,但主要在于学习其思想。

jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)

jdk1.9 默认垃圾收集器G1

7.G1收集器(Garbage-First (G1))

G1收集器(Garbage-First (G1))是收集器技术发展最前沿的成果之一,HotSpot团队赋予它的使命是可以替换掉CMS的收集器,与其他GC收集器相比,G1收集器拥有以下特点:

并行与并发:G1能充分利用多CPU下的优势来缩短Stop The World的时间,同时在其他部分收集器需要停止Java线程来执行GC动作时,G1收集器仍然可以通过并发来让Java线程同步执行。

分代收集:与其他收集器一样,分代的概念在G1中任然被保留。可以不需要配合其他的垃圾收集器,就独立管理整个Java堆内存的所有分代区域,且采用不同的方式来获得更好的垃圾收集效果。

空间整合:G1从整体来看,使用的是标记-压缩算法实现的,从局部两个Region来看,采用的是复制算法实现的,对内存空间的利用非常高效,不会像CMS一样产生内存碎片。

可以预测的停顿:除了追求低停顿以外,G1的停顿时间可以被指定在一个时间范围内。
如果不计算维护Remenbered Set的操作,G1收集器的工作阶段大致区分如下:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

其实,Java11官网描述中已经说明:G1取代了Concurrent Mark-Sweep(CMS)收集器。它也是默认的收集器。表明在Java11中G1是默认的垃圾收集器,而CMS收集器从JDK 9开始就不推荐使用了(deprecated)。

现在,让我们看一下oracle官网上对G1的描述:

以下是G1与其他收集器的主要区别:

并行GC只能作为一个整体压缩和回收旧代空间。G1在多个更短的集合中递增地分布这项工作。这样就大大缩短了暂停时间,从而降低了吞吐量。

与CMS类似,G1同时执行部分老一代空间回收。但是,CMS不能对旧代堆进行碎片整理,最终会遇到很长的完整GC堆。

G1的开销可能比其他收集器高,由于其并发性而影响吞吐量。

由于它的工作原理,G1有一些独特的机制来提高垃圾收集效率:

G1可以在任何收集期间回收旧代的一些完全空的大区域。这可以避免许多不必要的垃圾收集,无需太多努力就可以释放大量空间。

G1可以选择尝试同时删除Java堆上的重复字符串。

总是可以从旧代回收空的大型对象。您可以通过选项-XX:-G1EagerReclaimHumongousObjects禁用此功能。默认情况下禁用字符串重复数据删除。您可以使用选项-XX:+ g1enablestringdeduplicate启用它。

G1收集器对Java堆内存的布局和之前的收集器有明显的区别,虽然概念上任然保留了青年代和老年代的概念,但实际上他们不再是物理隔离的,而是共存于一片内存区域,如图:

《Java虚拟机—堆内存分代和GC垃圾收集算法》
《Java虚拟机—堆内存分代和GC垃圾收集算法》

G1将堆划分为一组大小相等的堆区域,每个区域都是连续的虚拟内存范围,如图9-1所示。

1.G1的堆内存布局

区域(Regions)是内存分配和内存回收的单位。浅灰色的表示空的可用内存;红色区域包括红色+S的区域整体构成了青年代,其中红色块是Edan区域,带有S的红色块是Survivor幸存者区域。蓝色区域整体构成了老年代(包含蓝色和”H”),其中横跨多个区域的蓝色”H”(Humongous)区域表示分配了连续内存空间的大对象。其它普通对象的内存则存在单独的区域中或分布式地散落与区域之中。

2.垃圾区域价值判断

G1避免了全堆垃圾扫描,而是在并发标记阶段跟踪各个对象在堆中的活动性。标记阶段完成后,能确定出堆中垃圾堆积区域的价值大小,并优先收集最大价值的区域,所以这也是G1名称的由来——Garbage-First。

判断垃圾区域的价值主要根据2点:1.单次回收所获取的可用空间大小 2.回收消耗的时长

根据这两点,我们可以看出G1并不是简单地优先收集最“大”的对象,尽管收集无引用的大对象可以获取最大的可用内存空间,但是一个大对象可能散布于内存的不同区域,从而使消耗的时间过长,这样垃圾回收的价值就偏低了。

3.可以预测的停顿实现原理

这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的收集时间内获取尽可能高的收集效率。用户可以指定垃圾收集的停顿时间t,G1使用暂停预测模型来满足用户定义的目标暂停时间t,并根据指定的目标暂停时间t选择要收集的区域位置及数量。

4.G1中的垃圾收集周期(Garbage Collection Cycle)

G1垃圾收集器从Java7开始出现到现在也有不少时间了,其算法还是具体实现肯定经过了不少改动和优化,所以网上OR参考书上的部分资料可能会有过时,下面的垃圾收集周期资料来源于官方Java11中垃圾收集器部分对G1的说明,链接:
HotSpot Virtual Machine Garbage Collection Tuning Guide

《Java虚拟机—堆内存分代和GC垃圾收集算法》
《Java虚拟机—堆内存分代和GC垃圾收集算法》 G1垃圾收集概述说明

在多数情况下,G1的生命周期在这两个阶段中来回交替:

  • 仅限青年(青年优先)Young-only
  • 空间回收Space Reclamatjion

Young-only阶段包含了GC垃圾收集过程(即图中的Cleanup),此过程中会逐渐用老年代中的对象填充进入当前可用内存中。在空间回收阶段,G1除了收集年轻代的空间还会对老年代的空间进行整理,过程结束后重新进入下一个Young-only过程。

1.Young-only阶段:

这个阶段的一开始,是一段常规的收集过程(青年代):此过程会在青年代区域中将存活的对象逐步「提升」至老年代(Edan提升至Survivor,Survivor提升至老年代),目的在于提供一个更宽敞的存储空间。当老年代的空间占用率达到初始堆占用阈值时(图中Old gen Occupency exceeds thredhold)会启动并发清理过程(同样是在青年代)

    • 并发启动Concurent start:此过程除了执行常规的青年代收集过程以外,还会启动标记过程。并发标记确定老年代区域中所有当前可达(存活)的对象,以便在下一个空间回收阶段保留这些对象。在标记过程中可能会被青年代常规的收集线程所中断。标记即将完成时会触发两次stop-the-world暂停期,标记Remark和清理Cleanup。
    • 标记Remark:会触发stop-the-world暂停期期间最终完成并发标记过程,并执行全局引用处理和类卸载。在标记和清理之间G1同时计算对象区域的价值信息,这些信息将在清理Cleanup期中用于执行清理。
    • 清理Cleanup:会触发stop-the-world暂停期,期间会决定是否进入Space Reclamatjion空间回收阶段。

2.空间回收Space Reclamatjion阶段:

主要进行老年代的空间回收和部分垃圾清理。因为老年代的对象大多是存活的,所以垃圾相对少一些。对于老年代中的对象区域,G1是采用转移整理的方式进行垃圾收集的。G1会将此对象的所占用的所有区域都转移到堆上连续的区域中,然后清理掉这部分空间,对此部分区域采取了标记-压缩算法。此过程不仅清理了垃圾还整理了内存空间,是非常高效的。

当G1判断空间转移整理过程的价值量较低时,空间回收阶段结束。

此阶段由多个混合的收集过程组成,意味着此过程除了整理老年代的活动对象空间以外还可以执行青年代的收集过程。

在空间回收之后,收集周期将以另一个Young-only阶段重新开始。

最后,再看一下oracle官网最新的Java11中关于HotSpot Vm中可用的垃圾收集器的介绍:

HotSpot虚拟机垃圾收集调整指南

1.串行收集器(Serial Collector)

串行收集器使用单个线程执行所有垃圾收集工作,这使得它相对高效,因为线程之间没有通信开销。

它最适合于单处理器机器,因为它不能利用多处理器硬件,尽管它在多处理器上对于具有小数据集(最多100 MB)的应用程序很有用。串行收集器在某些硬件和操作系统配置上是默认选择的,或者可以通过选项-XX:+UseSerialGC显式启用。

2.并行收集器(Parallel Collector)

并行收集器也称为吞吐量收集器,它是类似于串行收集器的分代收集器。串行收集器和并行收集器的主要区别是并行收集器有多个线程,用于加速垃圾收集。

并行收集器用于在多处理器或多线程硬件上运行的中型到大型数据集的应用程序。您可以使用-XX:+UseParallelGC选项启用它。

并行压缩是允许并行收集器并行执行主要收集的特性。没有并行压缩,主要的集合使用单个线程执行,这会显著限制可伸缩性。如果指定了-XX:+UseParallelGC选项,则默认启用并行压缩。您可以使用-XX:- useparalleldgc选项禁用它。

——————-

并发收集器(Concurrent Collectors),包含G1垃圾收集器和CMS收集器

并发标记清除(CMS)收集器和垃圾优先(G1)垃圾收集器是两个主要并发收集器。大多数并发收集器对应用程序并发执行一些昂贵的工作。

3.G1垃圾收集器(Garbage-First (G1) garbage collector)

这种服务器风格的收集器用于具有大量内存的多处理器机器。它满足了高概率的垃圾收集暂停时间目标,同时实现了高吞吐量。

在某些硬件和操作系统配置上默认选择G1,或者可以显式启用- xx:+UseG1GC。

4.CMS收集器(Concurrent Mark Sweep Collector)

此收集器适用于更短的垃圾收集暂停时间,并且能够与垃圾收集共享处理器资源的应用程序。

使用选项-XX:+UseConcMarkSweepGC启用CMS收集器

从JDK 9开始,CMS收集器就不推荐使用了(deprecated)。

5.Z收集器(The Z Garbage Collector)

Z垃圾收集器(ZGC)是一种可伸缩的低延迟垃圾收集器。ZGC并发执行所有昂贵的工作,而不停止应用程序线程的执行。

ZGC适用于需要低延迟(少于10毫秒的暂停)和/或使用非常大堆(几十兆兆字节)的应用程序。您可以使用-XX:+UseZGC选项启用is。

从JDK 11开始,ZGC提供实验性的功能。

5.内存分配和回收策略

不同的垃圾收集器有不同的算法和实现方式,譬如上面提到的G1垃圾收集器就比较特殊,但是大多数的内存分配和回收策略是相同的:

  • 对象优先在Elan区分配
  • 大对象直接进入老年代
  • 长期存活的对象进入老年代
  • 对象年龄的动态判断
  • 空间分配担保

下面我们以通常比较常规的Serial垃圾收集器中分代的默认排列为例说明分代式垃圾回收的实现过程:

《Java虚拟机—堆内存分代和GC垃圾收集算法》
《Java虚拟机—堆内存分代和GC垃圾收集算法》

Java堆内存被划分为物理上隔开的2个大区域:

  • 青年代Young Generation
  • 老年代Old Generation

其中青年代(新生代)又分为了3个独立的区域:

  • Edan
  • Survivor
  • Survivor

《Java虚拟机—堆内存分代和GC垃圾收集算法》
《Java虚拟机—堆内存分代和GC垃圾收集算法》

Edan:大多数新建的Java对象被分配到这里(少数很大的对象会直接划分在老年代),MinorGC后,此区域内的对象大多数被GC清理干净,释放内存,只有少数躲过了GC,从而提升至Survivor区域。

Survivor:青年代中含有两个Survivor区域,为什么?因为青年代采用的是复制算法,复制算法会将左边已经使用过的对象复制到右边Survivor未使用区域,然后对左边Survivor区域做一次GC垃圾回收。存放在Survivor区域的对象,都是经历过至少1此MinorGC的,如果在此区域的对象能够顺利躲过多次MinorGC,则会提升至老年代。

5.1对象优先在Elan区分配

大多数情况下,新生的对象会直接在Elan区域分配内存,当内存不够时,虚拟机将会发动一次MinorGC。

MinorGC:在年轻代内存区域上的垃圾收集过程,因为大多数在年轻代上的对象“朝生夕灭”,所以MinorGC非常频繁,一般收集的速度也很快。

MajorGC/Full GC:指发生在老年代的GC,此区域一般对象存活率高,GC一次的速度同城比MinorGC慢10倍以上。

5.2大对象直接进入老年代

所谓的大对象是指需要大量连续内存空间的Java对象,长字符串及大容量的数组。安放这些大对象,虚拟机会直接将其放在老年代,因为大对象一般涉及到的引用多,不容易「死」掉。而且大对象占内存,所以直接在老年代为其开辟一块连续的内存就比较合适。如果内存不够分配,虚拟机会触发垃圾收集过程。

5.3长期存活的对象进入老年代

既然虚拟机采用分代收集的策略来管理内存,那么内存回收时就应该相应的判别哪些对象该放在青年代,哪些放在老年代。为此,JVM给每个对象定义了一个「年龄」计数器。如果对象在Edan出生,并且经过一次MinorGC后仍然存在,则「年龄」增加1岁。当年龄增加到一定数目(如:默认为15岁),就会被「提升」至老年代。部分虚拟机提供了参数可以设置此年龄「阈值」

青年代和老年代在Java堆内存上被划分为两块不同的物理区域,其中青年代中又单独划分成了三块——Edan+Survivor+Survivor。在这些不同区域上任何一个内存“满”了以后,都会触发一次垃圾收集过程。Java中绝大部分的新创建的对象都被分配到了青年代中的Edan区,当青年代满了,就会触发一次在青年代上的垃圾收集过程。

5.4动态对象年龄判定

为了更好地适应不同程序的内存状况,虚拟机并不是永远要求对象年龄必须达到「阈值」才能提升至老年代。在有的垃圾收集器实现中,如果Survivor空间中相同年龄的对象占用空间>Survivor总空间的一半,则此年龄的所有对象就可以提前进入老年代,而不是必须达到阈值。

5.5空间分配担保

在MinorGC之前,JVM会首先检查老年代最大可用的连续内存空间是否 > 青年代所有对象总空间,并以其作为MajorGC执行的「担保」。如果大于则MinorGC可以正常执行。否则JVM会查看HandlePromotionFailure设置值是否允许担保失败,如果允许,则继续执行MinorGC,否则则执行MajorGC用来回收足够的内存空间。

    原文作者:Lyon
    原文地址: https://zhuanlan.zhihu.com/p/45558897
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。

相关文章