JVM垃圾收集器 解析与比较

一、背景
1.JVM组成

  • 类加载器:是加载类文件到内存。执行.class文件就需要用类加载器将字节码文件加载到内存中,然后通过JVM后续的模块进行加载执行程序。
  • 执行引擎:也叫解释器,负责解释命令,提交操作系统执行字节码文件。
  • 本地接口:是融合不同的编程语言为Java所用,其初衷是融合、调用C/C++程序,于是在内存中专门开辟了一块区域处理标记为native的代码。具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。
  • 运行时数据区:是整个JVM的重点。程序的编码被加载到运行时数据区运行。整个JVM框架通过类加载器加载文件,后执行器在内存中处理数据,再通过本地接口与异构系统行交互。
    在这里插入图片描述

2.JVM内存区域
a)程序计数器
程序计数器是一小块的内存区域,可以看做当前线程执行字节码的行号指示器,在虚拟机的概念模型里,字节码解释工作就是通过改变这个计数器的值来选取下一个要执行的字节码指令。比如分支控制,循环控制,跳转,异常等操作,线程恢复等功能都是通过这个计数器来完成。由于JVM的多线程是通过线程的轮流切换并分配处理器执行时间来实现的。因此,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能回到正确的执行位置,每条线程都需要自己独有的程序计数器,多条线程计数器之间互不影响,独立存储。称这类内存区域为线程私有的内存区域。

如果线程正在执行一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行native方法,则这个计数器则为空(undefined)。

此内存区域是Java中虚拟机中唯一一个没有规定任何OutOfMemoryError的内存区域。

b)Java虚拟机栈
Jvaa虚拟机栈与程序计数器一致,Java虚拟机栈也是线程私有的,生命周期与线程相同。虚拟机栈描述的是方法的执行内存模型,每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表,操作数栈,方法出口等信息。每一个方法从执行到结束的过程,就对应一个栈帧从入栈到出栈的过程。

局部变量表存放了编译器可知的四类八种基本数据类型,对象引用(Reference),它不等同于对象本身,可能是指向对象起始地址的引用指针。

局部变量表的内存分配在编译期已经完成分配了,其中64位长度的long和double会占用两个局部变量空间,其余的数据类型只占一个。当进入一个方法时,这个方法需要在栈中分配多大的内存空间是完全能够确定的,方法运行期间不改变局部变量表的大小。

如果线程在栈中申请的深度大于虚拟机所允许的深度,将出现StackOverFlowError异常; 如果虚拟机栈可以动态扩展(当前大部分虚拟机支持动态扩展,当然也允许固定长度的虚拟机栈),如果扩展无法申请到足够的内存,就会抛出OutOfMemoryError异常。

c)本地方法栈
本地方法栈与虚拟机栈的作用非常类似,只不过虚拟机栈执行的是Java方法,而本地方法栈执行的是本地native方法,在虚拟机规范中并没有对本地方法栈中方法使用的语言,使用方式与数据结构并没有强行规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机,如Sun的Hotspot虚拟机直接将虚拟机栈和本地方法栈合二为一。

当然与虚拟机栈一样,本地方法栈也会抛出StackOverFlowError异常和OutOfMemoryError异常。

d)Java堆
对于大多数应用来说,Java堆(Java Heap)是JVM所管理的内存中最大的一块区域,且Java堆是被所有线程所共享的一片区域,在虚拟机启动时创建。该区域的唯一目的就是存放实例对象,几乎所有的对象实例都在这里分配空间。这一点在JVM规范上描述的是:所有的对象实例以及数组都要在堆上分配空间。

Java堆是垃圾收集器管理的管理的主要区域,因此很多时候被称为GC堆。从内存分配的角度讲,由于现在的垃圾回收机制都是分代垃圾回收,所以堆中可以再划分为老年代和新生代,再细的划分为Eden区,Survivor区,其中Survivor区又可细分为From Survivor区和To Survivor区。根据JVM的规范规定,Java堆可以处于物理上不连续的内存空间,只要逻辑上是连续的即可。

像磁盘一样,既可以是固定大小的,也可以是可扩展的。不过当前主流的都采用可扩展的策略(采用-Xmx 和 -Xms控制)。如果在堆中没有完成内存分配,且堆也没有可扩展的内存空间,则会抛出OutOfMemoryError异常。

e)方法区
方法区与java堆一样,有各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,及时编译器编译后的代码等数据。

Java虚拟机相对而言对方法区的限制非常宽松,除了和堆一样不需要连续的空间和可以选择固定大小或者可扩展之外,还可以选择不实现垃圾回收。

相对而言,垃圾回收在这个区域算比较少见,但并非数据进入方法区以后就可以实现永久存活,这个区域的回收目标主要是常量池的回收和对类型的卸载,一般来说,这个区域的回收成绩是比较难以让人满意的。尤其是类型的卸载,条件相当苛刻。

根据Java虚拟机规范规定,当方法区无法满足内存分配时,将抛出OutOfMemoryError异常。

f)运行时常量池
运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法和接口的信息外,还有一项信息是常量池。用于存放编译器各种字面量和符号的引用,这部分内容将在类加载后进入到常量池中存储。

一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

运行时常量池相对于Class文件的常量池一个最大的特性就是动态性,Java语言并不要求常量一定在编译期间产生,也就是说并非预置入Class文件中常量池的内容才能进入常量池,在运行期间也可能将新产生的常量放进常量池,这种特性被利用最多的就是String的intern()方法。

既然运行时常量池属于方法区的一部分,自然具备方法区的约束,所以当内存申请不到的时候也会抛出OutOfMemoryError异常。

g)直接内存
直接内存并不属于JVM运行时数据区的一部分,但是这部分内存区域被频繁的调用,也可能发生OutOfMemoryError异常,所以一起讨论。显然本机的直接内存不会受到Java堆分配内存的影响,但是既然是内存,肯定要受到本机总内存的限制。

服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存。使得各个区域的内存总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常。

在这里插入图片描述

二、内存分配与回收策略
1.标记-清除算法

最基础的收集算法是标记-清除算法,分为标记和清除两个阶段。
1)第一步标记出所要回收的对象;
在这里插入图片描述
2)第二步在标记完成后统一回收所有被标记的对象。

在这里插入图片描述
标记-清除算法主要有两个问题:

  • 第一个是效率问题,标记和清除的效率都不高。
  • 第二个是空间分配问题,标记清除后会产生大量的不连续的内存空间,会产生大量的垃圾碎片,空间碎片太多可能会导致以后程序在运行过程中需要给较大对象分配空间时,无法找到足够的内存空间,而不得不提前进行一次垃圾收集动作,空间的利用率不高。

2.复制算法
为了解决效率问题,出现了复制的收集算法,将可用内存分为大小相等的两块,这样每次都是对半个区域进行回收,内存分配时也就不用考虑碎片等问题了,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

但这种算法将原来的内存缩小为一半,代价太高了。

主要分为以下步骤:
1)将可用内存分为大小相等的A、B两块,每次只使用其中的一块;
在这里插入图片描述
2)当A块内存区域用完,便将存活的对象复制到B块内存中;

在这里插入图片描述
3)然后再把已使用的A内存空间一次性清理掉,并把A内存空间作为备用区域。

在这里插入图片描述

3.标记整理算法
复制算法在存活对象比例比较高的情况下要进行较多的复制操作,效率将会变低,更关键的是,如果不想浪费50%的区域,则需要额外的空间进行分配担保,以应对内存中100%对象都存活的极端情况。

为解决以上问题,另一种标记-整理算法应运而生,标记过程与标记-清除算法一致,但后续步骤不是对可回收对象直接进行清理,主要分为以下步骤:

1)标记出所要回收的对象;
在这里插入图片描述
2)所有存活对象都向一端移动;
在这里插入图片描述

3)在标记完成后统一回收所有被标记的边界外的对象。

在这里插入图片描述
4.分代收集算法
IBM专门研究表明,新生的对象98%都是"朝生夕死"的,所以并不需要像复制算法按照1:1划分内存区域。

故当前商用的垃圾收集器都采用的是分代垃圾回收,是根据对象的存活周期将内存分为几块,一般是将java堆分为新生代和老年代,新生代又将内存分为一块较大的区域给Eden和两块较小的区域给Survivor,Hotspot区默认的Eden和Survivor的From区和To区的比例为8:1:1,也就是说新生代的可用内存为90%,只有10%的内存会被划分为保留内存。

分代垃圾回收的内存结构如下图:

在新生代,每次垃圾回收都有大量的对象死去,只有少量存活,这样就适合采用复制算法。只需要付出少量的对象复制成本就可以完成垃圾回收,而老年代因为存活率高,没有其他内存进行分配担保,就必须使用标记-清理或者标记-整理进行回收。这样就可以根据各个代的对象特点选用最适当的回收算法。

主要分为以下步骤:
1)系统在Eden区创建对象,当Eden区满时,会触发YoungGC(年轻代的垃圾回收);
在这里插入图片描述

2)采用复制算法存活对象复制到From区,存活的对象年龄对应+1,随后清空Eden区;
在这里插入图片描述

3)当Eden区再次满时,第二次触发YoungGC;
在这里插入图片描述

4)将Eden区与From区存活的对象复制到To区,存活的对象年龄对应+1,清空Eden区和From区;
在这里插入图片描述

5)当Eden区第三次满时,再次触发YoungGC;
在这里插入图片描述

6)将Eden区与To区存活的对象复制到From区,存活的对象年龄对应+1,清空Eden区和To区;
在这里插入图片描述

7)当经历15次YoungGC还依然存活的对象,其会直接进入到老年代;
在这里插入图片描述

8)或者当新生代的空间不够的时候,触发内存担保机制,创建对象直接进入到老年代;
在这里插入图片描述

9)当老年代满时,触发Full GC,采用标记-整理算法。
在这里插入图片描述
三、垃圾收集器
当前总共有7种垃圾收集器,分别为:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS和G1。

1.Serial收集器
Serial收集器是最基本,发展最悠久的收集器,在JDK1.3.1之前是虚拟机新生代垃圾回收的唯一选择。这个收集器是一个单线程的。它的单线程的意义并不仅仅说明它只会使用一个CPU或者一条收集线程去完成收集工作,最重要的是,它进行垃圾收集时,其他工作线程会暂停,直到收集结束。

虽然Serial收集器只能减少因垃圾回收而产生的线程停顿,无法完全消除,但它依然是虚拟机在Client模式下,新生代默认的垃圾收集器。

它有相对于其他垃圾收集器的优势,比如由于没有线程之间切换的开销,专心做垃圾收集自然能够收获最高的线程利用效率。在用户桌面应用背景下,一般分配给虚拟机的内存不会太大,收集几十兆或者一两百兆的新生代对象,停顿时间完全可以控制在几十毫秒到一百毫秒之间,这个是可以接受的,只要不是频繁发生。因此,Serial收集器在Client模式下,对于新生代来说依然是一个很好的选择。

总结以上:
1)新生代收集器,可以和Serial Old、CMS组合使用
2)采用复制算法
3)使用单线程进行垃圾回收,回收时会导致Stop The World,用户进程停止
4)Client模式新生代收集器
在这里插入图片描述

2.ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾回收之外,其余可控参数,收集算法,停止工作线程,对象分配原则,回收策略等与Serial收集器完全一致。

除了Serial收集器外,只有ParNew能与CMS配合使用,故其是Server模式下的新生代的首选的虚拟机收集器。在JDK1.5时期,HotSpot推出了一款在强交互应用划时代的收集器CMS,这款收集器是HotSpot第一款真正意义上的并发收集器,第一次实现了垃圾回收与工作线程同时工作的可能性。

不过CMS作为老年代的收集器,却无法与1.4中发布的最新的新生代垃圾收集器配合使用,反之只能使用Serial或者Parnew中的一个。ParNew收集器可以使用-XX:+UseParNewGC强行指定它,或者使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器。

ParNew收集器在单CPU环境下绝对不会有比Serial收集器更好的效果,甚至优于存在线程交互开销,该收集器在通过超线程技术实现的两个CPU的环境下都不能保证百分之百超越Serial收集器。当然,随着CPU数量的增加,对于GC时系统的有效资源利用还是很有好处的。在CPU非常多的情况下,可以使用-XX:ParallelGCThreads来限制垃圾回收线程的数量。

总结以上:
1)新生代收集器,可以和Serial Old、CMS组合使用
2)采用复制算法
3)使用多线程进行垃圾回收,回收时会导致Stop The World,其它策略和Serial一样
4)许多虚拟机Server模式的新生代收集器
在这里插入图片描述

3.Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器,采用复制算法,又是并行的多线程垃圾收集器。它的关注点与其它收集器的关注点不一样,CMS等收集器的关注点在于缩短垃圾回收时用户线程停止的时间,而Parallel Scavenge收集器则是达到一个可控制的吞吐量,所谓吞吐量就是CPU运行用户线程的时间与CPU运行总时间的比值,即 吞吐量 = (用户线程工作时间)/(用户线程工作时间 + 垃圾回收时间),比如虚拟机总共运行100分钟,垃圾收集消耗1分钟,则吞吐量为99%。

停顿时间越短越适合与用户交互的程序,良好的响应速度能提高用户体验,但是高吞吐量则可以高效率的利用CPU的时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的程序。

有两个参数控制吞吐量,分别为最大垃圾收集时间: -XX:MaxGCPauseMills, 直接设置吞吐量的大小: -XX:GCTimeRatio -XX:+UseAdaptiveSizePolicy自适应策略也是Parallel Scavenge收集器区别去Parnew收集器的重要一点

总结以上:
1)新生代收集器,可以和Serial Old、Parallel组合使用,不能和CMS组合使用
2)采用复制算法
3)使用多线程进行垃圾回收,回收时会导致Stop The World
4)关注吞吐量
在这里插入图片描述

4.Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法,这个收集器的主要目的也是在与给Client模式下使用。

在Server模式下,还有两种用途:

  • 一是在jdk5以前的版本中配合Parallel Scavenge收集器使用;
  • 二是作为CMS的备用方案,在并发收集发生Concurrent Mode Failure时使用。

总结以上:
1)年老代收集器,可以和所有的年轻代收集器组合使用,Serial收集器的年老代版本
2)标记-整理算法,会对垃圾回收导致的内存碎片进行整理
3)使用单线程进行垃圾回收,回收时会导致Stop The World,用户进程停止

5.Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法,这个收集器在jdk6中才开始使用的,在此之前Parallel Scavenge收集器一直处于比较尴尬的阶段,原因是,如果新生代采用了Parallel Scavenge收集器,那么老年代除了Serial Old之外,别无选择,由于老年代Serial在服务端的拖累,使得使用了Parallel Scavenge收集器也未必能达到吞吐量最大化的效果,由于单线程的老年代无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至不如Parallel Scavenge收集器 + CMS。

当Parallel Old收集器出现后,"吞吐量优先收集器"终于有了名副其实的组合,在注重吞吐量优先和CPU资源敏感的场合,可以采用Parallel Scavenge收集器 + Parallel Old收集器。

总结以上:
1)年老代收集器,只能和Parallel Scavenge组合使用,Parallel Scavenge收集器的年老代版本,Stop The World
2)多线程,采用标记-整理算法,会对垃圾回收导致的内存碎片进行整理
3)关注吞吐量的系统可以将Parallel Scavenge+Parallel Old组合使用
在这里插入图片描述

6.CMS(Concurrent Mark Sweep)收集器
CMS收集器是一种以获取最短停顿时间为目标的收集器。采用的标记-清除算法,只有初始标记和重新标记需要暂停用户线程。

过程分为4个步骤:
1)初始标记:仅仅关联GC Roots能直接关联到的对象,速度很快;
2)并发标记:进行GC Roots Tracing的过程;
3)重新标记:为了修正并发标记期间,因用户程序运作而导致标记产生变动的那一部分对象的标记记录;
4)并发清除;

由于整个过程中耗时最长的并发标记和并发清除过程收集器都能与用户线程一起工作,所以总的来说,CMS的内存回收过程与用户线程一起并发执行的。

CMS收集器的三大缺点:
1)CMS收集器对CPU资源非常敏感,是并发且维护用户进程的代价;
2)无法处理浮动垃圾,清除时产生新垃圾
3)因为基于标记清除算法,所以会有大量的垃圾碎片产生

总结以上:
1)年老代收集器,可以和Serial、ParNew组合使用
2)采用标记-清除算法,可以通过设置参数在垃圾回收时进行内存碎片的整理
3)CMS是并发算法,表示垃圾回收和用户进行同时进行,但是不是所有阶段都同时进行,在初始标记、重新标记阶段还是需要Stop the World。
4)CMS垃圾回收分这四个阶段,三次标记一次回收
5)适合于对响应时间要求高的系统,以最短回收停顿时间为目标
在这里插入图片描述

7.G1收集器
当下最流行的是G1(Garbage First)收集器,G1之前的收集器对内存的收集都是新生代或者老年代,而G1不在这样。使用G1收集器时,Java堆的内存布局就和其他收集器有很大区别,他将整个堆划分成多个大小想等的区域(Region).虽然还有概念上的新生代和年老代,但他们之间已经不在物理隔离了,他们都是一部分Region(不需要连续的)的集合。
G1能实现可预测的停顿是因为它可以避免对堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾的价值(回收的内存大小和时间的比值)大小,在后台维护一个优先列表,每次优先回收价值最大的Region,这也是可预测停顿的实现的原理。

特点包括:
1)新生代和老年代不再物理隔离,都属于一部分Region的集合,将堆分为大小相等的Region。
2)G1跟踪各个Region垃圾的价值大小以及回收需要时间维护一个Region优先列表,每次先回收价值最大的Region,这是G1-Garbage First名字的由来
3)G1运作分四个阶段,三次标记一次回收(初始标记,并发标记,最终标记,筛选回收)

总结以上:
1)并行并发,使用多个CPU缩短STW的时间
2)分代收集,不需要其他收集器配合也能独立管理堆
3)空间整合,整体基于标记整理算法,局部两个Region基于复制
4)可预测停顿,可以指定时间段M内GC过程时间不超过N
5)较低停顿,停顿时间更加可控可预测

在这里插入图片描述

四、总结
1.各收集器对比,入下图:

名称收集算法工作区域可配合对象线程并发适用场合
Serial复制算法新生代CMS;Serial Old单CPU;Client模式下
ParNew复制算法新生代CMS;Serial Old单CPU;Server模式下
Parallel Scavenge复制算法新生代Serial Old;Parallel Scavenge吞吐量控制,Client,server均可以
Serial Old标记整理算法老年代Serial,ParNew,Parallel Scavenge主要Client模式下
Parallel Old复制算法(Parallel Scavenge老年代版本)老年代Parallel Scavenge吞吐量控制,Client,server均可以
CMS(Concurretn Mark Sweep)标记清除算法老年代Serial,ParNew,Serial Old互联网站;B/S系统服务端
G1整体基于标记整理算法新生代&老年代CMS;Serial Old面向服务端应用

2.常见设置参数:
在这里插入图片描述
JVM中7种垃圾回收器,主要包括串行回收器(Serial、Serial Old)、并行回收器(ParNew、Parallel Scavenge、Parallel Old)、CMS回收器、G1回收器。

其中,负责年轻代中的垃圾回收器(Serial、ParNew、Parallel Scavenge),负责老年代中的垃圾回收器(Serial Old、CMS、Parallel Old),G1可以分代回收,能够独立管理整个GC堆。

在实际使用中,结合以上特点,基于垃圾回收器的性能测试,并根据实际应用的业务,选择垃圾回收器的搭配,并在JVM中参数配置相应的回收器。

参考文献:

  1. 原文链接:https://www.cnblogs.com/yesiamhere/p/6757831.html
  2. 原文链接:https://zhuanlan.zhihu.com/p/58896728
  3. 原文链接:https://www.cnblogs.com/yesiamhere/p/6757831.html
  4. 原文链接:https://blog.csdn.net/chroje/article/details/79573010
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值