Java Virtual Machine(一)

傷城~ 2023-09-25 12:43 175阅读 0赞

目录

    1. 概述
    1. 内存结构
      1. 程序计数器
      1. 虚拟机栈
        1. 概述
        1. 线程诊断
      1. 本地方法栈
        1. 概述
        1. 堆内存诊断
      1. 方法区
        1. 运行时常量池
        1. String Table
        1. 直接内存
    1. 垃圾回收
      1. 判断对象可以被回收的算法
        1. 引用计数法
        1. 可达性分析算法
      1. 五种常见引用类型
        1. 简介及其回收机制
        1. 代码演示
      1. 垃圾回收算法
        1. 标记清除算法
        1. 标记整理算法
        1. 复制算法
        1. 对比
        1. 分代回收
        1. 分代回收小结
        1. VM Options
        1. 演示垃圾回收
      1. 垃圾回收器
        1. 回收器分类
        1. G1

1. 概述

JVM: java程序运行环境(字节码运行环境)。

  • 好处:

    • 一处编译、到处运行
    • 自动内存管理,垃圾回收机制(GC)

a4757a395eed4dd48f6ba5866908665c.png9759fa618b7f41eea5f35b591c96f6da.png

2. 内存结构

1. 程序计数器

先看一段简单程序及其字节码:javap -c Demo1.class
a4b01231fecc44afa821f9f19ec97653.png4859bf11bf894e68bebf996d649b76d0.png
java代码执行流程:
fd41fd2c25884358b91115c4783f3bd6.png

程序计数器:

  • 作用: 记住下一条jvm指令的地址。

    • 二进制字节码前面的数字是下一条指令的地址
    • 物理实现:寄存器(速度很快)
  • 特点:

    • 线程私有
    • 不会存在内存溢出

2. 虚拟机栈

1. 概述

栈: 先进后出的数据结构。

虚拟机栈: 线程运行时的内存空间。

  • 一个栈由多个栈帧组成,一个栈帧就对应一个方法的调用所占用的内存
  • 每个线程只有一个活动栈帧,对应着当前执行的那个方法
  • 栈帧内存在每一次方法执行完之后都会弹出栈内存

栈内存溢出原因(StackOverflowError):

  • 栈帧过多(如,不断递归调用)
  • 栈内存过大

VM options设置栈内存大小:
-Xss256k 设置每个线程的栈大小。

  • jdk5之前,每个栈的大小是 256k,之后是 1M
  • 相同物理内存下,减小此值可生成更多线程,但操作系统对于一个进程的线程数是由限制的
  • 超出线程数限制,就会报错 StackOverflowError
    eab8ff90caa34e599e7477174680901c.pngd957e4cfe54743f5b74a1e125f5f813a.png

2. 线程诊断

Linux查看线程占用:

  • top 查看进程内存、CPU占用(定位进程)
  • ps H -eo pid,tid,%cpu | grep xxxxx 查看线程对CPU的占用(定位线程)

    • H 打印所有进程和线程
    • -eo 规定要输出的内容

      • pid,tid,%cpu pid,tid和CPU占用
    • grep 过滤进程的条件
  • jstack 进程id 列出进程中的所有线程(根据线程id—tid的十六进制数去找到目标线程)

排除线程死锁: 迟迟得不到结果。

  • jstack 进程id 会打印出死锁死锁所在范围
    Find one Java-level deadlock
    Java stack information for the threads listed above

3. 本地方法栈

本地方法栈: JVM在调用本地方法的时候需要的内存空间。

  • 本地方法:不是由Java代码编写的,可以直接与操作系统底层打交道的API(如,Object的clone()、hashCode()、notify()、wait())

4. 堆

1. 概述

堆: 通过 new ,创建对象都会使用堆内存。

  • 特点:

    • 线程共享,线程安全问题
    • 垃圾回收机制

VM options设置堆内存大小:

  • -Xmx8m 设置JVM最大堆内存为8M

2. 堆内存诊断

jps 查看有哪些java进程(显示:进程id 进程名)

jmap -heap pid 查看堆内存占用情况

jconsole 图形界面检查内存、线程、类…
4bbce31745714d779d01c553a796c734.png

jvisualvm
6d42f44697cc457490e405bad3b91645.pngf6e0007bc68d4bde807b6752f5fdd5a8.pngda589009e9ff456fa0ce9a717c205417.pnge5383cce97964486898dd8aade9f4cad.png

5. 方法区

方法区:

  • 线程共享区域
  • 存储与类结构相关的信息:run-time constant poolfieldmethod datamethodsconstructors
  • 虚拟机启动时创建
  • 逻辑上是堆的组成部分
    69f1ddda4e084b318d4bc01ebce3ce91.png

方法区内存溢出: 1.8之前是永久代内存不足导致内存溢出、1.8之后是元空间内存不足导致内存溢出。

VM options设置永久代最大保留区域(了解即可):
-XX:MaxPermSize=2048m
在1.8已经弃用。

VM options设置元空间:

  • -XX:MetaspaceSize=100m 设置元空间初始大小为100M
  • -XX:MaxMetaspaceSize=100m 设置元空间最大可分配大小为100M

通过字节码动态生成类的包:CGLIB

1. 运行时常量池

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

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

JVM指令集

2. String Table

  • 字符串的创建过程:
    请点击跳转我的文章: 从字节码分析字符串是否相等

  • 字符串延迟加载(实例化):
    d428f2aeffa741feaf22dd7a3077132f.png
    通过debug可以发现,只有当使用到某个字符串的时候,才会放入到串池(String Table)中,而且,不会重复放置相同的字符串。
    这就是延迟加载机制。

串池的位置:

  • 1.6 存在永久代中,内存溢出的时候会报错:java.lang.OutOfMemoryError: PermGen space
  • 1.8 存在堆中,内存溢出会报错:java.lang.OutOfMemoryError: Java heap spacejava.lang.OutOfMemoryError: GC overhead limit exceeded

    • 设置堆最大内存为4M:-Xmx4m
    • 代码:

      1. public class TestStringTableLocation {
      2. public static void main(String[] args) {
      3. List<String> list = new ArrayList<>();
      4. int i = 0;
      5. try {
      6. for (int j = 0; j < 10000000; j++) {
      7. list.add(String.valueOf(j).intern());
      8. i++;
      9. }
      10. } catch (Throwable e) {
      11. e.printStackTrace();
      12. } finally {
      13. System.out.println(i);
      14. }
      15. }
      16. }

      69eabc6c73634b0fb4c25bde7a7aaae6.png

      上述报错的原因:当98%的时间花在了垃圾回收上面,但是只有2%的堆空间被回收了,JVM就会放弃垃圾回收,直接报错(并不会报堆空间不足的错)
      如果想要报堆空间不足的错,就需要将上述的机制关掉:完整的虚拟机参数:-Xmx4m -XX:-UseGCOverheadLimit
      485c19a5949c46f698c3454a6e1686c4.png


演示串池的垃圾回收:
虚拟机参数:-Xmx4m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc

  • -XX:+PrintStringTableStatistics:打印有关StringTable和SymbolTable的统计信息(+是开启、-是关闭)
  • -XX:+PrintGCDetails: 打印输出详细的GC收集日志的信息
  • -verbose:gc:在控制台输出GC情况

    public class TestStringTableGC {

    1. public static void main(String[] args) {
    2. int i = 0;
    3. try {
  1. } catch (Throwable e) {
  2. e.printStackTrace();
  3. } finally {
  4. System.out.println(i);
  5. }
  6. }
  7. }

02f37da0963840febb061eb822909ee0.png1598356df7ac40b09f38748693d682a9.png

  1. public class TestStringTableGC {
  2. public static void main(String[] args) {
  3. int i = 0;
  4. try {
  5. for (int j = 0; j < 100; j++) {
  6. String.valueOf(j).intern();
  7. i++;
  8. }
  9. } catch (Throwable e) {
  10. e.printStackTrace();
  11. } finally {
  12. System.out.println(i);
  13. }
  14. }
  15. }

21cdc2385df749b88c51aa5e089cb683.png

通过不断修改循环的上限值,可以从控制台看到GC回收被触发:
ab35b7a159b048cf9c63f4eb6a2b6993.png


性能调优:
通过上述案例我们可以知道,String Table采用的是桶机制,所以:

  • 当桶足够多的时候,桶元素发生hash碰撞的几率就更小,查找速度就会增快
  • 当桶的数量较少的时候,桶元素发生hash碰撞的几率就更大,导致链表更长,从而降低查找速度

所以String Table的性能调优就是修改桶的个数。

虚拟机参数:

  • -Xms500m设置堆内存初始值为500m
  • -Xmx500m 设置堆最大内存为500M
  • -XX:+PrintStringTableStatistics 打印有关StringTable和SymbolTable的统计信息
  • -XX:StringTableSize=20000 设置串池的桶个数为20000

    public class TestStringTableOptimization {

    1. public static void main(String[] args) throws IOException {
    2. try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("D:\\YH\\examtest20210723.sql")))) {
    3. String line = null;
    4. long start = System.nanoTime();
    5. while (true) {
    6. line = reader.readLine();
    7. if (line == null) {
    8. break;
    9. }
    10. line.intern();
    11. }
    12. System.out.println("const:" + (System.nanoTime() - start) / 100000);
    13. }
    14. }

    }

当桶的个数为2000时: -XX:StringTableSize=20000
6a6583463d544d7c8fae8f2ddbf3ecbc.png
当桶的个数为10000时: -XX:StringTableSize=10000
dd3ce602229e429cb9c13e82b30510de.png
当桶的个数为1009时: -XX:StringTableSize=1009
169f7717a81c48d68a991edb94a6c23d.png


字符串入串池的优点: 极大节约了内存占用(重复的字符串只会在串池中存储一个)

  • 不存入串池 list.add(line);

    1. public class TestStringTableOptimization {
    2. public static void main(String[] args) throws IOException {
    3. List<String> list = new ArrayList<>();
    4. System.in.read();
    5. for (int i = 0; i < 10; i++) {
    6. try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("D:\\YH\\examtest20210723.sql")))) {
    7. String line = null;
    8. long start = System.nanoTime();
    9. while (true) {
    10. line = reader.readLine();
    11. if (line == null) {
    12. break;
    13. }
    14. /*line.intern();*/
    15. list.add(line);
    16. }
    17. System.out.println("const:" + (System.nanoTime() - start) / 100000);
    18. }
    19. }
    20. System.in.read();
    21. }
    22. }

    ec0b3a6356854b94b42da703ecaea7ef.png

  • 存入串池: list.add(line.intern());
    e653ecb7262a41758d1648040c8b0683.png

3. 直接内存

直接内存:

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

不使用直接内存:
d6b419e69faf4751a3212af385c18006.png

使用直接内存:
62d37f5738fe4be5b97f7edac2a8cfe0.png

直接内存使得java代码能直接读取到系统内存的数据,极大缩减了代码执行时间。

分配直接内存缓冲区的方法:

  1. public static ByteBuffer allocateDirect(int capacity) {
  2. return new DirectByteBuffer(capacity);
  3. }

直接内存溢出演示:

  1. public class TestDirectOut {
  2. static int _100Mb = 1024 * 1024 * 100;
  3. public static void main(String[] args) {
  4. List<ByteBuffer> list = new ArrayList<>();
  5. int i = 0;
  6. try {
  7. while (true) {
  8. // allocateDirect分配多少内存,就会占用本地多少内存
  9. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
  10. list.add(byteBuffer);
  11. i++;
  12. }
  13. } finally {
  14. System.out.println(i);
  15. }
  16. }
  17. }

72289e3e17c3466b9d6a522d835e49c6.png


分配和回收原理:

  • 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
  • ByteBuffer 的实现类内部,使用了 Cleaner(虚引用)来监测 ByteBuffer 对象,一旦ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleanerclean 方法调用 freeMemory 来释放直接内存

JVM调优常用参数: -XX:+DisableExplicitGC

  • 让显示的垃圾回收无效(即直接手动敲代码回收,如 System.gc()
  • 因为显示的垃圾回收是一种 Full gc 即,要回收新生代,还要回收老年代,会造成较长的代码停留时间

但是,禁用掉显示的垃圾回收之后,直接内存的回收就只能依靠 Cleaner 来检测回收了,这样就会导致直接内存长时间得不到释放。

  1. public static void main(String[] args) throws IOException {
  2. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
  3. System.out.println("分配完毕...");
  4. System.in.read();
  5. System.out.println("开始释放...");
  6. byteBuffer = null;
  7. System.gc(); // 显式的垃圾回收,Full GC
  8. System.in.read();
  9. }

这时候就需要使用Unsafe来手动回收内存了:

  1. public static void main(String[] args) throws IOException {
  2. Unsafe unsafe = getUnsafe();
  3. // 分配内存
  4. long base = unsafe.allocateMemory(_1Gb);
  5. unsafe.setMemory(base, _1Gb, (byte) 0);
  6. System.in.read();
  7. // 释放内存
  8. unsafe.freeMemory(base);
  9. System.in.read();
  10. }
  11. public static Unsafe getUnsafe() {
  12. try {
  13. Field f = Unsafe.class.getDeclaredField("theUnsafe");
  14. f.setAccessible(true);
  15. Unsafe unsafe = (Unsafe) f.get(null);
  16. return unsafe;
  17. } catch (NoSuchFieldException | IllegalAccessException e) {
  18. throw new RuntimeException(e);
  19. }
  20. }

3. 垃圾回收

1. 判断对象可以被回收的算法

1. 引用计数法

引用计数法: 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器就减1。任何时刻计数器为0的对象就是不再被使用的。

巨大缺陷: 很难解决对象之间相互循环引用的问题,因此JVM并未采用这种方法。

acc60141f5f5402f81cbbd033ffb6194.png

2. 可达性分析算法

可达性分析算法:

  • 用过一系列的 gc root 来判断对象是否被引用
  • 如果 gc root 可以直接或间接引用到某个对象,就表明该对象被引用,反之则说明该对象不可用
  • 在下次垃圾回收到达的时候,不可用的对象就会被回收
    482c14000c2e4f88a48860830d5ea07f.png
    如图,下次垃圾回收的时候,obj9、obj10、obj11就会被回收。

GC Roots对象取用范围:

  • System Class 系统类,启动类加载器加载的类(核心类)

    • 如Object、String、HashMap等
  • Native Stack 本地方法栈的操作系统方法
  • Busy Monitorsynchronized或者lock加锁的对象
  • Thread 活动线程用到的对象(一个线程对应一个栈,栈帧内的对象)

宣告对象死亡:
宣告对象死亡至少需要经历两次标记过程:

  • 第一次标记:

    • 在对对象进行可达性分析发现对象没有被 gc root 引用,则会对其进行标记并进行第一次筛选
    • 第一次筛选主要是为了判断改对象是否需要执行finalize()
      利用finalize()方法最多只会被调用一次的特性,我们可以实现延长对象的生命周期
      这是由于finalize()方法的调用时机具有不确定性,从一个对象变得不可到达开始,到finalize()方法被执行,所花费的时间这段时间是任意长的

      • 当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行
      • 如果对象被判定为有必要执行,则会被放到一个F-Queue队列
  • 第二次标记:

    • gc将F-Queue中的对象进行第二次标记
    • 如果这时候,对象通过调用finalize()gc root 引用链上的任何一个对象建立关联,那么此对象就会被移出即将被回收的队列

2. 五种常见引用类型

1. 简介及其回收机制

1838e59edbae40489840edfdbf6c46d7.png

强引用:

  • 如: Object obj = new Object() 变量obj强引用了实例出的对象
  • 只要引用链能找到此对象,就不会被回收

软/弱引用:

  • 只要没有被强引用直接地引用,都有可能被垃圾回收,如下图:
    63cf64a2038945aba7c016149c2da7a1.png
    软引用被回收: 因为obj2在引用cg root引用链中没有被强引用直接引用,所以在下一次垃圾回收内存不够的时候,有可能被回收
    弱引用被回收: obj3被强引用直接引用了,所以就不会被垃圾回收。可如果obj3也没有强引用直接引用,就会在下一次垃圾回收的时候被回收
  • 当软/弱引用的对象被回收之后,如果在创建软/弱引用的时候,被分配了一个引用队列,那么软/弱引用就会进入引用队列(这两者也会占用内存,也可以释放掉)
    a6cf634818b244ed87ff3ab54bad90f0.png
    b6c15514e00047dc8c2fa50163a7b3e6.png

虚/终结引用:

  • 虚/终结引用 必须配合引用队列来使用
  • 当 虚/终结引用对象 被创建的时候,就会创建一个引用队列
  • 虚引用回收:
    683188d6d9d340b4ac462c4a2c658ac9.png
    在创建ByteBuffer对象的时候,就会使用Cleaner来监测,而一旦没有强引用引用ByteBuffer的时候,ByteBuffer自己就会被垃圾回收掉,如下:
    281139b344004ba98f168761db7065ed.png
    但是这时候,直接内存还没有被回收,所以这时候,虚引用对象就会进入引用队列,由 Reference Handler 线程调用虚引用相关方法(Unsafe.freeMemory)释放直接内存
    52e16f6740ec490492c889e0393f82d1.png
  • 终结引用回收:
    当没有强引用去引用对象(重写了 finallize()的对象)的时候,JVM就会给此对象创建一个终结器引用
    30b03cb87d5742808587dd158476f8d8.png
    当对象被垃圾回收器回收的时候,终结器引用也会进入引用队列,但这时候对象还没有被回收
    a330f0480bf44a5a8dfedd76ae213435.png
    然后 Finalizer 线程(此线程优先级很低,被执行的机会很少)通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

2. 代码演示

在JDK1.2之前,只有引用和没引用两种状态。

SoftReference 实现软引用的类,WeakReference 实现弱引用的类、PhantomReference 实现虚引用的类。


软引用演示:

  • 虚拟机参数:-Xmx20m -XX:+PrintGCDetails -verbose:gc 最大堆内存20M,打印GC细节,控制台输出gc情况
  • 代码(软引用所引用对象的回收):

    • 不使用软引用情况:

      1. public class TestSoft {
  1. private static final int _4MB = 4 * 1024 * 1024;
  2. public static void main(String[] args) throws IOException {
  3. List<byte[]> list = new ArrayList<>();
  4. for (int i = 0; i < 5; i++) {
  5. list.add(new byte[_4MB]);
  6. }
  7. System.in.read();
  8. }
  9. 会造成内存溢出
  10. ![530bd51fa8f547b5b8213b77455eabf8.png][]
  11. * 使用软引用:
  12. public class TestSoft {
  13. private static final int _4MB = 4 * 1024 * 1024;
  14. public static void main(String[] args) throws IOException {
  15. soft();
  16. }
  17. public static void soft() {
  18. // list --> SoftReference --> byte[]
  19. List<SoftReference<byte[]>> list = new ArrayList<>();
  20. for (int i = 0; i < 5; i++) {
  21. SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
  22. System.out.println(ref.get());
  23. list.add(ref);
  24. System.out.println(list.size());
  25. }
  26. System.out.println("循环结束:" + list.size());
  27. for (SoftReference<byte[]> ref : list) {
  28. System.out.println(ref.get());
  29. }
  30. }
  31. }
  32. 不会造成内存溢出:
  33. ![8e5fd036a942459bbee3f76bdae36247.png][]
  34. **软引用特点:** 一次垃圾回收之后,内存仍然不足,就会把软引用所引用的对象回收。
  • 代码(软引用对象本身的回收

    • 使用引用队列 ReferenceQueue

      1. // 引用队列
      2. ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
      3. // 创建软引用的同时关联引用队列
      4. SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
      5. // 从队列中获取无用的 软引用对象,并移除
      6. Reference<? extends byte[]> poll = queue.poll();
      7. while( poll != null) {
      8. list.remove(poll);
      9. poll = queue.poll();
      10. }
    • 完整代码

      1. public class TestSoft{
      2. private static final int _4MB = 4 * 1024 * 1024;
      3. public static void main(String[] args) {
      4. List<SoftReference<byte[]>> list = new ArrayList<>();
      5. // 引用队列
      6. ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
      7. for (int i = 0; i < 5; i++) {
      8. // 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
      9. SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
      10. System.out.println(ref.get());
      11. list.add(ref);
      12. System.out.println(list.size());
      13. }
      14. // 从队列中获取无用的 软引用对象,并移除
      15. Reference<? extends byte[]> poll = queue.poll();
      16. while( poll != null) {
      17. list.remove(poll);
      18. poll = queue.poll();
      19. }
      20. System.out.println("===========================");
      21. for (SoftReference<byte[]> reference : list) {
      22. System.out.println(reference.get());
      23. }
      24. }
      25. }

弱引用演示:

  • 虚拟机参数:-Xmx20m -XX:+PrintGCDetails -verbose:gc
  • 代码(弱引用所引用的对象被回收

    1. public class TestWeak{
    2. private static final int _4MB = 4 * 1024 * 1024;
    3. public static void main(String[] args) {
    4. // list --> WeakReference --> byte[]
    5. List<WeakReference<byte[]>> list = new ArrayList<>();
    6. for (int i = 0; i < 10; i++) {
    7. WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
    8. list.add(ref);
    9. for (WeakReference<byte[]> w : list) {
    10. System.out.print(w.get()+" ");
    11. }
    12. System.out.println();
    13. }
    14. System.out.println("循环结束:" + list.size());
    15. }
    16. }

    dcbb6ef8267c4d7c97c58c76c9669f8c.png

3. 垃圾回收算法

1. 标记清除算法

109f307787b248e7b96942d68d2f5672.png

  • 先扫描没被 GC Root 引用链引用的对象,并对其进行标记
  • 释放被标记对象的内存
    并不会清零被释放的空间,新来的对象,会被插入到合适的释放空间中

优点:

  • 速度快,只需要记录地址

缺点:

  • 容易产生内存碎片
    只会清除对象,并不会释放的空间做进一步处理

2. 标记整理算法

8d81cf4ac1b9432ba98368d47e631ea9.png

优点:

  • 没有内存碎片

缺点:

  • 涉及到对象移动,效率偏低

3. 复制算法

先做标记:
e77868b67f8b48f5a8e9a221a3ddd209.png
a0ce5ad3d18a49de93f3f9c9eed37672.png

将被引用的对象复制到To中,顺便完成碎片整理:
ac9f0fba210f4fcfb198225930ef7a64.png
1228aa6615324b79b78820884a10518b.png
交换from和to的位置:
7af142f86ffa49d0b69de09e37328d4c.png

4. 对比


























算法 优点 缺点
标记清除(Mark Sweep) 速度快 会造成内存碎片
标记整理(Mark Compact) 没有内存碎片 速度慢
复制(Copy) 没有内存碎片 需要占用双倍内存空间

5. 分代回收

JVM采用的垃圾回收算法,是综合上述三种算法的一种叫做 分代回收 的算法。
e6bbc9d0e1e9436593179a07a4ece0fd.png
新生代:用完就可以丢的对象;老年代:需要一直使用的对象。
故而,老年代和新生代的回收策略不同。

  • 当新生代的伊甸园区逐渐放不下的时候,会触发一次垃圾回收——Minor GC
    标记→复制→To→To区幸存者寿命+1(初始是0)→回收伊甸园空间→交换幸存区from和to的位置
    7166cbc1abff4408aefc32cd39bc1de3.png
    ee513b5cc5e34c518641f5682358931e.png
    4bb216223bfa4be18b2c8ce34e4abef7.png
    eeb9b9253cad4115b75188d3fd731451.png
  • 继续往伊甸园存数据,当伊甸园又满了,就触发垃圾回收——Minor GC
    标记伊甸园区幸存对象、标记幸存区from中幸存对象→将幸存对象转移到from区→对幸存的对象寿命+1→回收内存空间→交换from和to的位置→伊甸园放入新对象
    3d5b4fdb937d42c6905542d4f844f3e9.png
    fa363741313b4084998c1cb9b4099980.png
    93ab8b92e6f54f13855ede94a6f728d5.png
    cda326328cdc45fc84c1bee77e90d618.png
  • 当新生代幸存区中某一对象的寿命超过了某一阈值(比如说15),就说明该对象的价值比较大,就会把该对象晋升到老年代中
    166d925e46974c43bf1e93c236b31a63.png
    4983706507ea4c68bb48c078c4ea6d36.png
  • 当老年代的内存空间不足的时候,就会触发垃圾回收——Full GC (标记整理算法)
    先对新生代进行回收——Minor GC,如果回收之后空间还是不足,就会对老年代进行回收
    81c2778cf3af46a883ff9340aa3cb388.png

6. 分代回收小结

  • 对象首先分配在 新生代——伊甸园区
  • 新生代空间不足触发 Minor GC,将 伊甸园From 中的存活对象 复制To 中,存活对象 +1,并且交换 FromTo
  • Minor GC 会引发 stop the world 暂停其他用户的线程,先让 gc 回收结束之后(速度很快,大部分都是回收,少部分复制),再恢复用户线程
  • 当对象寿命超过阈值的时候(最大阈值——15,4bit),就会晋升老年代
  • 当老年代空间不足,先尝试做一次Minor GC,如果空间仍不足,就触发 Full GC,然后引发 stop the world,但时停时间比 Minor GC 更长
  • Full GC 之后, 老年代空间仍然不足,就会触发 OutOfMemoryError

7. VM Options














































































参数 说明
-Xsssize 设置栈内存大小
-Xmssize 堆初始大小
-Xmxsize
-XX:MaxHeapSize=size
堆最大大小
-XX:MetaspaceSize=size 元空间初始大小
-XX:MaxMetaspaceSize=size 元空间最大可分配大小
-XX:-UseGCOverheadLimit 关闭GCOverheadLimit特性
-XX:+PrintStringTableStatistics 打印有关StringTable和SymbolTable的统计信息
-XX:+PrintGCDetails -verbose:gc 控制台打印GC详情
-XX:StringTableSize=size 设置串池的桶个数
-XX:+DisableExplicitGC 显示的垃圾回收无效
-Xmnsize
-XX:NewSize=size + -XX:MaxNewSize=size
新生代大小
-XX:SurvivorRatio=ratio 幸存区比例,radio:伊甸园占比
伊甸园:from:to=radio:1:1,默认是8:1:1
-XX:InitialSurvivorRatio=ratio
-XX:+UseAdaptiveSizePolicy
初始化比例
开启动态调整
-XX:MaxTenuringThreshold=threshold 晋升阈值
-XX:+PrintTenuringDistribution 晋升详情
-XX:+ScavengeBeforeFullGC 开启 FullGC 前 MinorGC
-XX:+UseSerialGC 使新生代和老年代都使用串行回收器

8. 演示垃圾回收

虚拟机参数: -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
0f122488ef31477c98e36470f5ade919.png

演示一:

  1. public class TestPolicy {
  2. private static final int _512KB = 512 * 1024;
  3. private static final int _1MB = 1024 * 1024;
  4. private static final int _6MB = 6 * 1024 * 1024;
  5. private static final int _7MB = 7 * 1024 * 1024;
  6. private static final int _8MB = 8 * 1024 * 1024;
  7. public static void main(String[] args) throws InterruptedException {
  8. ArrayList<byte[]> list = new ArrayList<>();
  9. list.add(new byte[_7MB]);
  10. list.add(new byte[_7MB]);
  11. list.add(new byte[_1MB]);
  12. }
  13. }

69710515d37744c080ee2ce3db1f26a9.png

演示二:大对象(即内存超过伊甸园大小的对象)
这时候,大对象因为在新生代放不下了,就会直接晋升到老年代,所以不会触发 Minor GC

  1. ArrayList<byte[]> list = new ArrayList<>();
  2. list.add(new byte[_8MB]);

bdde7ae4582546449f22d017ed6246ee.png

  1. ArrayList<byte[]> list = new ArrayList<>();
  2. list.add(new byte[_8MB]);
  3. list.add(new byte[_8MB]);

新生代放不下了,老年代也放不下,所以会报错:
但还是会挣扎一下,触发一次 Full GC ,顺带 MInor GC,如果还是放不下,那就回报错
0f4e27d8f1974783b24ed4bdd9b2e44b.png

演示三:线程抛出OOM,不会影响其他线程运行

  1. new Thread(() -> {
  2. ArrayList<byte[]> list = new ArrayList<>();
  3. list.add(new byte[_8MB]);
  4. list.add(new byte[_8MB]);
  5. }).start();
  6. System.out.println("sleep....");
  7. Thread.sleep(1000L);

de781dbf136c4ff191ab7d391bdde67c.png

  • 当一个线程抛出 OOM 之后,其占用的内存资源会被全部释放掉,因此不会影响到其他线程的运行

4. 垃圾回收器

1. 回收器分类

分类:

  • 串行

    • 单线程
    • 堆内存较小、适合个人电脑

      -XX:+UseSerialGC 会同时启动 serial(新生代串行回收,复制算法)和serialOld(老年代串行回收,复制算法)
      df785e584df44df5bf68dfba7c792871.png

  • 吞吐量优先

    • 多线程
    • 堆内存较大,多核CPU
    • 让单位时间内,STW的时间最短(追求最快的速度)

      jdk1.8默认使用的垃圾回收器
      -XX:+UseParallelGC -XX:+UseParallelOldGC 只需要开启其中一个,另外一个就会开启
      -XX:+UseAdaptiveSizePolicy 采用自适应大小策略(新生代大小)
      XX:GCTimeRatio=radio 调整吞吐量(垃圾回收时间占比 = 1/(1+radio),一般采用 19)
      -XX:MaxGCPauseMillis=ms 垃圾回收最大暂停毫秒数(默认值200ms,会与 GCTimeRatio 冲突)
      -XX:ParallelGCThreads=n 设置垃圾回收线程数
      c2d60ea2d0b84bfe9efff9e1107c3d97.png

  • 响应时间优先

    • 多线程
    • 堆内存较大,多核CPU
    • 尽可能让单次STW(stop the world)时间最短(次数很多,单次速度很快)
      -XX:+UseConcMarkSweepGC 开启 并行并发CMS垃圾回收器(在垃圾回收的一些阶段,可以和用户进程一起运行)

      • -XX:+UseParNewGC 工作在新生代的复制算法回收器,和CMS一起工作的
      • SerialOld 当CMS并发失败时,CMS会退化到此串行垃圾回收器

      -XX:ParallelGCThreads=n 指定并行 GC 线程的数量(最好与CPU核数相当)
      -XX:ConcGCThreads=threads GC并行时使用的线程数
      -XX:CMSInitiatingOccupancyFraction=percent 执行CMS垃圾回收的内存占比(默认65%开启CMS垃圾回收)
      -XX:+CMSScavengeBeforeRemark 重新标记之前对新生代进行一次垃圾回收
      01a33960240e412981bd8cab40de052a.png
      CMS采用标记清除算法,产生的碎片会比较多,导致并发失败,退化到SerialOld,然后处理好碎片之后再次回到CMS并发,这样退化的时候,会导致响应时间一下子变长。

2. G1

Garbage First

  • 适用场景:

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

    • -XX:+UseG1GC 开启
    • -XX:G1HeapRegionSize=size 设置堆的region大小,2的次方
    • -XX:MaxGCPauseMillis=time 垃圾回收最大暂停毫秒数(默认值200ms)
  • 回收阶段:
    c4dc8a9bab724b67bcc51ea103c2e621.png

    • Young Collection

      • 新建对象,存入伊甸园区域
        38b00ca7239144d09cf639250a425e22.png
        新生代的对象多了,就会存入幸存区
        104a3352d61b428ebbdd24fb006017ca.png
        当伊甸园转不下了或者幸存区的对象年龄超过了阈值,就会进入老年代
        697ae4cca8ef4bacbd191eff4d220db0.png
    • Young Collection + CM

      • Young GC 时会进行 GC Root初始标记
      • 老年代占用堆空间比例达到阈值时,进行 并发标记(不会 STW),由下面的 JVM 参数决定
        -XX:InitiatingHeapOccupancyPercent=percent 默认45%
        49cfadcc06014011b589bd3cb7675dd3.png
    • Mixed Collection

      • 会对 E、S、O 进行全面垃圾回收
      • 最终标记(Remark)会 STW
      • 拷贝存活(Evacuation)会 STW
        -XX:MaxGCPauseMillis=ms
        33e33b1e793e454e9592c8b75823be2d.png
        会回收那些内存占用较多的老年代。
    • Full GC

      • SerialGC

        • 新生代内存不足发生的垃圾收集 - minor gc
        • 老年代内存不足发生的垃圾收集 - full gc
      • ParallelGC

        • 新生代内存不足发生的垃圾收集 - minor gc
        • 老年代内存不足发生的垃圾收集 - full gc
      • CMS

        • 新生代内存不足发生的垃圾收集 - minor gc
        • 老年代内存不足发生 - full gc
      • G1

        • 新生代内存不足发生的垃圾收集 - minor gc
        • 老年代内存不足 - full gc
    • Young Collection 跨代引用

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

        寻找老年代中引用了新生代的对象,为了方便查找,会将老年代划分为卡表(512k)
        74ed8bea7cc14f07b833f48923ef2a04.png
        如果卡表中有对象引用了新生代,那么就称之为脏卡(dirty card)
        e87b2837d93242ac92cc5799e2fcc72b.png
        而新生代这边,会有一个 Remembered Set 记录从外部对于新生代对象的引用
        每次引用变更,都会通过 post-write barrierdirty card queue 去更新脏卡,然后由 concurrent refinement threads 更新 Remembered Set

  1. 这样可以加快新生代的垃圾回收速度
  2. * **Remark**
  3. pre-write barrier
  4. satb\_mark\_queue
  5. ![5beb9b115077472ab215f7d54733bfc4.png][]
  6. 没被remark的对象,如果没有被强引用引用,就会被回收

在并发环境中,对象C先被B引用,又被A引用,如下图:
fe4f6bb234d64195b9298c12513bdf27.png9922f15bf53542c7a993b76b63dd8622.png
因为C之前已经处理过了,所以A引用的时候不再remark,故而C就会被清理
为了防止上述情况,在对象引用发生改变时,JVM就会给对象添加上写屏障(对象引用发生改变,写屏障代码就会执行)
bd57ee0e60e24ac1b9230f1b94302ced.png
写屏障指令执行之后,就会将C加入到一个队列中,并将其置于 待处理 状态,然后进行 remark ,发现有强引用引用着C,就将其变成黑色
52483075c4344280a6e51caca67e6262.png53d89781e50c43b988981b983b325110.png


JDK 8u20 字符去重

  • 优点:节省大量内存
  • 缺点:略微多占用了 CPU 时间,新生代回收时间略微增加
  • vm options:-XX:+UseStringDeduplication

    • 将所有新分配的字符串放入一个队列
    • 当新生代回收时,G1并发检查是否有字符串重复
    • 如果它们值一样,让它们引用同一个 char[]

    不同于 str.intern()str.intern() 关注的是字符串对象
    字符串去重关注的是 char[],在 JVM 内部,使用了不同的字符串表

JDK 8u40 并发标记类卸载

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

JDK 8u60 回收巨型对象

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

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

  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent 老年代回收阈值(并发标记)
  • JDK 9 可以动态调整阈值

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

Java se官方文档

"D:\Java\jdk1.8.0_202\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC" 打印垃圾回收的虚拟机参数
b13debc9c1e2459a89102c71027bede2.png

发表评论

表情:
评论列表 (有 0 条评论,175人围观)

还没有评论,来说两句吧...

相关阅读