咸鱼

咸鱼是以盐腌渍后,晒干的鱼

0%

黑马程序员JVM学习笔记

本文是【黑马程序员JVM完整教程,全网超高评价,全程干货不拖沓-哔哩哔哩】教学视频学习所做的笔记

部分笔记内容拷贝自【JVM学习】

一、什么是JVM

定义

Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境)

好处

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收机制
  • 数组下标越界检查

比较

JVM JRE JDK的区别

常见JVM

二、内存结构

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • 方法区

1、程序计数器

作用:用于保存JVM中下一条所要执行的指令的地址

特点:

  • 线程私有
    • CPU会为每个线程分配时间片,当当前线程的时间片使用完以后,CPU就会去执行另一个线程中的代码
    • 程序计数器是每个线程私有的,当另一个线程的时间片用完,又返回来执行当前线程的代码时,通过程序计数器可以知道应该执行哪一行指令
  • 不会存在内存溢出

2、虚拟机栈

虚拟机栈

2.1 定义

Java Virtual Machine Stacks (Java虚拟机栈)

  • 每个线程运行时所需要的内存空间,称为虚拟机栈
  • 每个栈由多个**栈帧(Frame)**组成,对应着每次调用方法时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

演示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
method1();
}

private static void method1() {
method2(1, 2);
}

private static int method2(int a, int b) {
int c = a + b;
return c;
}
}

问题辨析:

  • 垃圾回收是否涉及栈内存?
    • 不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。
  • 栈内存的分配越大越好吗?
    • 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
    • -Xss size 指定栈内存大小,一般无需指定,使用默认的即可。
  • 方法内的局部变量是否是线程安全的?
    • 变量是否线程安全,要看此变量是线程共享的,还是私有的?
    • 如果方法内局部变量没有逃离方法的作用范围,则是线程安全
    • 如果如果局部变量引用了对象,并逃离了方法的作用范围,则需要考虑线程安全问题
    • 线程安全演示

2.2 内存溢出

栈内存溢出错误: Java.lang.stackOverflowError

发生原因:

  • 虚拟机栈中,栈帧过多(无限递归)
  • 虚拟机栈中,栈帧过大 (不容易出现)

2.3 线程运行诊断

案例1:CPU占用过多

  • 演示代码:while(true){...} nohup 运行

  • top 查看进程 PID 占用情况

  • ps H -eo pid, tid, %cpu | grep 具体PID 查看某进程(pid)中的线程(tid)占用CPU的详细情况

  • jstack PID 查看进程所有线程的nid (16进制),tid是10进制值,转换nid或tid比对,可以定位到线程的哪行代码出了问题。

案例2:程序运行很久都没有结果

  • 演示代码:死锁
  • jstack PID 查看线程运行情况,能看到死锁信息

3、本地方法栈

本地方法栈

一些带有 native 关键字的方法,Java调用C/C++方法。

本地方法栈 的作用就是为 本地方法 的运行提供内存空间。

4、堆(Heap)

堆

前面1.2.3点的 “栈” 都是线程 “私有” 的区,而 “堆” 和 “方法区” 是线程共享的区。

定义:通过new关键字创建的对象都会被放在内存

特点

  • 所有线程共享,堆内存中的对象都需要考虑线程安全问题
  • 有垃圾回收机制

4.1 堆内存溢出

堆内存虽然有垃圾回收,但如果不断产生新的对象,而产生的对象一直有引用,那么无法垃圾回收,堆内存将会耗尽,也就是堆内存溢出。

-Xmx Size 参数指定堆内存大小

堆内存溢出演示代码

堆内存溢出异常

4.2 堆内存诊断

  1. jps 工具

    命令行查看当前系统中Java进程

  2. jmap 工具

    命令行查看某时刻堆内存占用情况

    OracleJDK运行没问题,但OpenJDK可能无法直接运行:

    $ jmap -heap 37420
    Error: -heap option used
    Cannot connect to core dump or remote debug server. Use jhsdb jmap instead

  3. jconsole 工具

    图形界面多功能监控工具,可以连续监测

  4. jvisualvm 工具

    图形界面多功能监控工具,可以连续监测,可以对堆内存抓取快照,以下是示例:

5、方法区


方法区的定义摘自【JVM 规范】 翻译 :

  • Java虚拟机中有一个被所有线程共享方法区。它存储着每个类的结构(structures)信息,譬如运行时的常量池(run-time constant pool),字段(field),方法数据(medthod data),以及方法和构造方法的代码,包括一些在类和实例初始化和接口初始化时候使用的特殊方法。
  • 方法区JVM启动时候被创建。虽然方法区在逻辑层面上是堆的一部分(具体由厂商实现,如IBM,Oracle等),但是就简单实现来说既不会被回收也不会被压缩。这个规范并不强制指定方法区存放的位置也不会对编译过的代码有管理策略的限制(oraclejdk 1.8和1.7及之前实现就不一样)。
  • 方法区可能有一个固定的大小或者也可以通过计算大小去扩展也可以在不需要的时候被压缩。
  • 方法区的内存也不需要是连续的。
  • Jvm虚拟机实现可以提供给编程人员或者用户初始化方法区的大小,同时在方法区可变大小的情况下,控制这个方法区的最大值和最小值。
  • 和方法区有关联的异常情况:如果方法区满足不了构造所需要的内存,JVM就会抛出内存溢出错误:OutOfMemoryError

JVM内存结构示意图:
JVM内存结构

5.1 内存溢出

  • 1.8以前会导致**永久代(PermGen)**内存溢出
    参数-XX:MaxPermSize=8m 可以测试导致错误: java.lang.OutOfMemoryError: PermGen space
  • 1.8以后会导致**元空间(Metaspace)**内存溢出
    参数-XX:MaxMetaspaceSize=8m 可以测试导致错误: java.lang.OutOfMemoryError: Metaspace

加载过多的class会导致内存溢出,实际项目中容易出现该错误的常见场景有:spring 、mybatis等大型框架。

5.2 常量池

二进制字节码的组成:类的基本信息、常量池、类的方法定义(包含了虚拟机指令)

常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息

5.3 运行时常量池

常量池是在 *.class 文件中的,当该class类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实内存地址

5.4 串池(StringTable)

StringTable是字符串常量池(简称串池),其数据结构上是一个 hash 表,字符串对象就充当 hash 表中的 key,key 的不重复性,是 hash 表的基本特性。它是jdk用来解决以new的方式创建字符串对象的一个缺点:堆内存中会存在大量重复的字符串,占用内存。

5.4.1 StringTable的特性:
  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder
  • 字符串常量拼接的原理是编译器优化
  • 可以使用String.intern()方法,主动将串池中还没有的字符串对象放入串池中
  • 注意:无论是串池还是里面的字符串,都是对象

注意:示例运行的环境:

openjdk version “1.8.0_141”
OpenJDK Runtime Environment (build 1.8.0_141-8u141-b15-3~14.04-b15)
OpenJDK 64-Bit Server VM (build 25.141-b15, mixed mode)

StringTable示例1:
1
2
3
4
5
6
7
public class StringTableStudy {
public static void main(String[] args) {
String a = "a";
String b = "b";
String ab = "ab";
}
}

javap -v StringTableStudy.class反编译结果

1
2
3
4
5
6
7
8
9
Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
  • 当执行到 ldc #2 时,会把符号 a 变为 “a” 字符串对象,并放入串池中
  • 当执行到 ldc #3 时,会把符号 b 变为 “b” 字符串对象,并放入串池中
  • 当执行到 ldc #4 时,会把符号 ab 变为 “ab” 字符串对象,并放入串池中
  • 最终串池 StringTable [“a”, “b”, “ab”]

总结:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。

StringTable示例2:
1
2
3
4
5
6
7
8
9
public class StringTableStudy {
public static void main(String[] args) {
String a = "a";
String b = "b";
String ab = "ab";
//拼接字符串对象来创建新的字符串
String ab2 = a+b; //分析此行
}
}

javap -v StringTableStudy.class反编译结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Code:
stack=2, locals=5, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: return

只分析最后一行代码

  • 9: new #5 new一个StringBuilder对象
  • 13: invokespecial #6 执行StringBuilder的构造方法
  • 16: aload_1加载 参数astore_1
  • 17: invokevirtual #7 调用append方法
  • 20: aload_2 加载 参数astore_2
  • 21: invokevirtual #7 调用append方法
  • 24: invokevirtual #8 toString()方法,new一个在堆内存的对象
  • 27: astore 4 把toString()的对象存储

String ab2 = a+b; 创建字符串的过程是 StringBuilder().append(“a”).append(“b”).toString()

再看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StringTableStudy {
public static void main(String[] args) {
String a = "a";
String b = "b";

//存放在串池中
String ab = "ab";

//存放在堆内存
//StringBuilder().append(“a”).append(“b”).toString()
String ab2 = a+b;

//结果是false,因为两者的存放位置不同
System.out.println(ab == ab2);
}
}
StringTable示例3:
1
2
3
4
5
6
7
8
9
public class StringTableStudy {
public static void main(String[] args) {
String a = "a";
String b = "b";
String ab = "ab";
String ab2 = a+b;
String ab3 = "a" + "b"; //分析此行
}
}

javap -v StringTableStudy.class反编译结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Code:
stack=2, locals=6, args_size=1
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: ldc #4 // String ab
31: astore 5
33: return

直接看 29: ldc :javac在编译期会进行优化,结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了字符串“ab”,所以ab3直接从串池中获取值,所以进行的操作和 ab = “ab” 一致。

5.4.2 intern方法

调用字符串对象的intern方法,主动将串池中还没有的字符串对象放入到串池。

intern示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) {
//"a" "b" 被放入串池中,s则存在于堆内存之中
String s = new String("a") + new String("b");

//尝试将堆中s对象放入串池,
//串池中如果已存在此字符串并不会放入;
//串池中如果不存在此字符串则会放入串池,并把串池中的对象返回;
//此时串池中没有"ab",则s会放入到串池,并返回给s2
String s2 = s.intern();

//s3赋值,因为此时串池中已有"ab",则直接将串池中的内容返回
String s3 = "ab";
//因为堆内存与串池中的"ab"是同一个对象,所以以下两条语句打印的都为true
System.out.println(str == st2);
System.out.println(str == str3);
}
}

intern示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Main {
public static void main(String[] args) {

//"ab"被放入串池中
String x = "ab";
//"a" "b" 被放入串池中,s则存在于堆内存之中
String s = new String("a") + new String("b");

//尝试将堆中s对象放入串池,
//串池中已有"ab",则s不会放入到串池(s依然是堆中的对象),并返回串池中的"ab"给s2
String s2 = s.intern();

System.out.println(s2 == x);//true
System.out.println(s == x); //false,堆 != 串池
}
}

intern在JDK1.8和1.6上的区别:

  • 1.8

    将字符串对象尝试放入串池,如已存在则不放入,会把串池中的对象返回;如不存在则把对象放入串池,会把串池中的对象返回

  • 1.6

    将字符串对象尝试放入串池,如已存在则不放入,会把串池中的对象返回;如不存在则把对象复制一份放入串池,会把串池中的对象返回

5.4.3 StringTable的位置

  • 1.8 在堆内存中 (回收效率高)
  • 1.6 在永久代PermGen (触发垃圾回收时间晚,回收效率不高)
5.4.4 StringTable垃圾回收

​ StringTable在内存紧张时,会发生垃圾回收

5.4.5 StringTable调优
  • 因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间

    1
    -XX:StringTableSize=xxxx
  • 考虑是否需要将字符串对象入池

    可以通过intern方法减少重复入池

6、直接内存

  • 常见于NIO操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理

JVM常规文件IO流程(要调用“操作系统方法”操作IO)

JVM直接内存文件IO流程(直接操作IO)

直接内存是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率。

直接内存溢出:java.lang.OutOfMemoryError:Direct buffer memory

直接内存分配

1
2
3
static int _1M = 1024 * 1024
//通过ByteBuffer申请1M的直接内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);

直接内存释放

JVM并不能回收直接内存中的内容,它是如何实现回收的呢?看分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.IOException;
import java.nio.ByteBuffer;

//直接内存示例
//Windows上要通过任务管理器查看内存情况
public class TestDemo {

static int _1G = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1G);
System.out.println("分配直接内存完毕");
System.in.read();
System.out.println("开始释放直接内存");
byteBuffer = null;
System.gc();//显示垃圾回收,Full GC
System.in.read();
}
}

ByteBuffer.allocateDirect() 这个方法的实现是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

/**
* Allocates a new direct byte buffer.
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, each of its elements will be
* initialized to zero, and its byte order will be
* {@link ByteOrder#BIG_ENDIAN BIG_ENDIAN}. Whether or not it has a
* {@link #hasArray backing array} is unspecified.
*
* @param capacity
* The new buffer's capacity, in bytes
*
* @return The new byte buffer
*
* @throws IllegalArgumentException
* If the {@code capacity} is a negative integer
*/
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}

DirectByteBuffer 类构造函数实现是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Primary constructor
//
DirectByteBuffer(int cap) { // package-private

super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);

long base = 0;
try {
//申请内存
base = UNSAFE.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
UNSAFE.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
//Cleaner是虚引用类型 PhantomReference<Object>
//特点:当所关联的对象被回收,Cleaner会触发clean()方法
//这里关联的是对象this就是DirectByteBuffer,当DirectByteBuffer被回收触发Cleaner.clean()
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//Deallocator回调任务
private static class Deallocator
implements Runnable
{

private long address;
private long size;
private int capacity;

private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}

public void run() {
if (address == 0) {
// Paranoia
return;
}
//释放直接内存
UNSAFE.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}

}

Cleaner.clean()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Runs this cleaner, if it has not been run before.
*/
public void clean() {
if (!remove(this))
return;
try {
//调用run方法,也就是上面的回调任务Deallocator.run()方法
thunk.run();
} catch (final Throwable x) {
AccessController.doPrivileged(new PrivilegedAction<>() {
public Void run() {
if (System.err != null)
new Error("Cleaner terminated abnormally", x)
.printStackTrace();
System.exit(1);
return null;
}});
}
}

总结:

  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 Unsafe.freeMemory() 方法
  • ByteBuffer 的实现类内部,使用了Cleaner(虚引用)来监测 ByteBuffer 对象,ByteBuffer 对象被垃圾回收时会由 ReferenecHandler 后台线程通过Cleaner.clean()方法调用Unsafe.freeMemory()释放直接内存。

注意:-XX:+DisableExplicitGC 这个参数可以关闭 ”System.gc() 显式垃圾回收“ (在JVM调优会用),这样会导致以上分析的案例中,直接内存无法释放。

这种情况则需要自己主动通过 unsafe.freeMemory(address);来主动释放

上面这么复杂的直接内存分配和释放,其实就是Unsafe的两个方法,为了更加直观理解底层,请看一下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import sun.misc.Unsafe;
import java.io.IOException;
import java.lang.reflect.Field;

public class TestDemo {

static int _1G = 1024 * 1024 * 1024;

public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
long base = unsafe.allocateMemory(_1G);
unsafe.setMemory(base,_1G,(byte)0);
System.out.println("分配直接内存完毕");
System.in.read();

System.out.println("开始释放直接内存");
unsafe.freeMemory(base);
System.in.read();
}

/**
* 不建议这么获取并使用Unsafe,这里只是为了演示测试
* @return
*/
public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe)f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

三、垃圾回收

Java 垃圾回收主要关注的是 堆内存

1、如何判断对象可以回收?

1.1 引用计数法

给对象添加一引用计数器,被引用一次计数器值就加 1;当引用失效时,计数器值就减 1;计数器为 0 时,对象就可以垃圾回收,简单高效。

弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。所以Java虚拟机垃圾回收没有采用计数法。

1.2 可达性分析法

  • Java虚拟机中垃圾回收器采用可达性分析法来探索所有存活的对象

  • 扫描“堆”中的对象,看是否能够沿着 GC Roots 对象为起点的引用链接找到该对象,找不到表示可以回收。

  • 那些对象可以作为 GC RootsMenory Analyzer(MAT) 这个软件可以帮助分析。

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。 
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI(Native方法)引用的对象

1.3 四种引用(强、软、弱、虚)

JDK1.2 以前,一个对象只有被引用和没有被引用两种状态。
后来,Java 对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用强度依次逐渐减弱。

强引用

指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用。

特点:只有 GC Root 全部都不引用该对象时,才会回收强引用对象。

如上图:B、C对象都不引用A1对象时,A1对象才会被回收。

软引用

指还有用但并非必需的对象,内存不足会回收,类似 “SoftReference<Object> object = new SoftReference<Object>()”这类的引用。

特点:仅有软引用引用该对象时,发生垃圾回收后,如果仍然内存不足会再次触发“垃圾回收”回收软引用对象。

如上图:B对象 不再“强引用” A2对象,发生垃圾回收且内存不足时,G对象 “软引用”的 A2对象 就会被垃圾回收。

弱引用

和“软引用”很相似,区别在于:无论内存是否足够,都会被回收掉。类似“WeakReference<Object> object = new WeakReference<Object>()” 这类的引用。

特点:发生垃圾回收时,无论内存是否充足,都会回收掉弱引用。

如上图: 如果 B对象 不再强引用 A3对象,只有C对象弱引用 A3对象,发生垃圾回收时,则A3对象会被回收。

虚引用

当虚引用对象所引用的对象被回收以后,虚引用对象就会被放入引用队列中,由Reference Handler 线程调用虚引用相关发发是否内存。声明“PhantomReference<Object>”

虚引用的一个体现是释放“直接内存”所分配的内存,当引用的对象ByteBuffer被垃圾回收以后,虚引用对象Cleaner就会被放入引用队列中,然后调用Cleaner的clean方法来释放直接内存。

虚引用是最弱的一种引用关系。 无法通过虚引用来取得一个对象实例 。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

终结器引用

所有的类都继承自Object类,Object类有一个finalize方法。当发生垃圾回收时,某个对象不再被其他的对象所引用时,会先将终结器引用对象放入引用队列中,然后根据终结器引用对象找到它所引用的对象,然后调用该对象的finalize方法。调用以后,该对象就可以在下一次垃圾回收发生时被回收。

特点:

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

回收优先级低,不推荐使用终结器引用。

如上图:B对象不再引用A4对象。这时终结器引用对象就会被放入引用队列中(对象还没有被回收),引用队列会根据它,找到它所引用的对象A4,然后调用被引用对象的finalize方法,调用完成以后,该对象就可以在下次垃圾回收被回收。

示例

软引用示例:

清理软引用示例:

弱引用示例:

2、垃圾回收算法

2.1 标记清除

定义:标记清除算法顾名思义,是指在虚拟机执行垃圾回收的过程中,先采用标记算法确定可回收对象,然后垃圾收集器根据标识清除相应的内容,给堆内存腾出相应的空间。

优点:速度快

缺点容易产生大量的内存碎片,可能无法满足大对象的内存分配,一旦导致无法分配对象,那就会导致JVM启动GC,一旦启动GC,我们的应用程序就会暂停,这就导致应用的响应速度变慢。

2.2 标记整理

标记-整理 会将不被GC Root引用的对象回收,清楚其占用的内存空间。然后整理剩余的对象,可以有效避免因内存碎片而导致的问题,但是因为整体需要消耗一定的时间,所以效率较低。

优点:不会有内存碎片

缺点:速度慢

2.3 复制

将内存分为大小相等的两个区域,FROM和TO(TO中为空)。先将被GC Root引用的对象从FROM放入TO中,再回收不被GC Root引用的对象。然后交换FROM和TO。

优点:不会有内存碎片

缺点:需要占用双倍内存空间

3、分代垃圾回收

分为两大区:新生代和老年代。需长期存活的对象会存放在老年代区,很少发生垃圾回收;在新生代,垃圾回收发生频繁,处理生命周期比较短的对象。新生代也细分为三个区:

  • 新生代
    • 伊甸园(Eden)
    • 幸存区(Survivor)From
    • 幸存区(Survivor)To
  • 老年代

新生代区发生的垃圾回收称为:Minor GC

  • 新创建的对象默认存放在Eden区
  • 当Eden区内存不足,会触发垃圾回收 Minor GC ,存活的对象会复制到 “幸存区To”。存活对象寿命加1,再交换幸存区(From - To)。
  • Minor GC 会引发 stop the world ,暂停其他用户线程,等垃圾回收结束,用户线程才恢复运行。
  • 当Eden区再次内存不足,再触发垃圾回收 Minor GC ,Eden区存活对象复制到 “幸存区To”,并且回收 “幸存区From”,“幸存区From”存活对象也复制到“幸存区To”。存活对象寿命加1(原来“幸存区From”存活对象这时候寿命是 2 了),再交换幸存区(From - To)。
  • 如果幸存区中的对象的寿命超过阈值,会晋升老年代,最大寿命是15(4bit)。
  • 新生代和老年代中的内存都满了,就会先尝试触发 Minor GC ,如果空间扔不足,再触发Full GCstop the world 时间更长。

GC分析总结

  • 大对象处理策略

    当遇到一个较大的对象时,就算新生代的伊甸园为空,也无法容纳该对象时,会将该对象直接晋升为老年代

  • 线程内存溢出

    某个线程的内存溢出了而抛异常(out of memory),不会让其他的线程结束运行
    这是因为当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行,进程依然正常。

4、垃圾回收器

STW: Stop The World,Minor GC 会引发 Stop The World ,暂停其他用户线程,等垃圾回收结束,用户线程才恢复运行。

三种垃圾回收器概述和特点比较:

  • 串行
    • 单线程
    • 堆内存较小,适合个人电脑
  • 吞吐量优先
    • 多线程
    • 堆内存较大,多个CPU
    • 让单位时间内,总的STW时间最短,如2次GC: 0.2 + 0.2 = 0.4
  • 响应时间优先
    • 多线程
    • 堆内存较大,多个CPU
    • 尽可能让单次STW的时间最短,如5次GC: 0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5

4.1 串行

因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态。

4.2 吞吐量优先

并行执行,会暂停用户线程。

4.3 响应时间优先

并发执行,不暂停用户线程,和用户线程并发执行(与用户线程抢CPU)。

基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片。

4.4 G1 (Garbage First)

G1是一个垃圾回收器, JDK7 由官方支持, JDK9 开始被默认使用的垃圾回收器(废弃了CMS垃圾回收器),适用场景:

  • 同时注重吞吐量(Throughput)和低延迟(Low latency)
  • 超大堆内存(内存大的),会将堆内存划分为多个 大小相等 的区域(Region)
  • 整体上是 标记+整理 算法,两个区域(Region)之间是 复制 算法

相关的参数:

1
2
3
-XX:+UseG1GC                  //开关,JDK8要手动开启
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time //默认200ms
4.4.1 G1垃圾回收阶段

这是一个循环的过程

4.4.2 G1 Young Collection

会STW

E: 伊甸园区

S: 幸存区

O: 老年代

新生代伊甸园区

新生代垃圾回收:幸存对象 -> 复制算法 -> 幸存区

新生代垃圾回收:幸存区大龄对象 -> 复制算法 -> 老年代

4.4.3 G1 Young Collection + CM(并发标记)
  • 在 Young GC 时会对 GC Root 进行初始标记
  • 在老年代占用 堆内存的比例 达到阈值时,对进行并发标记(不会STW),由参数 -XX:InitiatingHeapOccupancyPercent=percent(默认45%)

4.4.4 G1 Mixed Collection (混合回收)

会对 E S O 进行全面的垃圾回收

  • 最终标记 (Remark) 会STW
  • 拷贝存活 (Evacuation) 会STW

-XX:MaxGCPauseMills:ms 用于指定最长的停顿时间

:为什么有的老年代被拷贝了,有的没拷贝?

因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)

4.4.5 Full GC
  • SerialGC
    • 新生代内存不足发生的垃圾回收 – minor gc
    • 老年代内存不足发生的垃圾回收 – Full GC
  • ParallelGC
    • 新生代内存不足发生的垃圾回收 – minor gc
    • 老年代内存不足发生的垃圾回收 – Full GC
  • CMS
    • 新生代内存不足发生的垃圾回收 – minor gc
    • 老年代内存不足
  • G1
    • 新生代内存不足发生的垃圾回收 – minor gc
    • 老年代内存不足(老年代所占内存超过阈值)
      • 如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理
      • 如果垃圾产生速度快于垃圾回收速度,便会触发Full GC
4.4.6 G1 Young Collection 跨代引用

新生代回收的跨代引用(老年代引用新生代)问题

老年代区细分为多个卡表,如果某卡表中有对象引用了新生代的对象,那么这块区域标记为 脏卡

  • 卡表与Remembered Set
    • Remembered Set 存在于E中,用于保存新生代对象对应的脏卡
      • 脏卡:O被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
  • 在引用变更时通过post-write barried + dirty card queue (更新指令先放入脏卡队列)
  • concurrent refinement threads 更新 Remembered Set (由一个线程完成更新脏卡的操作)

4.4.7 G1 Remark (重新标记)

黑色:已被处理,需要保留的

灰色:正在处理中的

白色:还未处理的

并发阶段标记时处理示例图:

并发下,下图中对C对象的处理可能受到用户线程的影响,比如:

标记前,“B对象”对“C对象”的引用被用户线程删除,这时“C对象”没有引用被标记为白色(垃圾),但标记尚未完成,“C对象”又被用户线程作为“A对象”的引用,但“C对象”已经被误标记为垃圾。

这种问题就要通过remark来解决。

重新标记阶段处理示意图:

  • 当引用发生改变时,JVM给它加入一个写屏障
  • 当A引用了C,就会给C加一个写屏障,写屏障的指令会被执行,指令执行的操作是将C放入一个队列(satb_mark_queue)当中,并将C变为 “处理中” 灰色状态
  • 在并发标记阶段结束以后,进入重新标记阶段(会STW),然后将放在该队列中的对象重新处理,发现有强引用引用它,就不会垃圾回收它。
4.4.8 JDK 8u20字符串去重

优点与缺点

  • 节省了大量内存
  • 新生代回收时间略微增加,导致略微多占用CPU

字符串去重

1
2
3
4
5
6
//开关
-XX:+UseStringDeduplication

//不同的对象,使用的是同一个char数组
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
  • 将所有新分配的字符串(底层是char[])放入一个队列
  • 新生代回收时,G1并发检查是否有重复的字符串
  • 如果字符串的值一样,就让他们引用同一个字符串对象
  • 注意,其与 String.intern() 的区别
    • String.intern()关注的是字符串对象
    • “字符串去重”关注的是char[]
    • 在JVM内部,使用了不同的字符串表
4.4.9 JDK 8u40 并发标记类卸载

JDK 8u40在并发标记阶段结束以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类。

开关:

1
-XX:+ClassUnloadingWithConcurrentMark 默认启用
4.4.10 JDK 8u60回收巨型对象
  • 一个对象大于region的一半时,就称为巨型对象
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

4.4.11 JDK 9并发标记起始时间的调整
  • 并发标记必须在堆空间占满前完成,否则退化为Full GC
  • JDK9之前,需要使用 -XX:InitatingHeapOccupancyPercent 指定比例
  • JDK9可以动态调整此值
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • JVM进行数据采样并动态调整
    • 总会添加一个安全的空档空间

5、GC(垃圾回收)调优

调优跟应用、环境等因素有关,需要根据经验做出调整。

查看Java虚拟机调优参数命令,具体命令可以看官网的文档说明。

1
java -XX:+PrintFlagsFinal -version | findstr "GC"

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
❯ java -XX:+PrintFlagsFinal -version | findstr "GC"
java version "11.0.1" 2018-10-16 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.1+13-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.1+13-LTS, mixed mode)
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} {default}
bool BindGCTaskThreadsToCPUs = false {product} {default}
uint ConcGCThreads = 1 {product} {ergonomic}
bool DisableExplicitGC = false {product} {default}
bool ExplicitGCInvokesConcurrent = false {product} {default}
uintx G1MixedGCCountTarget = 8 {product} {default}
uintx GCDrainStackTargetSize = 64 {product} {ergonomic}
uintx GCHeapFreeLimit = 2 {product} {default}
uintx GCLockerEdenExpansionPercent = 5 {product} {default}
bool GCLockerInvokesConcurrent = false {product} {default}
uintx GCPauseIntervalMillis = 201 {product} {default}
uint GCTaskTimeStampEntries = 200 {product} {default}
uintx GCTimeLimit = 98 {product} {default}
uintx GCTimeRatio = 12 {product} {default}
bool HeapDumpAfterFullGC = false {manageable} {default}
bool HeapDumpBeforeFullGC = false {manageable} {default}
size_t HeapSizePerGCThread = 43620760 {product} {default}
uintx MaxGCMinorPauseMillis = 18446744073709551615 {product} {default}
uintx MaxGCPauseMillis = 200 {product} {default}
int ParGCArrayScanChunk = 50 {product} {default}
uintx ParGCDesiredObjsFromOverflowList = 20 {product} {default}
bool ParGCTrimOverflow = true {product} {default}
bool ParGCUseLocalOverflow = false {product} {default}
uintx ParallelGCBufferWastePct = 10 {product} {default}
uint ParallelGCThreads = 4 {product} {default}
bool PrintGC = false {product} {default}
bool PrintGCDetails = false {product} {default}
bool ScavengeBeforeFullGC = false {product} {default}
bool UseAdaptiveGCBoundary = false {product} {default}
bool UseAdaptiveSizeDecayMajorGCCost = true {product} {default}
bool UseAdaptiveSizePolicyWithSystemGC = false {product} {default}
bool UseConcMarkSweepGC = false {product} {default}
bool UseDynamicNumberOfGCThreads = true {product} {default}
bool UseG1GC = true {product} {ergonomic}
bool UseGCOverheadLimit = true {product} {default}
bool UseGCTaskAffinity = false {product} {default}
bool UseMaximumCompactionOnSystemGC = true {product} {default}
bool UseParallelGC = false {product} {default}
bool UseParallelOldGC = false {product} {default}
bool UseSerialGC = false {product} {default}

----------------------------------------------------

$ "C:\Program Files\Java\jdk1.8.0_25\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)
uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product}
uintx AutoGCSelectPauseMillis = 5000 {product}
bool BindGCTaskThreadsToCPUs = false {product}
uintx CMSFullGCsBeforeCompaction = 0 {product}
uintx ConcGCThreads = 0 {product}
bool DisableExplicitGC = false {product}
bool ExplicitGCInvokesConcurrent = false {product}
bool ExplicitGCInvokesConcurrentAndUnloadsClasses = false {product}
uintx G1MixedGCCountTarget = 8 {product}
uintx GCDrainStackTargetSize = 64 {product}
uintx GCHeapFreeLimit = 2 {product}
uintx GCLockerEdenExpansionPercent = 5 {product}
bool GCLockerInvokesConcurrent = false {product}
uintx GCLogFileSize = 8192 {product}
uintx GCPauseIntervalMillis = 0 {product}
uintx GCTaskTimeStampEntries = 200 {product}
uintx GCTimeLimit = 98 {product}
uintx GCTimeRatio = 99 {product}
bool HeapDumpAfterFullGC = false {manageable}
bool HeapDumpBeforeFullGC = false {manageable}
uintx HeapSizePerGCThread = 87241520 {product}
uintx MaxGCMinorPauseMillis = 4294967295 {product}
uintx MaxGCPauseMillis = 4294967295 {product}
uintx NumberOfGCLogFiles = 0 {product}
intx ParGCArrayScanChunk = 50 {product}
uintx ParGCDesiredObjsFromOverflowList = 20 {product}
bool ParGCTrimOverflow = true {product}
bool ParGCUseLocalOverflow = false {product}
uintx ParallelGCBufferWastePct = 10 {product}
uintx ParallelGCThreads = 4 {product}
bool ParallelGCVerbose = false {product}
bool PrintClassHistogramAfterFullGC = false {manageable}
bool PrintClassHistogramBeforeFullGC = false {manageable}
bool PrintGC = false {manageable}
bool PrintGCApplicationConcurrentTime = false {product}
bool PrintGCApplicationStoppedTime = false {product}
bool PrintGCCause = true {product}
bool PrintGCDateStamps = false {manageable}
bool PrintGCDetails = false {manageable}
bool PrintGCID = false {manageable}
bool PrintGCTaskTimeStamps = false {product}
bool PrintGCTimeStamps = false {manageable}
bool PrintHeapAtGC = false {product rw}
bool PrintHeapAtGCExtended = false {product rw}
bool PrintJNIGCStalls = false {product}
bool PrintParallelOldGCPhaseTimes = false {product}
bool PrintReferenceGC = false {product}
bool ScavengeBeforeFullGC = true {product}
bool TraceDynamicGCThreads = false {product}
bool TraceParallelOldGCTasks = false {product}
bool UseAdaptiveGCBoundary = false {product}
bool UseAdaptiveSizeDecayMajorGCCost = true {product}
bool UseAdaptiveSizePolicyWithSystemGC = false {product}
bool UseAutoGCSelectPolicy = false {product}
bool UseConcMarkSweepGC = false {product}
bool UseDynamicNumberOfGCThreads = false {product}
bool UseG1GC = false {product}
bool UseGCLogFileRotation = false {product}
bool UseGCOverheadLimit = true {product}
bool UseGCTaskAffinity = false {product}
bool UseMaximumCompactionOnSystemGC = true {product}
bool UseParNewGC = false {product}
bool UseParallelGC := true {product}
bool UseParallelOldGC = true {product}
bool UseSerialGC = false {product}

5.1 调优领域

  • 内存
  • 锁竞争
  • CPU占用
  • IO
  • GC

5.2 确定目标

应用需要【低延迟】还是【高吞吐量】?情景举例:

  • 示例1:科学运算应用追求【高吞吐量】,延迟时间影响不大,可以选择【高吞吐量】垃圾回收器ParallelGC。
  • 示例2:互联网应用需要更快的响应时间,可以选择【低延迟】垃圾回收器。

选择合适的回收器:

  • 【低延迟】垃圾回收器:CMS G1 ZGC

  • 【高吞吐量】垃圾回收器:ParallelGC

  • Zing GC 回收器自称零STW,可管理超大内存

5.3 最好的GC是不发生GC

如果经常发生Full GC,首先应该检查代码问题,比如:查数据库把整张表的数据都拿出来了。

查看Full GC前后的内存占用,考虑以下几个问题

  • 数据是不是太多?(select * from big_tables)
  • 数据表示是否太臃肿
    • 对象图
    • 对象大小
  • 是否存在内存泄漏
    • static Map map = new HashMap()作为缓存
    • 一直往map防止对象而不移除,越积越多
    • 可以用软、弱引用解决
    • 或者Redis等第三方软件作为缓存

5.4 新生代调优

新生代的特点

  • 所有的new操作分配内存都是非常廉价的
    • TLAB: thread-local allocation buffer (线程局部分配缓冲区)
    • TLAB的作用是让每个线程用私有的伊甸园区来分配new对象内存
  • 死亡对象回收零代价
  • 大部分对象用过即死(朝生夕死)
  • Minor GC 所用时间远远小于 Full GC

新生代内存越大越好么?答案:不是!

  • 新生代内存太小:频繁触发Minor GC,会STW,会使得吞吐量下降
  • 新生代内存太大:老年代内存占比有所降低,会更频繁地触发Full GC。而且触发Minor GC时,清理新生代所花费的时间会更长

新生代调优:

  • Oracle建议新生代内存占堆大小的 25% ~ 50% ,-Xmn 参数设置新生代大小。
  • 老师推荐:容纳所有【并发量 * ( 请求 - 响应 ) 】的数据为宜

幸存区调优:

  • 幸存区大到能够保留【当前活跃对象 + 需要晋升的对象】
  • 晋升阈值配置得当,让长时间存活的对象尽快晋升

5.5 老年代调优

以CMS为例:

  • CMS的老年代内存越大越好
  • 先尝试不用调优,如果没有FullGC则不需要调优,有FullGC也先调优新生代
  • 观察FullGC时老年代内存占用,调大老年代预设值 1/4 ~ 1/3

5.6 案例分析

还是看视频吧