前言-废话
这依然是一篇面试向的文章,但是为了能把GC说明清楚,还是会引入一些JVM调优的实战技能,也可以作为JVM调优的入门篇。
GC,Garbage Collection,一直是装逼面试的重灾区,越是大厂越喜欢问。一方面GC确实很复杂,可以说JDK每次版本升级都涉及到GC的优化。再加上GC又比较虚,分析调优都有不小的门槛,基本上不可能有JAVA程序员能了解到GC的细节全貌。这也导致GC的面试题基本上是装逼无穷尽的。任何一个细节都可以找出无数个面试题,保证可以把你打击到恨不得钻地缝(别不服,一个CMS调优,JVM就有几百个参数,能搞懂的保证没有几个)。
但是,GC又确实很重要,一是因为对JVM调优的工作绝大多数都是针对GC。内存泄漏、Out Of Memory、应用越运行越卡、频繁宕机这些问题除了代码层面外,大都跟GC有关。另一个更重要的原因是GC是在面试时拉开跟别的程序员水平最重要的地方。框架、技术,大家天天都在用,那面试时怎么证明你比别人更牛呢?GC绝对是一大杀手锏。
下面跟我一起趟下这滩浑水把。
GC基础
JAVA引用类型:强引用、弱引用、软引用、虚引用
Java中4种引用的级别和强度由高到低依次为:强引用 -> 软引用 -> 弱引用 -> 虚引用
当垃圾回收器回收时,某些对象会被回收,某些不会被回收。垃圾回收器会从根对象Object
来标记存活的对象,然后将某些不可达的对象和一些引用的对象进行回收。
引用类型 | 被垃圾回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | 当内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 | 正常垃圾回收时 | 对象缓存 | 垃圾回收后终止 |
虚引用 | 正常垃圾回收时 | 跟踪对象的垃圾回收 | 垃圾回收后终止 |
1 强引用Strong Reference
强引用是最普遍的引用,如果一个对象有强引用,那垃圾回收期绝不会回收该对象。
2 软引用 SoftReference
如果一个对象只有软引用,则内存空间充足时,垃圾回收期不变会回收。但是如果内存空间不足,垃圾回收器就会回收该对象,节省内存空间。
3 弱引用 WeakReference
弱引用与软引用的区别在于,只有弱引用的对象拥有更短暂的生命周期。垃圾回收器线程在扫描他所管辖的内存区域时,一旦发现只具有弱引用的对象,不管当前内存是否足够,都会立即回收。不过,由于垃圾回收器线程是一个优先级非常低的线程,因此不一定会很快发现这些只具有弱引用的对象。弱引用可以重新声明成强引用。
4 虚引用 PhantomReference
虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
设置虚引用的唯一目的就是在这个对象被GC回收时,收到一个系统的通知或者后续添加进一步的处理。例如触发对象的finalize方法。
虚引用必须和引用队列联合(ReferenceQueue)使用。
5 引用队列 ReferenceQueue
ReferenceQueue是用来配合Reference工作的。而引用不使用引用队列一样的工作。
JVM基本参数
在开始GC前,先简单看下JVM的参数,这个在后面会需要用到。
JVM的参数分为三类:
1.标准命令:-开头,是所有HotSpot都支持的参数。例如 java -version,这类参数可以使用指令java -help或者java -?全部打印出来。
-
非标准指令: -X开头,特定版本的HotSpot支持的指令。例如java -Xms200M -Xmx200M。这类指令可以用java -X 全部打印出来。
-
最后一类,不稳定指令,这也是JVM调优的噩梦。-XX 开头,这些参数是跟特定HotSpot版本对应的,很有可能换个版本就没有了。详细的文档资料也特别少。下面列出JDK1.8的几个有用的指令。
java -XX:+PrintFlagsFinal:所有最终生效的不稳定指令。
java -XX:+PrintFlagsInitial:默认的不稳定指令
java -XX:+PrintCommandLineFlags:当前命令的不稳定指令 --这里可以看到是用的哪种GC。 JDK1.8默认用的ParallelGC
然后,还有JDK下的一些调优常用的工具:jInfo、jstack、jvisualVM等指令,这些都是很常用的。
还有,阿里开源的arthas也是JVM调优常用的工具,这个工具基于JVMTI(JVM Tool Interface)实现,实现了JDK除了jmap之外的几乎全部调优指令。使用文档很多,功能也很强大。例如 thread -b直接找出死锁
jmap指令可以用来操作JVM的整个堆内存,这在一些大型项目中,都是不被允许的。
什么是垃圾?
没人用的东西就是垃圾。内存中一个对象没有任何引用了,那他就是垃圾,需要回收。
在C和C++中需要手动释放内存,但是在JAVA中就不需要手动释放了,JVM虚拟机会自动完成。相比手动释放,JVM的垃圾回收机制编程上更简单,系统不容易出错。
这里有个概念性的面试题。内存泄漏与内存溢出。
垃圾内存无法回收,这就是内存泄漏。
内存泄漏积累到一定程度,就会造成内存溢出
有了这些基础,后面的GC就可以看到一些端倪了。在下面看GC算法之前,其实有一个主线脉络,就是GC算法是随着内存越来越大,不断进行改进的。而GC算法优化的目前就在减少STW(Stop The World),提升响应时间。下面开始直奔主题了。
GC算法
如何定位垃圾?
有两种定位垃圾的方法:
1:引用计数: 这种方式是给堆内存中每个对象记一个引用个数,引用个数为0的就认为是垃圾。
这是早期JDK使用的方式,但是这种方式无法标记出循环引用的情况。
2:根可达算法: 这就是在内存中,从引用根对象一直往下找引用,找不到的对象就是垃圾。
这里有个最经典的面试题,哪些是根?JVM中根包括JVM stack栈、native method stack 本地方法栈、run-time constant pool 常量池、static references in method area 静态变量、class 类
常见的垃圾回收算法
- Mark-Sweep 标记清除
这是最基础的垃圾回收算法。标记清除算法分两个阶段,标记阶段:把垃圾内存都标记出来。清除标记:把标记为垃圾的内存进行回收。
这种方式实现比较简单,但是有一个严重的问题就是会产生大量的内存碎片。而内存碎片太多可能会导致后续需要申请大内存对象时,会无法找到足够的内存空间,从而提前触发新的垃圾回收操作。
- Copying拷贝算法
为了解决标记清除算法的缺陷,后面就有了拷贝算法。拷贝算法就是将内存分为大小相等的两块,每次只使用其中的一块。垃圾回收时,将当前这一块的活对象拷贝到另一块内存,当前这一块直接全部清理掉,这样就不会出现内存碎片了。
这种算法没有碎片,但是浪费空间,内存只能用一半。而且,拷贝算法的性能跟存活对象多少有关,当存活对象过多时,拷贝算法的效率会大大降低。
- Mark-compack 标记压缩
为了解决拷贝算法的缺陷,后面又提出了标记压缩算法。该算法标记阶段和标记删除算法一样,但是在完成标记后,不是直接清理垃圾对象,而是将存活对象都往一端移动,然后清除掉端边界以外的内存。
这三种算法各有利弊,不存在哪个算法绝对比另一个算法好的问题。只是都有各自更适用的场景。
垃圾回收器整理
java从诞生到发展到现在最新的JDK14版本,总共产生了以下十个垃圾回收器:
面试时如果说十种垃圾回收器全部了解,那绝对是超级大牛了哈。
简单解释下这个图:
- 左侧方框内的是分代垃圾回收器,都属于比较早期的垃圾回收器。例如现在最常用的JDK版本,还是Parallel,但是也支持CMS和G1。方框中上方为年轻代垃圾回收器,下方是老年代垃圾回收器,有虚线连接的是可以配合使用的垃圾回收器。这其中有点诡异的是CMS到serialOld的连线。其实这里是指出CMS垃圾回收器中会有些情况就是使用的SerialOld垃圾回收器,这个后面再说。
- 右侧方框外的是不分代的垃圾回收器,属于更新一代的垃圾回收器。其中G1在JDK1.8中已经可以支持。ZGC是Oracle正版的新一代垃圾回收器,支持4T以上的内存空间。shennandoah则是目前在openJDK中使用的最新一代垃圾回收器,两者都基于java更新一代的虚拟机Zing,以后都有机会并入JDK的后续版本中。而Epsilon这个垃圾回收器比较特殊,因为他根本就不干活,只是在调试JDK时会偶尔用到。
看到这里,一下子多了非常多名词,那下面先介绍分代算法,然后再来介绍各个垃圾回收器。
内存分代模型
在钻进这个牛角尖之前, 我们还是先看个新鲜。先不加任何参数,直接用java指令执行以下这个小的java项目(这里用的版本是最常用的JDK1.8版本):
import java.util.*;
public class echo{
public static void main(String[] args) throws Exception{
System.out.println("start echo service");
List<Character> tl = new ArrayList<>();
for(;;){
char c = (char)System.in.read();
System.out.println(c);
tl.add(c);
}
}
}
然后我们使用arthas的dashboard指令,来看看简单的内存面板。–使用方法比较简单,这里就不介绍了。我们关注面板中的这一段。
红色方框这一段就表明了现在是使用的Parallel Scavenge垃圾处理器,并且展示了内存各个年代的大小。
这就引出了JVM的内存划分模型:
JVM在使用堆内存时,会将内存划分为三个区域,新生代(Young),老年代(Old),永久代(Permanent)三个区域。其中新生代大概占1/3,老年代占2/3。
-
新生代:
新生代用来存放被认为经常会被丢弃的对象。反生在新生代的GC过程称为MinorGC或者叫YoungGC,YGC。这一区域的GC效率是非常高的,对程序性能影响不大。
新生代会继续划分为Eden区,Survivor0 和 Survivor1三个区域,三个区域的内存比例默认是8:1:1 。new 出来的对象,会先进入Eden区。经过一次MinorGC或者叫Y(oung)GC后,会把存活的对象存入到S区。然后S0和S1则是用来互相保存一次YGC后存活的对象(这就是拷贝算法),并且会给对象加一个年代计数。如果对象过大或者对象的年代计数过大(阈值跟GC算法有关,PS默认是15,可以配置),就会将对象放入老年代
具体的JVM参数以后会逐步补充。
这里就有一个JVM的假设,就是new出来的对象,绝大部分会在一次YGC后全部清理掉。 -
老年代:
老年代是用来存储被认为是比较稳定的对象,接收从新生代晋升上来的对象。发生在这一区域的GC被称为MajorGC或者FullGC,FGC。FGC采用标记清除的方式进行内存回收。而FGC由于要扫描的内存区域非常大,因此是不能太频繁的进行GC的。FGC的频率与性能也是JVM调优重点关注的地方。当老年代的GC无法清除垃圾对象时,就会发生内存泄漏,而内存泄漏积累到一定程度,就会发生OOM(Out Of Memory)异常。
这里延伸出另一个问题STW(Stop The World)。FGC由于需要标记清除内存,在清除期间就必不可少的要停止内存的变动,这就需要冻结内存,于是这期间就会发生STW,表现为程序无响应。而JVM的GC算法优化的核心问题就是要缩短STW的频率及时间。
-
永久代:
这一区域被用来存放一些class或者Meta元数据的消息。包括虚拟机加载的类信息、常量池、静态变量和即时编译后的代码。这一区域内存是不会被GC回收的,而这一区域也是有可能抛出OOM的。在JDK1.8中,永久代被从内存中移除,改为一个被称为MetaSpace元数据区取代。并且,元数据区被转移出了JVM虚拟机,转而使用系统内存区,不由JVM的内存参数控制。
有了这个基础之后,我们再来看上面列出的各个GC是如何逐步进化的。
Serial 串行标记
这个就是单线程的内存回收器。是最早期的内存回收算法。适合几十兆的内存空间,在单CPU下的效果还可以,到多CPU后,性能还会有所下降。
整体内存回收的过程如下:
这个过程比较简单,就像足球比赛一样,需要GC时,直接暂停,GC完了之后再继续。
Parallel 并行标记
这个内存回收器就是在Serial的基础上,增加了多线程GC。PS+PO的组合是JDK1.8的默认垃圾回收器。在多CPU核下效果会比Serial更好。
比较我们开始贴出来的GC总结图,年轻代Parallel Scavenge+老年代Parallel Old的组合就是常说的PS+PO组合,这是比较稳定常用的组合。但是随着内存逐步扩大,即便使用多线程进行GC,STW的时间也依然会逐渐膨胀。而线程数显然是不可能一直增加的。于是,便进一步有人提出了CMS垃圾回收器。而ParNew其实就是Parallel垃圾回收器为了配合CMS提出的年轻代的垃圾回收器。
CMS:三色标记+Increment update+写屏障
然后到了CMS(ConcurrentMarkSweep ),就进入了垃圾回收期最噩梦的阶段。为什么这么说呢? 一是CMS是一个承上启下的垃圾回收器,他推动了垃圾回收器从分代模型向不分代模型进化。二是CMS相当复杂,JVM调优参数也是最多的。三是CMS有不可调和的浮动垃圾和序列化回收问题。因此,他在任何一个版本的JDK中都不是默认的垃圾回收期,并且在JDK1.9之后就取消了。
他出现的核心思想就是想要减少Parallel的STW时长,而他的想法就是并行标记,即GC与用户进程一起执行。
CMS是MarkSweep,必然会产生大量的内存碎片,当碎片到一定程度后,CMS老年代分配对象分配不下了,就会使用Serialold进行老年代串行回收。
他的大致流程如下图:
四个阶段
初始标记:STW,标记出根对象直接引用的对象。很快。
并发标记:继续标记其他对象,浪费80%GC时间,同应用程序一同运行
重新标记:STW,并发处理中产生了新的垃圾,或者有一些垃圾重新被使用,不再是垃圾。因为大部分垃圾在并发标记时处理过了,所以也很快。
并发清理:将产生的垃圾清除掉,但是同时也会产生新的垃圾。也就是浮动垃圾,这种浮动垃圾只能等待下一个垃圾回收进行处理
整理阶段
可以看到他的想法就是一轮标记中并不需要等待全部标记完成,而使GC过程可以分为多个阶段分段执行,并且让一部分阶段可以跟用户线程并行进行。所以CMS问题的核心就是在分阶段GC后,如何保证下一阶段可以继续进行,并且在中断过程中,用户线程导致的引用关系变化不会影响GC。而正是这个问题的处理过程,成了CMS与G1的根本区别。
CMS的核心算法是三色标记+Increment update。
三色标记:这是一个逻辑上的抽象,即将每个内存对象分成三种颜色状态,黑:自己和成员变量都标记完毕。灰:自己标记完了,但是成员变量未全部完成标记。白:自己未被标记。这种逻辑下,如果最终全部对象都标记完成了, 白色的对象就全都是垃圾了,下次就可以进行删除。
Increment update 增量标记:这是个什么东东呢?这就需要考虑以下这种情况:
1、内存中三个对象,A引用B, B引用C。初始标记情况A和B已扫描完, C尚未扫描,如下:
2、经过一轮暂停后,引用情况变成了A引用B, A引用C。如下:
此时如果只按三色标记,就会发生漏标记的问题。A已经是黑色,认为A已标记完,就不会再去找A的子引用。B是灰色,还会继续找B的子引用,而此时B是没有子引用的。那这种情况下,C就还是不会被扫描,就会被清除。但是C实际上是被引用的,这时就会造成验证的漏标记问题,造成A的引用C丢失了。这显然是不被允许的。
那怎么解决这种漏标记问题呢?CMS的解决方案就是Increment update,可以理解为下一轮扫描时,将A重新标记为灰色,继续扫描他的子引用,这样就不会溜掉C了。
CMS的问题:
CMS采用增量更新方式来进行并发标记以及重新标记,但是如果在并发标记阶段,内存变化非常大(比如A的属性有多个,同时有多个业务线程和GC标记线程等情况)时,CMS在重新标记阶段需要扫描的内存依然会非常大,STW时间依然会很长。
另外,CMS是采用的MarkSweep标记清除的方式,而标记清除方式不可避免的会产生大量的内存碎片。当内存碎片比较严重时,会提前触发一次FGC。而CMS在这个时候是使用SerialOld来进行老年代的FGC的,而这个STW就会非常长。
G1: 三色标记+SATB+写屏障
G1:Garbage First。是第一个不分代的垃圾回收器,但其实他并不是完全的不分代,他是逻辑分代,物理不分代,可以支持上百G的内存,是目前性能最高的垃圾收集器,JDK1.7开始引入,JDK1.8中已经支持,但不是默认的,需要使用**-XX:+UseG1GC**指令打开。而在JAVA9中被指定为默认垃圾收集器。
并且G1的算法更为先进,JVM的调优参数也较CMS有了大幅度的缩减,但是,如果你认为可以掌握G1的参数全貌,依然是个很艰难的事情,Oracle官方也很难找到全面详细的文档。G1最惊艳的地方是他可以设置期望的STW时长。‐XX:MaxGCPauseMillis -> 目标暂停时间(默认200ms)
G1 内存模型:
随着内存的继续扩大,老年代也会跟着不断膨胀,这样,即使并行标记,也无法快速完成垃圾清理。G1就不再将内存进行完整的划分,而是分成多个Region。每次垃圾清理并不清除整个内存,而是清除那些垃圾最多的Region。清除方式也是采用比较快速的拷贝算法,即将当前Region里的存活对象全部拷贝到另外一个空Region,然后当前Region就可以全部清除了。
G1的内存管理不再分为完整的新生代和老年代,而是划分为一个个的小区域Region。(‐XX:G1HeapRegionSize -> 指定分区大小(1MB32MB,且必须是2的幂),默认将整堆划分为2048个分区),每个Region的大小在132 M之间,具体大小取决于堆的大小。
对于每一个Region,会有一个唯一的分代标志。其中,H是表示大对象humongous object,H区是Old区的一种。HotSport中大对象的标准是超过一个Region的1/2。这些分代标志并不是确定的,而是会随时变化的。而每次的GC时,并不需要清理整代的内存区,而是可以选择一部分Region。并且在选择需要清理的Region时,会优先选择垃圾满了的Region,这也是G1名字的由来。
而在对Region进行清理时,是将整个Region中的存活对象复制到另一个Region。
G1的几个重要概念:
RSet: 每个Region会有1%~2%的内存空间用于保存RS , RemeberSet,在这里包含了所有引用了本Region对象的Region指针。
CSet: 本次GC需要清理的Region集合。
TLAB:Thread Local Allocation Buffer,默认启用,分配在Eden空间,属于单个线程,每一个线程都有一个TLAB用于分配当前线程创建的小对象,避免了多个线程同时往一个Eden Region分配内存造成的指针碰撞问题。
PLAB:Promotion Local Allocation Buffer,晋升的过程中,无论是晋升到S还是OLd区,都是在GC线程的PLAB中进行。每个GC线程都有一个PLAB。
G1的运行步骤:
YGC阶段,会将需要清理的 Eden和S0 中的存活对象复制到一个新的S1 Region中。
MixGc: 老年代的GC。也是跟CMS采用的三色标记的步骤。也分为四个步骤,但是稍有区别。
第一步、初始标记: 标记GcRoot对象及其直接引用的对象,同时标记GcRoot所在的Region。这一步是STW的。
第二步、标记Region: 通过RSet标记出上一阶段标记出的Region引用到的Old区Region。
第三步、并发标记:跟CMS中的步骤一样,只是遍历的范围不再需要遍历整个Old区,而需要遍历第二步标记出来的Region。
第四部、重新标记: 跟CMS中的重新标记一样,只是他是采用SLAB方案来取代CMS的增量更新方案。
第五步、垃圾清理:G1采用复制算法来清理整个Region的信息。这时是会产生STW的。而这个阶段G1只选择垃圾较多的Region,并不是完全清理。
G1的SATB:
G1同样采用的三色标记的方案,那他也依然要处理之前CMS提到的漏标的问题。G1采用的方案是SATB:Snapshot At the Begining。就是在开始时保持一个快照。还是从上面CMS的第二个步骤继续:
SATB会在开始时保存一个内存引用的快照,然后在下次扫描时,通过比较发现B到C的引用消失了,此时就会把B到C的这个引用推到GC的堆栈中。下次扫描时,G1线程就能单独对C进行引用扫描。再配合之前提到的G1的RSet,就可以快速定位到引用到C的Region,这样可以大大缩短扫描出A到C引用的过程。
这里有非常多的面试题,G1的Region不连续,那他怎么快速找到相关联的Region?G1并不需要Region连续,只要他们逻辑上相关联就行。
还有: G1两个region不是连续的,而且之间还有可达的引用,我现在要回收其中一个,另一个会被怎么处理。 复制拷贝。关于G1,还有个很常见的面试题:G1是不是彻底解决了STW?答案是NO。三色标记虽然大大减少了STW,将一部分标记时间改为了与用户线程并行,但是在他的初始标记和增量标记阶段,还是有STW的,只是这个STW被分成了多个阶段,从而大大缩减了。
另外,关于核心算法之外,还涉及到很多的细节,这里就不做详细介绍了。
未来的垃圾回收器:ZGC、shennandoah
ZGC是JDK11开始引入的新型垃圾回收器。而shennandoah则是RedHat在openJDK中引入的非官方垃圾回收器,目前还没有引入官方JDK中。这两个垃圾回收器都是基于JDK的新型虚拟机Zing开发的,未来都有可能被JDK选为默认垃圾回收器。到这两个垃圾回收器成熟的时候,JVM调优的工作就差不多要消失了。
ZGC,目前还在测试阶段,对我们大众,也就停留在尝鲜的阶段。ZGC在HotSpot版本中是采用的颜色指针的概念。相比G1,有很多亮瞎眼的优秀特性。例如,ZGC是一个完全不分代的垃圾回收期(未来有可能分为冷热数据)。他支持的最大内存大小在JDK11中是4TB(2的42次方),到JDK13中,最大支持6TB(2的44次方),这已经是天方夜谭级别的配置了。而他的STW据说非常短,到了几毫秒的级别,基本可以跟C的效率相当了,并且内存的大小基本不会影响STW时间。然后关于ZGC的JVM调优工作基本上是零,是的,他的算法自己就会给自己调优。
而至于shennandoah,由于是非官方出品,虽然效果挺不错,但是跟ZGC的竞争还是稍处下风。他更像是G1的改进版,有资料表明shennandoah甚至跟G1有很多代码是共用的。
整理部分JVM调优参数
JVM调优参数太多太复杂了,完整的资料也很少,再加上不同版本变化很大。这里只列几个平常比较常用的参考一下把。
以JDK1.8为例,使用java -XX:+PrintFlagsInitial 可以打印出所有的不稳定参数的默认值,可以看看其中比较重要的几个指令。
1 选择垃圾回收器:这些参数可以与上面提到的这些垃圾回收器印证。
-XX:UseG1GC; -XX:UseParNewGC; -XX:UseParallelGC; -XX:UseParalleloldGC; -XX:UseSerialGC, -XX:UseConcMarkSweepGC
然后可以用grep G1和grep CMS,比较一下两个GC的配置参数,G1大概二十多个,CMS有将近100个
2 规划GC日志文件,以下指令可以规划分五个文件轮训,每个文件20M。共保存最近100M的GC日志。
-Xloggc: /opt/xxx/logs/xxx-xxx-gc.%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause -verbose:gc
3 调整内存分配:
一般是使用-XX:MaxGCPauseMillis=50 这个参数设置每次年轻代垃圾回收的最长时间。JVM会自动调整年轻代内存大小,以适应这个时间。
另外,还有一些常用的设置内存分配的参数:
-Xms:初始堆内存大小
-Xmx:堆内存最大值
-XX:NewSize: 设置年轻代大小
-XX:NewRatio:设置年轻代与老年代的内存比例,默认是2,表示年轻代与老年代的内存比例是1:2
-XX:SurvivorRatio:设置年轻代中Eden区与Survivor区的内存比例。这里注意下Survivor是有两个的,默认值是8,这就表示Eden:S0:S1的内存大小比例是8:1:1
4 TLAB:Thread Local Allocation Buffer,线程本地分配缓存区。
在指令中,可以看到几个参数: -XX:UseTLAB; -XX:ZeroTLAB; -XX:TLABSize(TLAB空间大小,默认0); -XXTLABWasteTargetPercent(TLAB最大浪费空间站Eden区百分比,默认1); -XX:TLABWasteIncrement(TLAB空间增长,默认4)等等几个跟TLAB有关的参数,这了就是对TLAB内存块相关的配置。
5 然后有一些调试用的指令:
-XX:+PrintGC: 打印GC信息
-XX:+PrintGCDetails: 打印GC详细信息,这里主要是用来观察FGC的频率以及内存清理效率。
-XX:+PrintGCTimeStamps -XX:+PrintGC:
-XX:PrintHeapAtGC: 打印GC前后的堆栈信息
-Xloggc:filename : GC日志打印文件
最后
虽然JDK发展这么快,但是目前使用最多的依然是JDK1.8版本,期待以后JDK越来越强大吧。估计等到了ZGC这一类亮瞎眼的新特性成熟了,JAVA环境会有个大的提升把。不过到时候,这篇文章也就成了一个古老的传说了,大家且看且珍惜把。