要知道JVM的GC是如何进行的,首先来了解一下JVM内存结构
一 JVM内存结构
1.1 运行时数据区
首先我们看看Java虚拟机规范是怎么说的:
Java虚拟机规范规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区,如下图所示:
程序计数器:
JVM支持多线程同时执行,每一个线程都有自己的PC Register,线程正在执行的方法叫做当前方法,如果是java代码,PC Register里面存放的就是当前正在执行的指令的地址,如果是C代码,则为空。
Java虚拟机栈:
Java虚拟机栈(Java Virtual Machine Stacks)是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
堆:
Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
方法区:
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。
常量池:
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
本地方法栈:
本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
另外的一块内存区域:
直接内存:
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁使用,而且也有可能导致OutOfMemoryError异常出现。
在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道与缓冲区的I/O方式,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样在某些场景中会显著提高性能,因为避免了在java堆和native堆中来回复制数据。
显然,本机直接内存的分配不会收到java堆大小的限制。
1.2 JVM内存结构
JVM的内存结构与之前所说的运行时数据区,区别在于一个是具体实现,一个是Java虚拟机的规范,接下来我们就实际来看看JVM是如何实现的。
在实际的JVM中内存结构,以HotSpot JVM为例,这也是官方自带的JVM,其内存结构如下图所示:
Metaspace:
Class、Package、Method、Field、字节码、常量池、符号引用等等
CCS:
CCS压缩类空间,启用短指针才会存在。每个对象都有一个指向自己class的指针,考虑性能,可以使用32位指针,那么使用的class就会放到ccs空间。
查看ccs区的变化,我们可以设置不使用CCS,使用下面这个参数
- -XX:+UseCompressedClassPointers
然后使用jstat -gc threadID 指令输出内存使用统计信息
查看输出的CCSC CCSU项
可以知道,如果禁用CCS,那么CCSC 和 CCSU都是0
CodeCache:
JIT编译后的本地代码、JNI使用的C代码
查看CodeCache变化
使用以下参数:
- -Xcomp:完全以编译方式执行,第一次运行代码就编译成本地代码,所以第一次运行很慢。
- -Xint:完全解释执行,运行的时候可能会慢一点
- 默认:JVM自己决定是否编译代码
然后使用jstat -gc threadID 指令输出内存使用统计信息
查看MC MU项变化
设置非堆区大小:
- -XX:MetaspaceSize-XX:MaxMetaspaceSize:metaspace区大小
- -XX:+UseCompressedClassPointers:设置启用CCS区,默认1G
- -XX:CompressedClassSpaceSize:设置CCS区大小
- -XX:InitialCodeCacheSize:codecache初始大小
- -XX:ReservedCodeCacheSize:codecache最大大小
一般而言不需要设置codecache和CCS,只要设置metaspacesize大小就好,因为其他的大小会随metaspacesize大小而改变。
关于堆区的内存结构比较复杂,建议去查看这篇博客,其中不但讲了堆区的信息,也讲了设置堆区的一些参数,我个人觉得写的非常不错:
http://www.cnblogs.com/duanxz/p/6076662.html
或者:
https://blog.csdn.net/u012799221/article/details/73180509
两篇是一样的。
二 垃圾收集算法
如何判断对象已死?
虚拟机使用的是一种称为可达性分析算法的方法来判断对象是否存活。
可达性分析算法:
以一系列GC root对象最为起始点,通过引用链不可达到达的对象,则被判定为可回收对象
GC root 对象可以是是:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
回收的“缓刑”阶段
即使被可达性分析算法认为不可达,那对象也不是非死不可的,他还会:
第一次被标记,然后被筛选是否有必要执行finalize()方法,没有必要执行是:
- 1.对象没有覆盖finalize()方法
- 2.finalize()已经被虚拟机执行过一次。
如果认为有必要执行,则把这个对象放到F-Queue队列中。稍后GC将对F-Queue中的对象第二次标记,如果对象在finalize()方法中将自己与任何一个对象关联,即可从即将回收队列中移除,否则就需要被回收了
finalize()方法只会被调用一次,也不鼓励使用这种方法来拯救对象。
2.1 垃圾收集算法
标记-清除算法
算法分为“标记"和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有。
缺点:
• 效率不高。标记和清除两个过程的效率都不高。
• 产生碎片。碎片太多会导致提前GC。
复制算法:
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优缺点:
• 实现简单,运行高效,但是空间利用率低。
标记-整理算法:
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
优缺点:
• 没有了内存碎片,但是整理内存比较耗时。
分代收集算法:
根据对象存活周期的不同将内存划分为几块。一般分为新生代和老年代。而后根据不同对象特点,分别使用不同的收集算法,例如典型的方法;
• Young区用复制算法
• Old区用标记清除或者标记整理
提示: 虚拟机中通常都不止一种垃圾收集器,可以选择组合来使用。新生代和老年代使用的垃圾收集器不同,自然使用的算法也不同。
三 垃圾收集器
3.1 垃圾收集器总览
3.1.1 垃圾收集器主要分为以下几种:
- 串行收集器Serial:Serial、Serial Old;在服务器上很少使用,主要用于嵌入式设备。
- 并行收集器Parallel:Parallel Scavenge、Parallel Old;吞吐量优先
- 并发收集器Concurrent:CMS、G1;停顿时间优先
下面对几个概念做出解释:
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。适合科学计算、后台处理等弱交互场景
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾收集线程在执行的时候不会停顿用户程序的运行。适合对响应时间有要求的场景,比如Web。
- 停顿时间:垃圾收集器做垃圾回收中断应用执行的时间。-XX:MaxGCPauseMillis
- 吞吐量:花在垃圾收集的时间和花在应用时间的占比。-XX:GCTimeRatio=n,垃圾收集时间占:1/1+n;理想情况是吞吐量大时停顿时间小。GC调优也是向这个方向。
3.1.2 我们可以使用如下几个参数来设置使用哪个垃圾收集器:
使用串行垃圾收集器:
- -XX:+UseSerialGC
- -XX:+UseSerialOldGC
组合为:Serial + Serial Old收集器
使用并行收集器:
- -XX:+UseParallelGC
- -XX:+UseParallelOldGC
组合为Parallel Scavenge + Parallel Old
Server模式下的默认收集器
JVM会根据内存和CPU情况自动判断是否为Server模式。
jinfo -flag UserparallelGC PID:查看是否是使用并行收集器,当然也可以用来查看是否使用其他垃圾收集器,将UserparallelGC换成别的参数即可,如UseSerialGC
使用并发收集器:
- CMS:XX:+UseConcMarkSweepGC ;-XX:+UseParNewGC
- G1:-XX:+UseG1GC
在CMS中如果使用了UseConcMarkSweepGC那么新生代中默认开启UseParNewGC
垃圾收集器组合为:PerNew + CMS + Serial Old(CMS空间分配担保失败退化成SerialOld)
使用G1的话,新生代老年代都使用G1
3.1.3 垃圾收集器的搭配
有连线可以搭配使用
比如:
Old区使用CMS
Young区使用serial
虚线解释:CMS空间分配担保失败退化成SerialOld
推荐使用G1
从这里我们也知道了,每种垃圾收集器使用的区,在后面对每种垃圾收集器具体的描述的时候,不要忘记结合这副图来理解。
3.1.4 选择使用哪种
下面是一些指导原则
- 优先调整堆的大小让服务器自己来选择
- 如果内存小于100M,使用串行收集器
- 如果是单核,并且没有停顿时间的要求,串行或者JVM自己选
- 如果允许停顿时间超过1秒,选择并行或者JVM自己选
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器
如果对垃圾收集感兴趣,想了解更多信息,可以查看:
https://docs.oracle.com/javase/8/ 中的HotSpot Virtual Machine一栏中的 HotSpot Virtual Machine Garbage Collection Tuning Guide
3.2 每种垃圾收集器详解
下面就上面3.1.3垃圾收集器搭配的图中提到的垃圾收集器做一个详细的描述。
3.2.1 Serial收集器
Serial收集器比较简单,它会在用户不可见的情况下把用户正常工作的线程全部停掉,而后开启GC线程,这看起来是难以接受的,就像每工作一个小时就要停止5分钟。它的运行实例图如下:
Serial收集器有自己的优点。首先它相对于其他收集器的单线程简单而高效,在内存容量低,对停顿时间又不太严苛的场景下,只要不是频繁发生,还是一个很好的选择。也是虚拟机运行在Client模式下默认新生代收集器。
3.2.2 Serial Old收集器
运行示例图如上图所示,它实际上就是Serial的老年代版。除了给Client模式下的虚拟机使用,在Server模式下,他还有下面两个主要用途:
- 在JDK1.5以及之前版本中与Parallel Scavenge收集器配合使用
- 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
你将在后面的描述中理解上面的意思。
3.2.3 PerNew 收集器
PerNew收集器其实就是Serial收集器的多线程版本,在实现过程中这两种收集器也共用了相当多的代码,工作过程示例图如下:
虽然相对于Serial收集器,它没有太多的创新,但是却依旧是Server模式下虚拟机中首选的新时代收集器,原因在于,除了Serial收集器,只有他能和CMS收集器配合使用。
PerNew收集器在单CPU的情况下绝对不会有比Serial收集器更好的效果,但是随着CPU数量的增加,GC时其对系统资源的有效利用还是非常有好处的。
3.2.4 Parallel Scavenge 收集器
其和PerNew收集器有非常的相似的地方,如使用复制算法,并行的多线程收集器。。。。但是它有自己的特别之处。它的目的是达到一个可控制的吞吐量。
其提供了两个参数用于精确控制吞吐量
- -XX:MaxGCPauseMillis:最大垃圾收集停顿时间。
- -XX:GCTimeRatio:直接设置吞吐量大小。
此外该收集器还可以实现自适应调节,我们只需要设置好最大堆大小和停顿时间或吞吐量,设置好一个优化目标后具体的细节参数调节就可以交给虚拟机去完成。当然在实际开发过程很少会这样做,我们大多都是手动调节。
3.2.4 Parallel Old收集器
是Parallel Scavenge收集器的老年代版本。在Parallel Old之前,实际上Parallel Scavenge只能和Serial Old进行组合使用,而Serial Old的拖累使得Parallel Scavenge未必在整体应用上可以获得吞吐量最大化的能力。Parallel Scavenge和Parallel Old配合使用可以使得“吞吐量优先”终于可以名副其实。
其和Parallel Scavenge配合使用的示例图如下:
3.2.5 CMS收集器
CMS(Concurrent Mark Sweep 并发标记清除)收集器是一种获取最短回收停顿时间为目标的收集器。
从名字也可以看出来CMS基于标记清除算法的,它的运行过程相对于上面提到的垃圾收集器要复杂一点。主要分为以下四个步骤:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
其中初始标记和重新标记都需要STW(Stop the world)。
- 初始标记:只是标记GC Root能直接关联到的对象,速度很快。
- 并发标记:完成上一步中剩下的关联。
- 重新标记:标记并发标记过程中新产生的垃圾。时间一般要比初始标记长一点,但远小于并发标记时间
- 并发清除:清除垃圾
并发标记和并发清除这两个最耗费时间的任务和用户任务一起工作,所以从整体上看来垃圾清除和用户任务是一起并发执行的。
但是它有以下几个主要的缺陷:
- CPU资源敏感
- 无法处理浮动垃圾
- 可能会产生大量的空间碎片(这一点在标记-清除算法中有提到)
其运行示例图如下:
在上面的描述的算法中都有下面几个特点:
- 新生代和老年代使用不同的算法;
- 年轻代收集使用单eden、双survivor进行复制算法;(这个复制算法不是2.1节中提到的哪个复制算法,而是1.2节提到的博客中对复制算法的改进复制算法)
总结垃圾收集算法使用的算法如下:
- Serial:复制算法
- Parallel Scavenge:复制算法
- PerNew:复制算法
- CMS:标记-清除(并发标记清除)
- Parallel Old:标记-整理
- Serial Old:标记-整理
在接下来的G1收集器中我们将看到,虚拟机关于新生代和老年代的概念没有之前这么强烈了。实际上G1垃圾收集器和前面所描述的垃圾收集器有很大的不同,也是一个重点。
3.2.6 G1收集器 (重点)
G1是一款面向服务端引用的垃圾收集器,主要有以下几个特点:
- 并发于并行
- 分代收集
- 空间整合
- 可预测停顿
上面所说的垃圾收集器都是整个新生代或者整个老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就和其他收集器有很大差别,他将整个Java堆划分为多个大小相等的独立区域:Region,虽然还保留有新生代和老年代的概念,但是新生代和老年代不再是物理隔离的,他们是一部分Region(不需要连续)的集合.
如何做到可预测停顿时间
G1收集器之所以可以做到可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值(回收所获取的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这样一来可以尽可能提高垃圾收集的效率。
如何避免在判断对象是否存活的时候扫描全堆
实际上在G1垃圾收集器中还有一个重要的问题,就是:将Java堆分为一个一个Region后,垃圾收集真的就可以以Region为单位进行了吗?听起来顺理成章但是,Region中不可避免的含有堆别的Region堆中的对象的引用,在判断对象是否存活的时候,不就必须扫描整个堆了吗?实际上在别的垃圾收集器中这也是一个问题,但是其他收集器只会面临两个不同的区域,新生代和老年代。而在G1中却有很多Region,相互之间引用的问题就愈加显现出来。
解决办法是在虚拟机中引入Remembered Set。以G1中的Region为例。在G1中的每个Region都有一个与之对应的Remembered Set。当虚拟机发现在对引用类型的数据进行写操作的时候,会产生一个Write Barrier暂时中断写操作,检查引用的对象是否处于不同的Region中,如果是,则通过CardTable把相关的引用信息记录到被引用对象所属的Region的Remembered Set中。
如果忽略Remembered Set的操作,G1收集器的运作大致分为以下几个步骤:
初始标记:和CMS类似
并发标记:和CMS类似
最终标记:和CMS类似
筛选回收:在筛选回收中,首先对各个Region回收的价值进行排序,而后根据用户所期望的停顿时间来制定回收计划。
这里只是简单的讲解了几个G1收集器的概念,实际的G1收集器非常复杂,这里就不做过的的解释了。更多描述G1垃圾收集器的信息,可以查阅以下博客。其中不单有对G1的详细描述,也对其他垃圾收集器的总结概括:
https://blog.csdn.net/coderlius/article/details/79272773
最后给出几个导出GC日志的JVM参数,:
- -XX:+PrintGCDetails:设置导出GC日志
- -XX:+PrintGCTimeStamps:设置时间戳
- -XX:+PrintGCDateStamps:设置日期戳
- -Xloggc:$CATALINA HOME/logs/gc.log:设置GC日志导出路径
- -XX:+PrintHeapAtGC:发生GC的时候打印堆的使用情况
- -XX:+PrintTenuringDistribution:发生GC的时候打印新生代年龄的信息
GC日志直接分析的话,会比较繁杂,我们可以使用工具:
网站:http://gceasy.io/
软件:GCViewer