0
点赞
收藏
分享

微信扫一扫

JVM第二天 -(二)垃圾回收


1. 如何判断对象可以回收
2. 垃圾回收算法
3. 分代垃圾回收
4. 垃圾回收器
5. 垃圾回收调优

1. 如何判断对象可以回收

1.1 引用计数法

        对象有一个记录引用个数的计数,只要其他变量引用该对象,计数就加1。当变量不再引用它,计数就减1。计数为0,表明该对象不再被引用,可作为垃圾回收。

        但是该计数法有一个弊端,就是产生循环引用,如下图中,AB对象各自引用对方,引用计数都不为0,即使没人再引用他们俩,但是还是不能被回收,造成内存泄露。

JVM第二天 -(二)垃圾回收_后端

 1.2 可达性分析算法

        可达性分析算法首先要确定一系列根对象。

         ● Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
         ● 扫描堆中的对象,看是否能够沿着 GC Root 对象 为起点的引用链找到该对象,找不到,表示可以回收

        首先确定一系列根对象,所谓根对象,可以理解为那些肯定不能被垃圾回收的对象。在垃圾回收之前,首先会对堆中的所有对象进行一次扫描,查看某个对象是不是直接被根对象直接或间接引用,如果是,则不能回收,不是,则可以回收
         ● 哪些对象可以作为 GC Root ?

      1.2.1 通过MAT工具查看堆内存中可作为GC root的对象有哪些

        演示代码

//演示GC root
public static void main(String[] args) throws InterruptedException, IOException {
List<Object> list1 = new ArrayList<>();
list1.add("a");
list1.add("b");
System.out.println(1);
System.in.read();

list1 = null;
System.out.println(2);
System.in.read();
System.out.println("end...");
}

        

注意:

        注意区分引用变量和对象,如list1只是一个引用,他是存储在活动栈帧里的,他是一个局部变量,他后面引用的对象是存储在堆里的,根对象也是指堆中的对象,而不是引用变量

        先把堆内存的当前状态转储成一个文件,jmap -dump:format=b,live,file=1.bin $pid(b是抓取成二进制文件,live是指只抓取存活对象,被回收的对象不抓取,live在抓取之前会主动触发一次垃圾回收,file表示存的哪个文件)

1、list1置空之前:

JVM第二天 -(二)垃圾回收_软引用_02

(1)System Class  ,系统类,由启动类加载器加载的类,都是核心的类,一定不会被回收。

(2)Native Stack,包含操作系统引用的一些Java对象。Java虚拟机在进行方法调用时,必须执行一些操作系统的方法,而操作系统需要引用一些Java对象

(3)Busy Monitor,已被加锁的对象,也可作为根对象

(4)Thread,活动线程,其对象不可被回收,线程运行时都由一次次的方法调用组成,每次方法调用都会产生一个栈帧,栈帧内使用的东西都可以作为根对象。

 如活动线程执行过程中局部变量引用的对象都可以作为根对象,如ArrayList,方法参数args引用的字符串数组对象也是根对象。

JVM第二天 -(二)垃圾回收_开发语言_03

 2、list1置空之后:主线程内已无ArrayList,局部变量置为null,表示不再引用ArrayList对象。然后执行jmap -dump:format=b,live,file=2.bin 2024,触发GC,被GC垃圾回收掉,所以在根对象列表中无法找到他。

JVM第二天 -(二)垃圾回收_后端_04

1.3 四种引用

JVM第二天 -(二)垃圾回收_后端_05

        

 1. 强引用
        ● 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

        实际上平时用的所有的引用都是强引用,如通过=运算符把新建的对象赋值给一个变量,称这个变量强引用了这个对象。强引用的特点是:只要沿着GC Root的引用链能找到他,就不会被垃圾回收。B对象和C对象都强引用了A1对象,当GC Root对他的强引用都断开时,才可以被垃圾回收。称之为强引用。

2. 软引用(SoftReference)
        ● 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用
对象

        ● 可以配合引用队列来释放软引用自身

        只要对象没有被直接的强引用所引用,在垃圾回收时,都可以被回收掉。如A2对象有一个软引用引用他,被B对象直接强引用,如果B对象不再引用他,则可以在垃圾回收发生时,被垃圾回收掉。

       
3. 弱引用(WeakReference)
        ● 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
        ● 可以配合引用队列来释放弱引用自身

        软引用是相同前提条件下内存不足时才会再次触发垃圾回收,回收掉软引用对象。

        软、弱引用还可配合回收队列工作,当软引用的对象被回收掉(这里例如A2对象被回收掉),软引用本身是一个对象,他会进入引用队列,弱引用对象也是如此。他们自身也要占用内存,如果想要释放他俩,则需要使用引用队列找到他俩,依次遍历释放。

JVM第二天 -(二)垃圾回收_java_06


4. 虚引用(PhantomReference)

        ● 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象(这里指ByteBuffer)回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法(Unsafe.freeMemory())释放ByteBuffer申请的直接内存 

JVM第二天 -(二)垃圾回收_软引用_07

 

JVM第二天 -(二)垃圾回收_软引用_08

5. 终结器引用(FinalReference)
        ● 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象
暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize
方法,第二次 GC 时才能回收被引用对象

 

JVM第二天 -(二)垃圾回收_软引用_09

 1.4 软引用应用

        软引用通常使用在什么场景,来看这样一个案例。

       案例代码

package cn.itcast.jvm.t2;

import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;

/**
* 演示软引用 后两个参数表示打印垃圾回收的详细参数
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_3 {

private static final int _4MB = 4 * 1024 * 1024;



public static void main(String[] args) throws IOException {
/*List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(new byte[_4MB]);
}

System.in.read();*/
soft();


}

public static void soft() {
// list --> SoftReference --> byte[]

List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());

}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}
}

      

JVM第二天 -(二)垃圾回收_开发语言_10

       存在的问题:

       执行main方法,由于初始设置堆内存为20MB,list添加了5个4MB的数组,报堆内存溢出错误。若Byte数组存储的是非关键数据,又被list 强引用,如果堆内存不足,此时就无法回收该Byte数组。

        解决方法:使用弱引用或者软引用

        list先引用软引用对象,再用软引用对象引用byte数组。list强引用SoftReference对象,SoftReference软引用byte数组。

public static void soft() {
// list --> SoftReference --> byte[]

List<SoftReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());

}
System.out.println("循环结束:" + list.size());
for (SoftReference<byte[]> ref : list) {
System.out.println(ref.get());
}
}

        运行结果:5个数组对象成功添加到ist中,循环结束后,前四个对象被回收,只剩下最后一个对象。

JVM第二天 -(二)垃圾回收_后端_11

堆内存占用情况

        只剩余最后一个byte数组,其他都被回收掉了。

JVM第二天 -(二)垃圾回收_开发语言_12

         总结

        软引用在内存空间敏感下的一个好处:不重要的对象使用软引用关联,在堆内存空间紧张时,就可将其回收。

 1.5 软引用_引用队列

        上文中,list里的一些软引用对象已经是null了,没必要保存在list集合当中,配合引用队列把软引用对象本身清理掉,

        示例代码

        

package cn.itcast.jvm.t2;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;

/**
* 演示软引用, 配合引用队列
*/
public class Demo2_4 {
private static final int _4MB = 4 * 1024 * 1024;

public static void main(String[] args) {
List<SoftReference<byte[]>> list = new ArrayList<>();

// 引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

for (int i = 0; i < 5; i++) {
// 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
System.out.println(ref.get());
list.add(ref);
System.out.println(list.size());
}

// 从队列中获取无用的 软引用对象,并移除
Reference<? extends byte[]> poll = queue.poll();
while( poll != null) {
list.remove(poll);
poll = queue.poll();
}

System.out.println("===========================");
for (SoftReference<byte[]> reference : list) {
System.out.println(reference.get());
}

}
}

1.6 弱引用应用

        弱引用通常使用在什么场景,来看这样一个案例。

        list先引用弱引用对象,再用弱引用对象引用byte数组。list强引用WeakReference对象,WeakReference弱引用byte数组。

/**
* 演示弱引用
* -Xmx20m -XX:+PrintGCDetails -verbose:gc
*/
public class Demo2_5 {
private static final int _4MB = 4 * 1024 * 1024;

public static void main(String[] args) {
// list --> WeakReference --> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
for (WeakReference<byte[]> w : list) {
System.out.print(w.get()+" ");
}
System.out.println();

}
System.out.println("循环结束:" + list.size());
}
}

运行结果

        循环5次

JVM第二天 -(二)垃圾回收_后端_13

         循环10次

JVM第二天 -(二)垃圾回收_jvm.gc_14

 总结:

        和弱饮用使用方法类似,参照弱引用。

2. 垃圾回收算法

2.1 标记清除算法

        定义: Mark Sweep

        阶段一:先标记,看哪些对象可以被垃圾回收。

        阶段二:清除,释放对象占用的空间。释放不是把占用的整个内存进行清零的操作,而是记录对象的起始和结束地址,放入空闲地址链表里就可以了,下次分配新对象,就从空闲链表中找,看有没有一块足够的空间容纳新对象,如果有,就进行内存分配。并不会把占用的内存进行清零的操作。

        优点:
                ● 速度较快

        缺点:

                ● 容易产生内存碎片,空闲空间不连续。

JVM第二天 -(二)垃圾回收_后端_15

2.2 标记整理

 定义:Mark Compact

                缺点:
                ● 速度慢。对象在整理过程中需要移动,一移动,如果有引用引用了这些对象,则需要改变引用的地址。

        阶段一:标记可被回收的对象

        阶段二:内存被回收后,多加了整理这一个步骤。即合并空闲内存区域。减少了内存碎片。

JVM第二天 -(二)垃圾回收_开发语言_16

 2.3 复制

        把内存区域划成了大小相等的两块区域

JVM第二天 -(二)垃圾回收_软引用_17

        阶段一:先标记,再把FROM区域存活的对象复制到TO区域,然后就完成了区域的整理。复制完成,FROM区域全是无用的对象,直接全部被回收。并且交换FROM和TO区域。

定义:Copy

        优缺点:
        ● 不会有内存碎片
        ● 需要占用双倍内存空间

3. 分代垃圾回收

     

分代思想

       实际上,虚拟机不会使用单独的垃圾回收算法。而是多个算法协同工作,具体的实现即是jvm里的分代的垃圾回收机制。

分代介绍

Java8 时,堆被分为了两份:新生代和老年代(1:2),在 Java7 时,还存在一个永久代

  • 新生代使用:复制算法
  • 老年代使用:标记 - 清除 或者 标记 - 整理 算法

Minor GC 和 Full GC:

  • Minor GC:回收新生代,新生代对象存活时间很短,所以 Minor GC 会频繁执行,执行的速度比较快
  • Full GC:回收老年代和新生代,老年代对象其存活时间长,所以 Full GC 很少执行,执行速度会比 Minor GC 慢很多

Eden 和 Survivor 大小比例默认为 8:1:1

JVM第二天 -(二)垃圾回收_java_18

分代分配

工作机制:

  • 对象优先在 Eden 分配:当创建一个对象的时候,对象会被分配在新生代的 Eden 区,当Eden 区要满了时候,触发 YoungGC
  • 当进行 YoungGC 后,此时在 Eden 区存活的对象被移动到 to 区,并且当前对象的年龄会加 1,清空 Eden 区
  • 当再一次触发 YoungGC 的时候,会把 Eden 区中存活下来的对象和 to 中存活的对象,移动到 from 区中,这些对象的年龄会加 1,清空 Eden 区和 to 区
  • To 区永远是空 Survivor 区,From 区是有数据的,每次 MinorGC 后两个区域互换
  • From 区和 To 区 也可以叫做 S0 区和 S1 区
  • 新生代的垃圾回收叫MinorGC或YoungGC

晋升到老年代:

  • 长期存活的对象进入老年代:为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中
    ​​​-XX:MaxTenuringThreshold​​:定义年龄的阈值,对象头中用 4 个 bit 存储,所以最大值是 15,默认也是 15
  • 大对象直接进入老年代:需要连续内存空间的对象,最典型的大对象是很长的字符串以及数组;避免在 Eden 和 Survivor 之间的大量复制;经常出现大对象会提前触发 GC 以获取足够的连续空间分配给大对象
    ​​​-XX:PretenureSizeThreshold​​:大于此值的对象直接在老年代分配
  • 动态对象年龄判定:如果在 Survivor 区中相同年龄的对象的所有大小之和超过 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代

空间分配担保:

  • 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的
  • 如果不成立,虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于将尝试着进行一次 Minor GC;如果小于或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

总结:

  • 对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是 15(4bit)(但也并不是一定要等到15岁才晋升)
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长

相关VM参数

JVM第二天 -(二)垃圾回收_软引用_19

案例分析垃圾回收过程

1.没有任何对象时运行:

/** * 演示内存的分配策略 */ public class test { // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc public static void main(String[] args) { } }

JVM第二天 -(二)垃圾回收_java_20


2.新建一个ArrayList对象引用7MB对象

/** * 演示内存的分配策略 */ (1)先放7MB public class test { private static final int _512KB = 512 * 1024; private static final int _1MB = 1024 * 1024; private static final int _6MB = 6 * 1024 * 1024; private static final int _7MB = 7 * 1024 * 1024; private static final int _8MB = 8 * 1024 * 1024; // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC public static void main(String[] args) { ArrayList<byte[]> list = new ArrayList<>(); list.add(new byte[_7MB]); } }

JVM第二天 -(二)垃圾回收_开发语言_21

 (2)再放512KB

list.add(new byte[_512KB]);

JVM第二天 -(二)垃圾回收_jvm.gc_22

(3)再放512KB

JVM第二天 -(二)垃圾回收_软引用_23


3.大对象直接晋升到老年代

(1)放一个8MB对象 /** * 演示内存的分配策略 */ public class test { private static final int _512KB = 512 * 1024; private static final int _1MB = 1024 * 1024; private static final int _6MB = 6 * 1024 * 1024; private static final int _7MB = 7 * 1024 * 1024; private static final int _8MB = 8 * 1024 * 1024; // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC public static void main(String[] args) { ArrayList<byte[]> list = new ArrayList<>(); list.add(new byte[_8MB]); // list.add(new byte[_512KB]); // list.add(new byte[_512KB]); } }

大对象在老年代空间足够,但新生代内存肯定不足的情况下,大对象直接晋升到老年代。

JVM第二天 -(二)垃圾回收_java_24

 (2)再放一个8MB对象

                由于新生代和老年代都放不下16MB对象,而且,这两个对象被ArrayList引用,无法被回收,所以造成outOfMemory

JVM第二天 -(二)垃圾回收_开发语言_25

 (3)当内存溢出发生在子线程中,他的内存溢出是否会导致整个主线程的结束?

public static void main(String[] args) throws InterruptedException { new Thread(() -> { ArrayList<byte[]> list = new ArrayList<>(); list.add(new byte[_8MB]); list.add(new byte[_8MB]); }).start(); System.out.println("sleep...."); Thread.sleep(1000L); }

结论:不会,主线程并没有意外结束,一个线程内的outOfMemory不会导致整个线程的结束。

4. 垃圾回收器

分类

1. 串行
○ 单线程:在垃圾回收发生时,把其他线程都暂停,这时,一个单线程的垃圾回收器出现。
○ 堆内存较小,适合个人电脑

2. 吞吐量优先
○ 多线程
○ 堆内存较大,需要多核 cpu支持
○ 让单位时间内, STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高

3. 响应时间优先
○ 多线程
○ 堆内存较大,需要多核 cpu支持
○ 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5

4.1 串行

JVM第二天 -(二)垃圾回收_java_26

 4.2 吞吐量优先

- XX:+UseParallelGC ~ - XX:+UseParallelOldGC   (Parallel是并行的意思)

新生代的垃圾回收器 复制                                  标记+整理


- XX:+UseAdaptiveSizePolicy     

采用自适应的大小调整策略(调整新生代的大小,打开这个开关后,ParallelGC会动态地调整新生代的比例)


- XX:GCTimeRatio=ratio   

调整垃圾回收时间和总时间的一个占比,占比计算公式1/(1+radio),分子是垃圾回收时间。一旦不满足要求,则会增大堆的大小,即增大ratio,一般ratio设置为19,100分内,允许5分钟的垃圾回收时间。


- XX:MaxGCPauseMillis=ms

最大垃圾回收暂停时间


- XX:ParallelGCThreads=n         控制GC运行时的线程数

JVM第二天 -(二)垃圾回收_软引用_27

4.3 响应时间优先

4.4 G1

G1特点

G1(Garbage-First)是一款面向服务端应用的垃圾收集器,应用于新生代和老年代、采用标记-整理算法、软实时、低延迟、可设定目标(最大STW停顿时间)的垃圾回收器,用于代替 CMS,适用于较大的堆(>4 ~ 6G),在 JDK9 之后默认使用 G1

G1 对比其他处理器的优点:

  • 并发与并行:
  • 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW
  • 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此不会在整个回收阶段发生完全阻塞应用程序的情况
  • 其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作,而 G1 GC 可以采用应用线程承担后台运行的 GC 工作,JVM 的 GC 线程处理速度慢时,系统会调用应用程序线程加速垃圾回收过程
  • 分区算法
  • 从分代上看,G1 属于分代型垃圾回收器,区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区。从堆结构上看,新生代和老年代不再物理隔离,不用担心每个代内存是否足够,这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC
  • 将整个堆划分成约 2048 个大小相同的独立 Region 块,每个 Region 块大小根据堆空间的实际大小而定,整体被控制在 1MB 到 32 MB之间且为 2 的 N 次幂,所有 Region 大小相同,在 JVM 生命周期内不会被改变。G1 把堆划分成多个大小相等的独立区域,使得每个小空间可以单独进行垃圾回收
  • 新的区域 Humongous:本身属于老年代区,当出现了一个巨型对象超出了分区容量的一半,该对象就会进入到该区域。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储,为了能找到连续的H区,有时候不得不启动 Full GC
  • G1 不会对巨型对象进行拷贝,回收时被优先考虑,G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为 0 的巨型对象就可以在新生代垃圾回收时处理掉
  • Region 结构图:

JVM第二天 -(二)垃圾回收_后端_30

  • 空间整合:
  • CMS:标记-清除算法、内存碎片、若干次 GC 后进行一次碎片整理
  • G1:整体来看是基于标记 - 整理算法实现的收集器,从局部(Region 之间)上来看是基于复制算法实现的,两种算法都可以避免内存碎片
  • 可预测的停顿时间模型(软实时 soft real-time):可以指定在 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒
  • 由于分块的原因,G1 可以只选取部分区域进行内存回收,这样缩小了回收的范围,对于全局停顿情况也能得到较好的控制
  • G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间,通过过去回收的经验获得),在后台维护一个优先列表,每次根据允许的收集时间优先回收价值最大的 Region,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率
  • 相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多

G1垃圾收集器的缺点:

  • 相较于 CMS,G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1 无论是为了垃圾收集产生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高
  • 从经验上来说,在小内存应用上 CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势。平衡点在 6-8GB 之间

应用场景:

  • 面向服务端应用,针对具有大内存、多处理器的机器
  • 需要低 GC 延迟,并具有大堆的应用程序提供解决方案

记忆集

记忆集 Remembered Set 在新生代中,每个 Region 都有一个 Remembered Set,用来被哪些其他 Region 里的对象引用(谁引用了我就记录谁)

JVM第二天 -(二)垃圾回收_软引用_31

  • 程序对 Reference 类型数据写操作时,产生一个 Write Barrier 暂时中断操作,检查该对象和 Reference 类型数据是否在不同的 Region(跨代引用),不同就将相关引用信息记录到 Reference 类型所属的 Region 的 Remembered Set 之中
  • 进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏

垃圾收集器在新生代中建立了记忆集这样的数据结构,可以理解为它是一个抽象类,具体实现记忆集的三种方式:

  • 字长精度
  • 对象精度
  • 卡精度(卡表)

卡表(Card Table)在老年代中,是一种对记忆集的具体实现,主要定义了记忆集的记录精度、与堆内存的映射关系等,卡表中的每一个元素都对应着一块特定大小的内存块,这个内存块称之为卡页(card page),当存在跨代引用时,会将卡页标记为 dirty,JVM 对于卡页的维护也是通过写屏障的方式

收集集合 CSet 代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中

  • CSet of Young Collection
  • CSet of Mix Collection

工作原理

G1 中提供了三种垃圾回收模式:YoungGC、Mixed GC 和 FullGC,在不同的条件下被触发

  • 当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程
  • 标记完成马上开始混合回收过程

JVM第二天 -(二)垃圾回收_后端_32

顺时针:Young GC → Young GC + Concurrent Mark → Mixed GC 顺序,进行垃圾回收

  • Young GC:发生在年轻代的 GC 算法,一般对象(除了巨型对象)都是在 eden region 中分配内存,当所有 eden region 被耗尽无法申请内存时,就会触发一次 young gc,G1 停止应用程序的执行 STW,把活跃对象放入老年代,垃圾对象回收
    回收过程
  1. 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口
  2. 更新 RSet:处理 dirty card queue 更新 RS,此后 RSet 准确的反映对象的引用关系
  • dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet
  • 作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好
  1. 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 Young CSet 中进行回收
  2. 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间
  3. 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作
  • Concurrent Mark
  • 初始标记:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC
  • 根区域扫描 (Root Region Scanning):扫描 Survivor 区中指向老年代的,被初始标记标记了的引用及引用的对象,这一个过程是并发进行的,但是必须在 Young GC 之前完成
  • 并发标记 (Concurrent Marking):在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中
  • 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(防止漏标
  • 筛选回收:并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率
  • Mixed GC:当很多对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,除了回收整个 young region,还会回收一部分的 old region,过程同 YGC
  • 注意:是一部分老年代,而不是全部老年代,可以选择哪些老年代 region 收集,对垃圾回收的时间进行控制在 G1 中,Mixed GC 可以通过​​-XX:InitiatingHeapOccupancyPercent​​ 设置阈值
  • Full GC:对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的垃圾回收,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC
    产生 Full GC 的原因:
  • 晋升时没有足够的空间存放晋升的对象
  • 并发处理过程完成之前空间耗尽,浮动垃圾

相关参数

  • ​-XX:+UseG1GC​​:手动指定使用 G1 垃圾收集器执行内存回收任务
  • ​-XX:G1HeapRegionSize​​:设置每个 Region 的大小。值是 2 的幂,范围是 1MB 到 32MB 之间,目标是根据最小的 Java 堆大小划分出约 2048 个区域,默认是堆内存的 1/2000
  • ​-XX:MaxGCPauseMillis​​:设置期望达到的最大 GC 停顿时间指标,JVM会尽力实现,但不保证达到,默认值是 200ms
  • ​-XX:+ParallelGcThread​​:设置 STW 时 GC 线程数的值,最多设置为 8
  • ​-XX:ConcGCThreads​​:设置并发标记线程数,设置为并行垃圾回收线程数 ParallelGcThreads 的1/4左右
  • ​-XX:InitiatingHeapoccupancyPercent​​:设置触发并发 Mixed GC 周期的 Java 堆占用率阈值,超过此值,就触发 GC,默认值是 45
  • ​-XX:+ClassUnloadingWithConcurrentMark​​:并发标记类卸载,默认启用,所有对象都经过并发标记后,就可以知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
  • ​-XX:G1NewSizePercent​​:新生代占用整个堆内存的最小百分比(默认5%)
  • ​-XX:G1MaxNewSizePercent​​:新生代占用整个堆内存的最大百分比(默认60%)
  • ​-XX:G1ReservePercent=10​​:保留内存区域,防止 to space(Survivor中的 to 区)溢出

JDK 8u20 字符串去重

优点:节省大量内存 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加

  • XX:+UseStringDeduplication
  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时, G1并发检查是否有字符串重复
  • 如果它们值一样,让它们引用同一个 char[]
    注意,与 String.intern() 不一样
  • String.intern() 关注的是字符串对象
  • 而字符串去重关注的是 char[]
  • 在 JVM 内部,使用了不同的字符串表

JDK 8u40 并发标记类卸载

        所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类 - XX:+ClassUnloadingWithConcurrentMark 默认启用。

 JDK 8u60 回收巨型对象

  • 一个对象大于 region 的一半时,称之为巨型对象
  • G1 不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生
    代垃圾回收时处理掉

 JDK 9 并发标记起始时间的调整

并发标记必须在堆空间占满前完成,否则退化为 FullGC

JDK 9 之前需要使用 - XX:InitiatingHeapOccupancyPercent

JDK 9 可以动态调整

  • XX:InitiatingHeapOccupancyPercent 用来设置初始值
  • 进行数据采样并动态调整
  • 总会添加一个安全的空档空间

 JDK 9 更高效的回收

250+ 增强
180+bug 修复
​​​ https://docs.oracle.com/en/java/javase/12/gctuning​​

5. 垃圾回收调优

G1 的设计原则就是简化 JVM 性能调优,只需要简单的三步即可完成调优:

  1. 开启 G1 垃圾收集器
  2. 设置堆的最大内存
  3. 设置最大的停顿时间(STW)

不断调优暂停时间指标:

  • ​XX:MaxGCPauseMillis=x​​ 可以设置启动应用程序暂停的时间,G1会根据这个参数选择 CSet 来满足响应时间的设置
  • 设置到 100ms 或者 200ms 都可以(不同情况下会不一样),但设置成50ms就不太合理
  • 暂停时间设置的太短,就会导致出现 G1 跟不上垃圾产生的速度,最终退化成 Full GC
  • 对这个参数的调优是一个持续的过程,逐步调整到最佳状态

不要设置新生代和老年代的大小:

  • 避免使用 -Xmn 或 -XX:NewRatio 等相关选项显式设置年轻代大小,G1 收集器在运行的时候会调整新生代和老年代的大小,从而达到我们为收集器设置的暂停时间目标
  • 设置了新生代大小相当于放弃了 G1 为我们做的自动调优,我们需要做的只是设置整个堆内存的大小,剩下的交给 G1 自己去分配各个代的大小
举报

相关推荐

RHCSA第二天

CSS第二天

html第二天

出差第二天

MySQL第二天

集合第二天

java第二天

DOM第二天

JavaSE 第二天

【JavaSE 第二天】

0 条评论