前言
垃圾:简单说就是内存中已经不在被使用到的内存空间就是垃圾。
垃圾回收(Garbage Collection,GC):顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
Java 语言出来之前,大家都在拼命的写 C 或者 C++ 的程序,而此时存在一个很大的矛盾,C++ 等语言创建对象要不断的去开辟空间,不用的时候又需要不断的去释放空间,既要写构造函数,又要写析构函数,很多时候都在重复的 allocated,然后不停的析构。于是,有人就提出,能不能写一段程序实现这块功能,每次创建,释放空间的时候复用这段代码,而无需重复的书写呢?
1960年,基于 MIT 的 Lisp 首先提出了垃圾回收的概念,用于处理C语言等不停的析构操作,而这时 Java 还没有出世呢!所以实际上 GC 并不是Java的专利,GC 的历史远远大于 Java 的历史!
Java 垃圾回收机制
垃圾回收主要关注 Java 堆
Java 内存运行时区域中的程序计数器、虚拟机栈、本地方法栈随线程而生灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的(尽管在运行期会由 JIT 编译器进行一些优化),因此这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
而 Java 堆不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是这部分内存。
怎么定义垃圾
引用计数算法(Reachability Counting)
通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。如果该对象被其它对象引用,则它的引用计数加1,如果删除对该对象的引用,那么它的引用计数就减1,当该对象的引用计数为0时,那么该对象就会被回收。
- 优点:实现简单、效率高。
-
缺点:不能解决对象之间循环引用的问题。
可达性分析算法(根搜索算法)
可达性分析算法(Reachability Analysis)的基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。
在 Java 语言中,可作为 GC Root 的对象包括以下4种:
虚拟机栈(栈帧中的本地变量表)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(NPE),类加载器等等
所有被同步锁持有的对象
反应Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等
目前的主流Java虚拟机使用的都是准确式GC,HotSpot虚拟机使用了一组叫做OopMap的数据结构以达到准确式GC的目的。
在HotSpot中,对象的类型信息里有记录自己的OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。所以从对象开始向外的扫描可以是准确的;这些数据是在类加载过程中计算得到的。
-
每个被JIT编译过后的方法也会在一些特定的位置记录下OopMap,记录了执行到该方法的某条指令的时候,栈上和寄存器里哪些位置是引用。这样GC在扫描栈的时候就会查询这些OopMap就知道哪里是引用了。这些特定的位置主要在:
- 1、循环的末尾
- 2、方法临返回前 / 调用方法的call指令后
- 3、可能抛异常的位置
在OopMap的协助下,JVM可以很快的做完GC Roots枚举,但是JVM并没有为每一条指令生成一个OopMap。
记录OopMap的这些"特定位置"被称为安全点(safepoint),即当前线程执行到安全点后才允许暂停进行GC。
如果一段代码中,对象引用关系不会发生变化,这个区域中任何地方开始GC都是安全的,那么这个区域称为安全区域。
引用分类
StrongReference: 强引用
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
public class Main {
public static void main(String[] args) {
new Main().fun1();
}
public void fun1() {
Object object = new Object();
Object[] objArr = new Object[1000];
}
}
SoftReference: 软引用
软引用是用来描述一些有用但并不是必需的对象,在 Java 中用 java.lang.ref.SoftReference
类来表示。对于软引用关联着的对象,只有在内存不足的时候 JVM 才会回收该对象。因此,这一点可以很好地用来解决 OOM 的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。下面是一个使用示例:
import java.lang.ref.SoftReference;
public class Main {
public static void main(String[] args) {
SoftReference<String> sr = new SoftReference<String>(new String("hello"));
System.out.println(sr.get());
}
}
WeakReference: 弱引用
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
import java.lang.ref.WeakReference;
public class Main {
public static void main(String[] args) {
WeakReference<String> sr = new WeakReference<String>(new String("hello"));
System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());
}
}
PhantomReference: 虚引用
“虚引用”也称为幽灵引用或幻影引用,顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在 Java 中用 java.lang.ref.PhantomReference
类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。要注意的是,虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class Main {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
}
}
跨代引用
跨代引用:也就是一个代中的对象引用另一个代中的对象。
跨代引用假说:跨代引用相对于同代引用来说只是极少数。
隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或同时消亡。
记忆集
记忆集(Remembered Set):一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
记忆集记录精读
不考虑效率和成本,最简单的实现可以用非收集区域中所有含跨代引用的对象数组。但空间开销大,成本高。
在垃圾收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针,故选择更粗狂的颗粒记录,可供选择的有字节精读、对象精读、卡精读(卡表)。
字节精读:每个记录精确到一个机器字长,该字节包含跨代指针。
对象精度:每个记录精确到一个对象,该对象里有字段含有跨代指针。
卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。
卡表(Card Table):是记忆集的一种具体实现,它定义了记忆集的记录精度、与堆内存的映射关系等。卡表最简单的形式可以只是一个字节数组。
卡表的每个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块称为卡页(Card Page)。
写屏障(write barrier)
写屏障就是对一个对象引用进行写操作(即引用赋值)之前或之后附加执行的逻辑。
通过写屏障来实现当对象状态改变后,维护卡表状态。
写屏障与记忆集
每次在对一个对象引用进行赋值的时候,会产生一个写屏障中断操作,然后检查将要写入的引用指向的对象是否和该引用当前指向的对象处在不同的region中;如果不同,通过CardTable将相关的引用信息记录到Remembered set中;当进行垃圾收集时,在GC根节点的枚举范围内加入Remembered Set,就可以保证不用进行全局扫描。
判断是否垃圾的步骤
1、如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。
当对象没有覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,直接进行第二次标记。
两个步骤走完后,如果任然没有使用,那就属于垃圾。
如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。
这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,因为如果一个对象在 finalize() 方法中执行缓慢,将很可能会一直阻塞 F-Queue 队列,甚至导致整个内存回收系统崩溃。
GC类型
MinorGC/YoungGC:发生在新生代的收集动作。
MajorGC/OldGC:发生在老年代的GC,目前只有CMS收集器会有单独收集老年代的行为。
MixedGC:收集整个新生代以及部分老年代,目前只有G1收集器会有这种行为。
FullGC:收集整个Java堆和方法区的GC。
Stop The World
Java中Stop-The-World机制简称STW,Java中一种全局暂停现象,多半由于GC引起。所谓全局停顿,就是所有Java代码停止运行,native代码可以执行,但不能与JVM交互。
其危害是长时间服务停止,没有响应;对于HA系统可能引起主备切换,严重危害生产环境。
目前垃圾收集器的优化方向基本上都是在尽量减少或者缩短Stop The World的时间。
jvm-Stop the world:https://blog.csdn.net/jakeswang/article/details/107673734
垃圾收集类型
串行收集:GC单线程内存回收,会暂停所有的用户线程,如:Serial
并行收集:多个GC线程并发工作,此时用户线程是暂停的,如:Parallel
并发收集:用户线程和GC线程同时执行(不一定是并行,可能交替执行),不需要暂停用户线程,如:CMS
判断类无用条件
JVM中该类的所有实例都已经被回收。
加载该类的ClassLoader已经被回收。
没有任何地方引用该类的Class对象。
无法在任何地方通过反射访问这个类。
垃圾收集算法
Mark-Sweep: 标记-清除算法
这是最基础的算法,就像它名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象(如哪些内存需要回收所描述的对象),对标记完成后统一回收所有被标记的对象,如下图所示:
优点:简单。
缺点:
- 一个是效率问题,标记和清除两个过程的效率都不高。
- 另一个是空间问题,标记清除后会产生大量的不连续的内存碎片,可能会导致后续无法分配大对象而导致再一次触发垃圾收集动作。
Copying: 复制算法
为了针对标记-清除算法的不足,复制算法将可用内存容量划分为大小相等的两块,每次只使用一块。当一块的内存用完了,就将还存活的对象复制到另一块上面去。然后把已使用过的内存空间一次清理掉,如下图所示:
优点:实现简单,运行高效,不用考虑内存碎片问题。
缺点:使用内存比原来缩小了一半。
现在的商业虚拟机都采用这种收集算法来回收新生代,有企业分析的得出其实并不需求将内存按1:1的比例划分,因为新生代中的对象大部分都是“朝生夕死”的。所以,HotSpot虚拟机默认的Eden和Survivor的大小比例是8:1。一块Eden和两块Survivor,每次使用一块Eden和一块Survivor,也就是说只有10%是浪费的。如果另一块Survivor都无法存放上次垃圾回收的对象时,那这些对象将通过“担保机制”进入老年代了。
担保机制—分配担保
当新生代进行垃圾回收后,新生代的存活区放置不下或Eden区放置不下新的大对象,那么需要把这些对象放置到老年代去的策略,也就是老年代为新生代的GC做空间分配担保,步骤如下:
- 1、在发生MinorGC前,JVM会检查老年代的最大可用的连续空间,是否大于新生代所有的对象的总空间,如果大于,可以确保M inorGC是安全的。
- 2、如果小于,那么JVM会检查是否设置了允许担保失败,如果允许,则继续检查老年代最大可用的连续空间,是否大于历次晋升到老年代对象的平均大小。
- 3、如果大于,则尝试进行一次MinorGC。
- 4、如果不大于,则该做一次FullGC。
Mark-Compact: 标记-整理算法
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在年轻代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。
标记-整理算法和标记-清除算法差不多,都是一开始对回收对象进行标记,但后续不是直接对对象清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,如下图所示:
分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
垃圾收集器
垃圾收集算法只是内存回收的方法,垃圾收集器是来具体实现这些算法并实现内存回收的。
不同厂商、不同版本的虚拟机实现差别很大,HotSpot中包含的收集器如下图所示:
注: 当老年代配了 CMS收集器时, 如果内存使用率超过了一定的比例, 系统会抛出 Concurrent Mode Failure,此时会自动采用 Serial Old收集器做 Full GC。
1、红色虚线在 Jdk8时, 将Serial与 CMS的组合和ParNew与 Serial Old的组合声明为废弃, 并在 Jdk9时完全弃用了。
2、黄色虚线在 Jdk14时, 弃用了 Parallel Scavenge与 Serial Old的组合。
3、绿色虚线在 Jdk14时, 完全弃用了 CMS垃圾收集器。
近期垃圾收集器发展过程
- Jdk1.7u4开始全面支持 G1垃圾收集器。
- Jdk9时 G1成为了,默认的垃圾收集器, 替代了 CMS. (CMS声明为废弃)。
- Jdk10时 G1垃圾收集器,实现了并行性来改善了最坏情况下的延迟。
- Jdk11时引入了 Epsilon垃圾收集器,又称为 No-Op(无操作)收集器。同时,引入了 ZGC(The Z Garbage Collector),Oracle公司的可伸缩的低延迟垃圾收集器。
- Jdk12时增强了G1垃圾收集器,自动返回未用的堆内存给操作系统。同时,引入 OpenJDK引入了,红帽公司开发的 Shenandoah GC低延迟垃圾收集器(试验性阶段)
- Jdk13时增强了 ZGC,自动返回未用堆内存给操作系统。
- Jdk14时完全弃用了 CMS垃圾收集器(如果显式设置会提示警告,但不会中断。而会自动选择默认收集器就是 G1)。扩展了 ZGC在 MacOS和 Windows上的应用。
具体组合如下:
Young | Tenured | JVM options | Description |
---|---|---|---|
Serial | Serial Old | -XX:+UseSerialGC | 单线程进行GC,适合单CPU或小内存,单机程序 |
Serial | CMS+SerialOld | -XX:-UseParNewGC -XX:+UseConcMarkSweepGC | CMS进行GC失败时,会自动使用Serial Old 策略进行GC;JDK8中声明为废弃 |
Parallel Scavenge | Serial Old | -XX:+UseParallelGC | Jdk1.5及之前版本的搭配使用 |
Parallel Scavenge | Parallel Old | -XX:+UseParallelGC -XX:+UseParallelOldGC | 适合多CPU,需要最大吞吐量,如后台计算型应用;是JDK8默认收集器策略 |
Parallel New | Serial Old | -XX:+UseParNewGC | JDK8中声明为废弃 |
Parallel New | CMS+SerialOld | -XX:+UseParNewGC -XX:+UseConcMarkSweepGC | 适合多CPU,追求低停顿时间,需快速响应如互联网应用 |
G1 | -XX:+UseG1GC | JDK9默认收集器 | |
ZGC | -XX:+UseZGC |
垃圾收集算法有:复制算法、标记-清除算法、标记-整理算法。
1、串行垃圾回收器
串行的垃圾收集器有两种,Serial与Serial Old,一般两者搭配使用。是单线程收集器,在垃圾收集时,会Stop The World。
优点是简单,对于单CPU,由于没有多线程的交互开销,可能更高效,是默认的Client模式下的新生代收集器。
通过-XX:+UseSerialGC开启,会使用Serial+Serial Old的收集器组合。
新生代采用Serial,是利用复制算法;老年代使用Serial Old采用标记-整理算法。
2、并行垃圾回收器
ParNew(并行)收集器:使用多线程进行垃圾回收,在垃圾收集时,会Stop The World。
在并发能力好的cpu环境里面,它停顿的时间要比串行收集器短,但对于单cpu或并发能力较弱的CPU,由于多线程的交互开销,可能比串行收集器更差。
是Server模式下首选的新生代收集器,且能和CMS收集器配合使用。
默认开启的收集线程数和cpu数量一样,运行数量可以通过修改-XX:ParallelGCThreads设定。用于新生代收集,复制算法。
使用-XX:+UseParNewGC,和Serial Old收集器组合进行内存回收。
使用CMS默认开启ParNew。
3、新生代Parallel Scavenge收集器
新生代Parallel Scavenge收集器:是一个用于新生代的、使用复制算法的、并行的收集器。
跟ParNew很类似,但更关注吞吐量,吞吐量优先,吞吐量=代码运行时间/(代码运行时间+垃圾收集时间),也就是高效率利用cpu时间,尽快完成程序的运算任务可以设置最大停顿时间MaxGCPauseMillis以及,吞吐量大小GCTimeRatio。
通过-XX:+UseParallelGC开启,Server模式下默认提供了其和SerialOld进行搭配的分代收集方式。
通过-XX:+UseParallelOldGC参数使用Parallel Scavenge + Parallel Old器组合进行内存回收。
-XX:MaxGCPauseMillis:设置GC的最大停顿时间。
-XX:GCTimeRatio:设置吞吐量大小。
-XX:+UseAdaptiveSizePolicy:设置了该参数,则随着GC,会动态调整新生代的大小,Eden,Survivor比例等,以提供最合适的停顿时间或者最大的吞吐量。
4、CMS收集器
CMS( Concurrent Mark-Sweep 并发标记清除)收集器是一种以获得最短回收停顿时间为目标的收集器。从名字就能直到其是给予标记-清除算法的。但是它比一般的标记-清除算法要复杂一些,分为以下4个阶段:
- 初始标记:标记一下GC Roots能直接关联到的对象,会“Stop The World”。
- 并发标记:GC Roots Tracing,可以和用户线程并发执行。
- 重新标记:标记期间产生的对象存活的再次判断,修正对这些对象的标记,执行时间相对并发标记短,会“Stop The World”。
- 并发清除:清除对象,可以和用户线程并发执行。
优点:低停顿,并发执行。
-
缺点:
- 1、并发执行,对CPU资源压力大。
- 2、无法处理在处理过程中产生的垃圾,可能导致FullGC。
- 3、由于它是基于标记-清除算法的,那么就无法避免空间碎片的产生。CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。
所谓浮动垃圾,在CMS并发清理阶段用户线程还在运行着,伴随程序运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只能留待下一次GC时再清理掉。
参数设置
- -XX:+UseConcMarkSweepGC:使用ParNew+CMS+Serial Old的收集器组合。Serial Old将作为CMS出错的后备收集器。
- -XX:CMSInitiatingOccupancyFraction:指定当年老代空间满了多少后进行垃圾回收,默认80%。
- -XX:+UseCMSCompactAtFullCollection:(默认是开启的)在CMS收集器顶不住要进行FullGC时开启内存碎片整理过程,该过程需要STW。
- -XX:CMSFullGCsBeforeCompaction:指定多少次FullGC后才进行整理。
- -XX:ParallelCMSThreads:指定CMS回收线程的数量,默认为:(CPU数量+3)/4。
G1收集器
把G1单独拿出来的原因是其比较复杂,在JDK 1.7确立是项目目标,在JDK 7u2版本之后发布,并在JDK 9中成为了默认的垃圾回收器。通过“-XX:+UseG1GC”启动参数即可指定使用G1 GC。
- G1从整体看还是基于标记-清除算法的,但是局部上是基于复制算法的。这样就意味者它空间整合做的比较好,因为不会产生空间碎片。
- G1还是并发与并行的,它能够充分利用多CPU、多核的硬件环境来缩短“stop the world”的时间。
- G1还是分代收集的,但是G1不再像上文所述的垃圾收集器,需要分代配合不同的垃圾收集器,因为G1中的垃圾收集区域是“分区”(Region)的。G1的分代收集和以上垃圾收集器不同的就是除了有年轻代的ygc,全堆扫描的full GC外,还有包含所有年轻代以及部分老年代Region的Mixed GC。
- G1还可预测停顿,通过调整参数,制定垃圾收集的最大停顿时间。
- G1跟踪各个Region里面垃圾堆的价值大小,在后台维护一个优先列表,每次根据允许的时间来回收价值最大的区域,从而保证在有限时间内的高效收集。
G1收集器的运作大致可以分为以下步骤:初始标记、并发标记、最终标记、筛选回收。
- 初始标记阶段:仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Set)的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这个阶段需要STW,但耗时很短。
- 并发标记阶段:从GC Roots开始对堆中对象进行可达性分析,找到存活的对象,这阶段耗时较长,但是可以和用户线程并发运行。
- 最终标记阶段:是为了修正在并发标记期间因用户程序继续运行而导致标记产生变化的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记需要把Remembered Set Logs的数据合并到Remembered Sets中,这阶段需要暂停线程,但是可并行执行。
- 筛选回收阶段:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来确定回收计划。
G1收集器运行示意图如下图所示。
G1分区的概念
G1的堆区在分代的基础上,引入分区的概念。G1将堆分成了若干Region,以下和”分区”代表同一概念。(这些分区不要求是连续的内存空间)Region的大小可以通过G1HeapRegionSize参数进行设置,其必须是2的幂,范围允许为1Mb到32Mb。 JVM的会基于堆内存的初始值和最大值的平均数计算分区的尺寸,平均的堆尺寸会分出约2000个Region。分区大小一旦设置,则启动之后不会再变化。如下图简单画了下G1分区模型。
Eden regions(年轻代-Eden区)
Survivor regions(年轻代-Survivor区)
Old regions(老年代)
Humongous regions(巨型对象区域)
Free regions(未分配区域,也会叫做可用分区)-上图中空白的区域
G1中的巨型对象是指,占用了Region容量的50%以上的一个对象。Humongous区,就专门用来存储巨型对象。如果一个H区装不下一个巨型对象,则会通过连续的若干H分区来存储。因为巨型对象的转移会影响GC效率,所以并发标记阶段发现巨型对象不再存活时,会将其直接回收。ygc也会在某些情况下对巨型对象进行回收。
分区可以有效利用内存空间,因为收集整体是使用“标记-整理”,Region之间基于“复制”算法,GC后会将存活对象复制到可用分区(未分配的分区),所以不会产生空间碎片。
G1参数设置:
- -XX:+UseG1GC:使用 G1 (Garbage First) 垃圾收集器
- -XX:MaxGCPauseMillis=n:设置最大GC停顿时间(GC pause time)指标(target),这是一个软性指标(soft goal),JVM 会尽量去达成这个目标。
- -XX:InitiatingHeapOccupancyPercent=n:堆空间占用了多少(百分比)的时候触发GC,默认值为 45。
- -XX:NewRatio=n:新生代与老生代(new/old generation)的大小比例(Ratio),默认值为 2。
- -XX:SurvivorRatio=n:eden/survivor 空间大小的比例(Ratio),默认值为 8。
- -XX:MaxTenuringThreshold=n:新生代到老年代的对象年龄,默认值为 15。
- -XX:ParallelGCThreads=n:设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同。
- -XX:ConcGCThreads=n:并发垃圾收集器使用的线程数量,默认值随JVM运行的平台不同而不同。
- -XX:G1ReservePercent=n:设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%。
- -XX:G1HeapRegionSize=n:设置G1区域的大小,值是2的次幂,范围是1MB到32MB,目标是根据最小的Java堆大小划分出约2048个区域。
G1 GC的分类和过程
JDK10 之前的G1中的GC只有Young GC,Mixed GC。Full GC处理会交给单线程的Serial Old垃圾收集器。
Young GC年轻代收集
在分配一般对象(非巨型对象)时,当所有Eden region使用达到最大阀值并且无法申请足够内存时,会触发一次Young GC。每次Young GC会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。到Old区的标准就是在PLAB中得到的计算结果。因为Young GC会进行根扫描,所以会stop the world。
Young GC的回收过程如下:
1、根扫描,跟CMS类似,Stop the world,扫描GC Roots对象。
2、处理Dirty card,更新RSet.
3、扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。
4、拷贝扫描出的存活的对象到survivor2/old区
5、处理引用队列,软引用,弱引用,虚引用
Mix GC混合收集
Mixed GC是G1 GC特有的,跟Full GC不同的是Mixed GC只回收部分老年代的Region。哪些old region能够放到CSet里面,有很多参数可以控制。比如G1HeapWastePercent参数,在一次young GC之后,可以允许的堆垃圾百占比,超过这个值就会触发mixed GC。
G1MixedGCLiveThresholdPercent参数控制的,old代分区中的存活对象比,达到阀值时,这个old分区会被放入CSet。
Mixed GC一般会发生在一次Young GC后面,为了提高效率,Mixed GC会复用Young GC的全局的根扫描结果,因为这个Stop the world过程是必须的,整体上来说缩短了暂停时间。
Mix GC的回收过程可以理解为Young GC后附加的全局concurrent marking,全局的并发标记主要用来处理old区(包含H区)的存活对象标记,过程如下:
- 初始标记(Initial Mark)。标记GC Roots,会STW,一般会复用Young GC的暂停时间。如前文所述,初始标记会设置好所有分区的NTAMS值。
- 根分区扫描(Root Region Scan)。这个阶段GC的线程可以和应用线程并发运行。其主要扫描初始标记以及之前Young GC对象转移到的Survivor分区,并标记Survivor区中引用的对象。所以此阶段的Survivor分区也叫根分区(Root Region)。
- 并发标记(Concurrent Mark)。会并发标记所有非完全空闲的分区的存活对象,也即使用了SATB算法,标记各个分区。
- 最终标记(Remark)。主要处理SATB缓冲区,以及并发标记阶段未标记到的漏网之鱼(存活对象),会STW,可以参考上文的SATB处理。
- 清除阶段(Clean UP)。上述SATB也提到了,会进行bitmap的swap,以及PTAMS,NTAMS互换。整理堆分区,调整相应的RSet(比如如果其中记录的Card中的对象都被回收,则这个卡片的也会从RSet中移除),如果识别到了完全空的分区,则会清理这个分区的RSet。这个过程会STW。
清除阶段之后,还会对存活对象进行转移(复制算法),转移到其他可用分区,所以当前的分区就变成了新的可用分区。复制转移主要是为了解决分区内的碎片问题。
Full GC
G1在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发Full GC。Full GC使用的是stop the world的单线程的Serial Old模式,所以一旦触发Full GC则会STW应用线程,并且执行效率很慢。JDK 8版本的G1是不提供Full GC的处理的。对于G1 GC的优化,很大的目标就是没有Full GC。
ZGC收集器
ZGC是一款在JDK11中新加入的具有实验性质的低延迟垃圾收集器,目前仅支持Linux/x86-64。
ZGC的设计目标是:支持TB级内存容量,暂停时间低(<10ms),对整个程序吞吐量的影响小于15%。
ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。
GC性能指标
- 吞吐量:应用花在非GC上的时间百分比,应用代码执行时间/运行的总时间。
- GC负荷:与吞吐量相反,指应用花在GC上的时间百分比,GC时间/运行的总时间。
- 暂停时间:应用花在GC stop-the-world的时间。
- GC频率:就是GC在一个时间段发生的次数。
- 反应速度:从一个对象变成垃圾到这个对象被回收的时间。
- 交互式的应用要求暂停时间越少越好。
GC触发条件
- Minor GC触发条件:Eden区满时
-
- Full GC触发条件:
- 调用System.gc时,系统建议执行Full GC,但是不必然执行
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
- 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
JVM内存容量配置原则
-
新生代尽可能设置大点,如果太小会导致:
- YGC次数更加频繁
- 可能导致YGC对象直接进入老年代,如果此时老年代满了,会触发FGC。
-
对老年代,针对响应时间有限应用:由于老年代通常采用并发收集器,因此其大小要综合考虑并发量和并发持续时间等参数。
- 如果设置小了,可能会造成内存碎片,高回收频率会导致应用暂停。
- 如果设置大了,会增加回收的时间。
对老年代,针对吞吐量优先的应用:通常设置较大的新生代和较小的老年代,这样可以尽可能回收大部分短期对象,减少中期对象,而老年代尽量存放长期存活的对象。
依据对象的存活周期进行分类,对象优先在新生代分配,长时间存活的的对象进入老年代。
根据不同代的特点,选取合适的收集算法:少量对象存活,适合复制算法;大量对象存活,适合标记清除或标记整理算法。
参考:
https://zhuanlan.zhihu.com/p/73628158
https://www.cnblogs.com/czwbig/p/11127159.html
https://segmentfault.com/a/1190000010463373
https://blog.csdn.net/iva_brother/article/details/87886525
http://blog.itpub.net/69906029/viewspace-2654005/
https://www.cnblogs.com/lfs2640666960/p/8522588.html
https://www.cnblogs.com/lsgxeva/p/10231201.html
https://www.cnblogs.com/blythe/p/7488061.html
https://www.cnblogs.com/jimoer/p/13170249.html
https://blog.51cto.com/u_11440114/5103211
https://blog.csdn.net/lovewangyihui/article/details/122442440