JVM核心考案 | 系统性学习 | 含源码分析及代码实验 | 无知的我费曼笔记(图文排版无水印)

JVM核心考案 | 系统性学习 | 含源码分析及代码实验 | 无知的我费曼笔记(图文排版无水印),第1张

无知的我正在复盘JVM。。。
笔记特点是

  • 重新整理了涉及资料的一些语言描述、排版而使用了自己更容易理解的描述。。
  • 提升了总结归纳性
  • 同样是回答了一些常见关键问题。。

文章目录
  • 虚拟机
    • JVM 内存结构
      • 内存结构-内存划分
      • 内存结构-会发生内存溢出的区域
      • 内存结构-方法区、永久代、元空间
    • JVM 内存参数
      • 堆内存,按大小设置
      • 堆内存,按比例设置
      • 元空间内存设置
      • 代码缓存内存设置
    • JVM 垃圾回收
      • 垃圾回收-标记清除法
      • 垃圾回收-标记整理法
      • 垃圾回收-标记复制法
      • 垃圾回收-GC 与分代回收算法
      • 垃圾回收-三色标记
      • 垃圾回收-并发漏标
      • 垃圾回收-垃圾回收器
    • JVM 内存溢出
      • 内存溢出-误用线程池导致的内存溢出
      • 内存溢出-查询数据量太大导致的内存溢出
      • 内存溢出-动态生成类导致的内存溢出
    • JVM 类加载
      • 类加载-三个阶段
      • 类加载-对双亲委派的误解
    • JVM 四种引用
      • 强引用
      • 软引用(SoftReference)
      • 弱引用(WeakReference)
      • 虚引用(PhantomReference)
      • 代码演示虚引用
      • 代码演示弱引用
      • 代码演示Cleaner
    • JVM finalize
      • finalize-理解
      • finalize-原理
      • finalize-缺点

虚拟机 JVM 内存结构 内存结构-内存划分

  • 执行 javac 命令编译源代码为字节码

  • 执行 java 命令

    1. 创建 JVM,调用“类加载子系统”加载 class,将类的信息存入方法区 //@类信息包括 类的继承关系、类上注解、类名、类的成员变量、方法字节码、 *** 作引用的其他符号等等
    2. 创建 main 线程,存放该线程到 JVM 虚拟机栈,开始执行 main 方法代码
    3. 如果遇到了未见过的类,会继续触发类加载过程,同样会存入方法区
    4. 当需要创建对象时,会使用堆内存来存储对象
    5. 当不再使用的对象(没有任何被调用)、内存不足时,垃圾回收器回收其内存
    6. 调用方法时,方法内的局部变量、方法参数所使用的是 JVM 虚拟机栈中的栈帧内存 //@栈帧内存 就是帧形式的空间;占用空间为1M
    7. 调用方法时,先要到方法区获得到该方法的字节码指令,由解释器将字节码指令解释为机器码执行 //这是因为cpu无法识别字节码,只能识别机器码
    8. 调用方法时,会将要执行的指令行号读到程序计数器 //这样是为了,当发生了线程切换后、想要恢复时就可以从中断的位置继续 //@线程切换的机制?当前线程不会一直占用cpu,而是间接地交给其他线程占用
    9. 对于非 java 实现的方法调用,存放在本地方法栈 ;通过本地方法接口,来调用 *** 作系统提供的本地库//@非 java 实现的方法? 需要 *** 作系统的函数来实现的,如hashcode()); 又叫做 本地方法
    10. 对于热点方法(就是调用频繁的代码),或者频繁的循环代码,由 JIT 即时编译器将这些代码编译成机器码缓存起来,提高执行性能

JVM 内部线程分类——根据线程是否私有

线程私有 //@每一个线程创建相同的独立的内容

  • 程序计数器
  • 虚拟栈

线程共享 //@每一个线程使用同一个内容

  • 方法区

其他说明

加粗字体代表了 JVM 虚拟机组件

对于 Oracle 的 Hotspot 虚拟机实现,不区分虚拟机栈和本地方法栈

内存结构-会发生内存溢出的区域
  • 出现 OutOfMemoryError 的情况
    • 堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收
    • 方法区内存耗尽 – 加载的类越来越多。比如说很多框架都会在运行期间动态产生新的类 //在实际场景中,很少见。这是因为一般设置的内存等于物理内存
    • 虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时
  • 出现 StackOverflowError 的区域
    • 虚拟机栈内部-方法调用次数过多。这是因为每次调用方法都会占用1M内存 且 栈空间有限;//比如说方法递归调用未正确结束、反序列化 json 时循环引用

不会出现内存溢出的区域 – 程序计数器。这是因为其占用内存一直不变

内存结构-方法区、永久代、元空间

方法区

  • 是 JVM 规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
  • //@“定义” 是指,实现方法可以有多种,不固定

方法实现的方式

  • 永久代是 Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)
  • 元空间是 Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间

  1. 当第一次使用某个类时,存储类信息到元空间 并 在堆中创建对应的类对象
  2. 根据类对象,创建实例,以此可以访问元空间的类信息

回收元空间中的类信息的过程

  1. 当所有实例对象不被使用时,GC就会释放他们所占用的内存。此时满足了可以释放类加载对象的条件
  2. 释放类加载对象,对应的类信息也会随之释放
JVM 内存参数 堆内存,按大小设置

解释:

  • -Xms 最小堆内存(包括新生代和老年代)
  • -Xmx 最大对内存(包括新生代和老年代)
  • 通常建议将 -Xms 与 -Xmx 设置为大小相等,即不需要保留内存,不需要从小到大增长,这样性能较好
  • -XX:NewSize 与 -XX:MaxNewSize 设置新生代的最小与最大值,但一般不建议设置,由 JVM 自己控制
  • -Xmn 设置新生代大小,相当于同时设置了 -XX:NewSize 与 -XX:MaxNewSize 并且取值相等
  • 保留是指,一开始不会占用那么多内存,随着使用内存越来越多,会逐步使用这部分保留内存。下同
堆内存,按比例设置

解释:

  • -XX:NewRatio=2:1 表示老年代占两份,新生代占一份
  • -XX:SurvivorRatio=4:1 表示新生代分成六份,伊甸园占四份,from 和 to 各占一份
元空间内存设置

解释:

  • class space 存储类的基本信息,最大值受 -XX:CompressedClassSpaceSize 控制
  • non-class space 存储除类的基本信息以外的其它信息(如方法字节码、注解等)
  • class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制

注意:

  • 这里 -XX:CompressedClassSpaceSize 这段空间还与是否开启了指针压缩有关,这里暂不深入展开,可以简单认为指针压缩默认开启
代码缓存内存设置

解释:

  • 如果 -XX:ReservedCodeCacheSize < 240m,所有优化机器代码不加区分存在一起
  • 否则,分成三个区域(图中笔误 mthod 拼写错误,少一个 e)
    • non-nmethods - JVM 自己用的代码
    • profiled nmethods - 部分优化的机器码
    • non-profiled nmethods - 完全优化的机器码

线程内存设置

官方参考文档

  • https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE
JVM 垃圾回收 垃圾回收-标记清除法

标记清除法过程

  1. 找到 GC Root 对象。即那些一定不会被回收的对象。如正执行方法内,局部变量引用的对象、静态变量引用的对象
  2. 标记阶段:沿着 GC Root 对象的引用链找,标记它直接或间接引用到的对象
  3. 清除阶段:释放未加标记的对象占用的内存

执行速度的决定因素

标记速度与存活对象线性关系

清除速度与内存大小线性关系

最大缺点

是会产生内存碎片。这是因为当需要大片连续空间时,这些内存碎片成为了阻碍。比如说申请数组空间

在实际应用中

已经没有垃圾回收器使用“标记清楚算法”了

垃圾回收-标记整理法

标记整理法过程

  1. 标记阶段、清理阶段与标记清除法类似
  2. 多了一步整理的动作。集中存活的对象到一端,没有空隙。// 这样可以避免内存碎片产生

特点

  • 标记速度与存活对象线性关系
  • 清除与整理速度与内存大小成线性关系
  • 缺点是性能上较慢
  • 在实际场景中,适合老年代的垃圾回收
垃圾回收-标记复制法

标记复制法过程

  1. 将整个内存分成两个大小相等的区域——from 和 to。其中 to 总是处于空闲,from 存储新创建的对象
  2. 标记阶段。与前面的算法类似
  3. 在找出存活对象后,会将它们从 from 复制到 to 区域 //这样的结果是 自然完成了碎片整理
  4. 复制完成后,交换 from 和 to 的位置即可

特点

  • 标记与复制速度与存活对象成线性关系
  • 缺点是会占用成倍的空间
  • 在实际场景中,老年代不适合使用该算法,但适合新生代。这是因为老年代存活对象太多
垃圾回收-GC 与分代回收算法

GC 的目的

在于实现无用对象内存自动释放,减少内存碎片、加快分配速度

回收区域

是堆内存,不包括虚拟机栈。这是因为当方法被调用完时,它会自动被回收

判断无用对象的方法

使用可达性分析算法,三色标记法标记存活对象,回收未标记对象

GC 具体的实现

称为垃圾回收器

GC 大都采用了分代回收思想

  • 理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收
  • 前者是新生代,后者是老年代。新生代采用标记复制法、老年代一般采用标记整理法
  • 根据 GC 的规模可以分成 Minor新生代 GC,Mixed混和 GC,Full所有 GC
垃圾回收-三色标记

记录对象的标记状态

  • 黑色 – 已标记
  • 灰色 – 标记中
  • 白色 – 还未标记

三色标记-过程

  1. 起始的三个对象还未处理完成,用灰色表示
  1. 该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色
  1. 依次类推
  1. 沿着引用链都标记了一遍
  1. 在最后,如图标记的白色对象,即为垃圾
垃圾回收-并发漏标

并发漏标问题-机制

当比较先进的垃圾回收器(老年代)进行并发标记时,而且 当用户线程修改了对象的引用时 //能够运行的线程有两种 垃圾回收线程、用户线程

并发漏标问题-两种解决办法

  1. Incremental Update 增量更新法。
    • 被赋值的对象就会被记录下来;在重新标记阶段再确认一遍
    • //CMS 垃圾回收器采用
  2. Snapshot At The Beginning,SATB 原始快照法。
    • 新加对象会被记录
    • 被删除引用关系的对象也被记录
    • //G1 垃圾回收器采用
垃圾回收-垃圾回收器

垃圾回收器 - Parallel GC

  • 老年代和新生代有不同的回收算法;不支持并发 *** 作

    • eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程
    • old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程
  • 注重吞吐量

垃圾回收器 - ConcurrentMarkSweep GC

  • 它是工作在 old 老年代,支持并发标记的一款回收器,采用并发清除算法
    • 并发标记时不需暂停用户线程
    • 重新标记时仍需暂停用户线程
  • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
  • **注重响应时间 **//在实际场景中,互联网注重;该垃圾回收器已经被淘汰了,有更好的选择

垃圾回收器 - G1 GC

  • 响应时间与吞吐量兼顾
  • 划分成多个区域。每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
  • 分成三个阶段:新生代回收、并发标记、混合收集
  • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC

G1 回收阶段过程 - 新生代回收

  1. 初始时,所有区域都处于空闲状态
  1. 创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象
  1. 当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程
  1. 复制完成,将之前的伊甸园内存释放
  1. 随着时间流逝,伊甸园的内存又有不足
  1. 将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代
  1. 释放伊甸园以及之前幸存区的内存

G1 回收阶段过程 - 并发标记与混合收集

  1. 当老年代占用内存超过阈值后,触发并发标记,这时无需暂停用户线程
  1. 并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来)。
  1. 混合收集阶段中,参与复制的有 eden、survivor、old,下图显示了伊甸园和幸存区的存活对象复制
  1. 下图显示了老年代和幸存区晋升的存活对象的复制
  1. 复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集
JVM 内存溢出 内存溢出-误用线程池导致的内存溢出
package day03;

import day02.LoggerUtils;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

// -Xmx64m
// 模拟短信发送超时,但这时仍有大量的任务进入队列
public class TestOomThreadPool {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        LoggerUtils.get().debug("begin...");
        while (true) {
            executor.submit(()->{
                try {
                    LoggerUtils.get().debug("send sms...");
                    TimeUnit.SECONDS.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

这是因为该方法newFixedThreadPool()使用了LinkedBlockingQueue队列作为参数,该队列无法设置队列的上限,最终不是报错,而是内存溢出

package day03;

import day02.LoggerUtils;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

// -Xmx64m
// 模拟短信发送超时,但这时仍有大量的任务进入队列
public class TestOomThreadPool {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        LoggerUtils.get().debug("begin...");
        while (true) {
            executor.submit(()->{
                try {
                    LoggerUtils.get().debug("send sms...");
                    TimeUnit.SECONDS.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

这是因为该方法newCachedThreadPool无法设置线程数的上限

内存溢出-查询数据量太大导致的内存溢出
package day03;

import org.openjdk.jol.info.ClassLayout;

import java.nio.charset.StandardCharsets;

// 演示对象的内存估算
public class TestOomTooManyObject {
    public static void main(String[] args) {
        // 对象本身内存
        long a = ClassLayout.parseInstance(new Product()).instanceSize();//注意!这里没有计算对象属性中的引用类型引用的对象
        System.out.println(a);
        // 一个字符串占用内存。注意!这里只计算了字符串对象的本身,没有计算字符串其他引用类型。比如字符串类型中的byte[]数组类型
        String name = "联想小新Air14轻薄本 英特尔酷睿i5 14英寸全面屏学生笔记本电脑(i5-1135G7 16G 512G MX450独显 高色域)银";
        long b = ClassLayout.parseInstance(name).instanceSize();
        System.out.println(b);
        String desc = "【全金属全面屏】学生商务办公,全新11代处理器,MX450独显,100%sRGB高色域,指纹识别,快充(更多好货)";
        long c = ClassLayout.parseInstance(desc).instanceSize();
        System.out.println(c);
        //计算字符串中的byte[]数组这一引用类型。
        System.out.println(16 + name.getBytes(StandardCharsets.UTF_8).length);//为什么要加上16?这是因为byte[]数组对象本身也存储了对象头+数组长度
        System.out.println(16 + desc.getBytes(StandardCharsets.UTF_8).length);
        // 一个对象估算的内存
        long avg = a + b + c + 16 + name.getBytes(StandardCharsets.UTF_8).length + 16 + desc.getBytes(StandardCharsets.UTF_8).length;
        System.out.println(avg);
        // ArrayList 24, 中Object[] 16 共 40
        System.out.println((1_000_000 * avg + 40) / 1024 / 1024 + "Mb");
    }

    static public class Product {
        private int id;
        private String name;
        private int price;
        private String desc;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getPrice() {
            return price;
        }

        public void setPrice(int price) {
            this.price = price;
        }

        public String getDesc() {
            return desc;
        }

        public void setDesc(String desc) {
            this.desc = desc;
        }
    }
}

  • 这是因为当很多个用户调用findAll方法,查询结果就会占用非常大的内存。
  • 解决办法是 不要让其调用findAll方法 || 给findAll方法加上limit条件,而不是添加查询条件
内存溢出-动态生成类导致的内存溢出
package day03;

import groovy.lang.GroovyShell;

import java.io.FileReader;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;

// -XX:MaxMetaspaceSize=24m
// 模拟不断生成类, 但类无法卸载的情况
public class TestOomTooManyClass {

//    static GroovyShell shell = new GroovyShell();

    public static void main(String[] args) {
        AtomicInteger c = new AtomicInteger();
        while (true) {
            try (FileReader reader = new FileReader("script")) {
                GroovyShell shell = new GroovyShell();
                shell.evaluate(reader);
                System.out.println(c.incrementAndGet());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 这是因为第三方类长期存在 且 是根对象,导致整个元空间无法被垃圾回收,最后元空间过多而内存溢出
  • 解决办法是不让其成为static对象而是成为局部对象
JVM 类加载 类加载-三个阶段
  1. 加载

    1. 将类的字节码载入方法区,并创建类.class 对象

    2. 如果此类的父类没有加载,先加载父类

    3. 加载是懒惰执行

  2. 链接

    1. 验证 – 验证类是否符合 Class 规范,合法性、安全性检查
    2. 准备 – 为 static 变量分配空间,设置默认值
    3. 解析 – 将常量池的符号引用解析为直接引用
  3. 初始化

    1. 静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个 方法,在初始化时被调用
    2. static final 修饰的基本类型变量赋值,在链接阶段就已完成
    3. 初始化是懒惰执行

验证手段

  • 使用 jps 查看进程号
  • 使用 jhsdb 调试,执行命令 jhsdb.exe hsdb 打开它的图形界面
    • Class Browser 可以查看当前 jvm 中加载了哪些类
    • 控制台的 universe 命令查看堆内存范围
    • 控制台的 g1regiondetails 命令查看 region 详情
    • scanoops 起始地址 结束地址 对象类型 可以根据类型查找某个区间内的对象地址
    • 控制台的 inspect 地址 指令能够查看这个地址对应的对象详情
  • 使用 javap 命令可以查看 class 字节码
类加载-对双亲委派的误解

下面面试题的回答是错误的

错在哪了?

  • 假设你自己的类加载器用双亲委派,那么优先由启动类加载器加载真正的 java.lang.System,自然不会加载假冒的

  • 假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的 java.lang.System 时,它需要先加载父类 java.lang.Object,而你没有用委派,找不到 java.lang.Object 所以加载会失败

  • 以上也仅仅是假设。事实上 *** 作你就会发现,自定义类加载器加载以 java. 打头的类时,会抛安全异常,在 jdk9 以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了

JVM 四种引用 强引用
  1. 普通变量赋值即为强引用,如 A a = new A();

  2. 通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收

软引用(SoftReference)
  1. 例如:SoftReference a = new SoftReference(new A());

  2. 如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象

  3. 软引用自身需要配合引用队列来释放

  4. 典型例子是反射数据

弱引用(WeakReference)
  1. 例如:WeakReference a = new WeakReference(new A());

  2. 如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象

  3. 弱引用自身需要配合引用队列来释放

  4. 典型例子是 ThreadLocalMap 中的 Entry 对象

虚引用(PhantomReference)
  1. 例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);

  2. 当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理 //必须配合引用队列一起使用

  3. 典型例子是 Cleaner 释放 DirectByteBuffer 关联的直接内存

代码说明

  • day03.reference.TestPhantomReference - 演示虚引用的基本用法
  • day03.reference.TestWeakReference - 模拟 ThreadLocalMap, 采用引用队列释放 e
代码演示虚引用
package day03.reference;

import day02.LoggerUtils;

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

public class TestPhantomReference {
    public static void main(String[] args) throws IOException, InterruptedException {
        ReferenceQueue<String> queue = new ReferenceQueue<>();// 创建引用队列
        List<MyResource> list = new ArrayList<>();
        list.add(new MyResource(new String("a"), queue));
        list.add(new MyResource("b", queue));
        list.add(new MyResource(new String("c"), queue));

        System.gc(); // 触发垃圾回收,同时被垃圾回收的虚引用对象会存放到队列中去,为了知道哪些虚引用对象被垃圾回收了
        Thread.sleep(100);
        Object ref;
        while ((ref = queue.poll()) != null) {//如果队列空
            if (ref instanceof MyResource resource) {
                resource.clean();
            }
        }
    }

    static class MyResource extends PhantomReference<String> {//继承虚引用
        public MyResource(String referent, ReferenceQueue<? super String> q) {//引用对象;传入引用队列.
            super(referent, q);
        }
        // 释放外部资源的方法。除了垃圾回收java对象外,一般还要这个
        public void clean() {
            LoggerUtils.get().debug("clean");
        }
    }
}

结果是 字符“b”没有被清理掉。这是因为该字符还被放入了字符串池中

代码演示弱引用
package day03.reference;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

public class TestWeakReference {

    public static void main(String[] args) {
        MyWeakMap map = new MyWeakMap();
        map.put(0, new String("a"), "1");
        map.put(1, "b", "2");
        map.put(2, new String("c"), "3");
        map.put(3, new String("d"), "4");
        System.out.println(map);

        System.gc();
        System.out.println(map.get("a"));
        System.out.println(map.get("b"));
        System.out.println(map.get("c"));
        System.out.println(map.get("d"));
        System.out.println(map);
        map.clean();
        System.out.println(map);
    }

    // 模拟 ThreadLocalMap 的内存泄漏问题以及一种解决方法
    static class MyWeakMap {
        static ReferenceQueue<Object> queue = new ReferenceQueue<>();
        static class Entry extends WeakReference<String> {//继承弱引用
            String value;

            public Entry(String key, String value) {
                super(key, queue);//传入参数被弱引用 且 当垃圾回收时,key会存入队列中
                this.value = value;//值是强引用
            }
        }
        public void clean() {
            Object ref;
            while ((ref = queue.poll()) != null) {
                System.out.println(ref);
                for (int i = 0; i < table.length; i++) {
                    if(table[i] == ref) {
                        table[i] = null;
                    }
                }
            }
        }

        Entry[] table = new Entry[4];

        public void put(int index, String key, String value) {
            table[index] = new Entry(key, value);
        }

        public String get(String key) {
            for (Entry entry : table) {
                if (entry != null) {
                    String k = entry.get();
                    if (k != null && k.equals(key)) {
                        return entry.value;
                    }
                }
            }
            return null;
        }

        @Override//打印键值状态
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("[");
            for (Entry entry : table) {
                if (entry != null) {
                    String k = entry.get();
                    sb.append(k).append(":").append(entry.value).append(",");
                }
            }
            if (sb.length() > 1) {
                sb.deleteCharAt(sb.length() - 1);
            }
            sb.append("]");
            return sb.toString();
        }
    }
}

结果是“b”没有被回收

代码演示Cleaner
package day03.reference;

import day02.LoggerUtils;

import java.io.IOException;
import java.lang.ref.Cleaner;

// 前面讲的弱、虚引用配合引用队列,目的都是为了找到哪些 java 对象被回收,从而进行对它们关联的资源进行进一步清理
// 为了简化 api 难度,从 java 9 开始引入了 Cleaner 对象
public class TestCleaner1 {
    public static void main(String[] args) throws IOException {
        Cleaner cleaner = Cleaner.create();

        cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 1"));//当第一个参数对象被回收了,则打印后面的内容
        cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 2"));
        cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 3"));
        MyResource obj = new MyResource();
        cleaner.register(obj, ()-> LoggerUtils.get().debug("clean 4"));
        cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 5"));
        cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 6"));

        System.gc();
        System.in.read();//如果不用来暂停,就不会有输出结果。这是因为那些线程是守护线程,当主线程没有代码执行而结束时,守护线程也就结束了
    }

    static class MyResource {

    }
}

结果是第4个没有被回收,也就没有执行后面参数中的代码

下面使用的是java自己用的Cleaner来实现的

package day03.reference;

import day02.LoggerUtils;

import java.io.IOException;

import jdk.internal.ref.Cleaner;

public class TestCleaner2 {
    public static void main(String[] args) throws IOException {
        Cleaner cleaner1 = Cleaner.create(new MyResource(), ()-> LoggerUtils.get().debug("clean 1"));
        Cleaner cleaner2 = Cleaner.create(new MyResource(), ()-> LoggerUtils.get().debug("clean 2"));
        Cleaner cleaner3 = Cleaner.create(new MyResource(), ()-> LoggerUtils.get().debug("clean 3"));
        Cleaner cleaner4 = Cleaner.create(new MyResource(), ()-> LoggerUtils.get().debug("clean 4"));

        System.gc();
        System.in.read();
    }

    static class MyResource {

    }
}

JVM finalize finalize-理解
  • 它是 Object 中的一个方法,如果子类重写它,垃圾回收时此方法会被调用,可以在其中进行资源释放和清理工作
  • 将资源释放和清理放在 finalize 方法中非常不好,非常影响性能,严重时甚至会引起 OOM,从 Java9 开始就被标注为 @Deprecated,不建议被使用了

举例说明 代码实现finalize重写

package day03.reference;

import day02.LoggerUtils;

import java.io.IOException;

public class TestFinalize {
    static class Dog {
        private String name;

        public Dog(String name) {
            this.name = name;
        }

        @Override
        protected void finalize() throws Throwable {
            LoggerUtils.get().debug("{}被干掉了?", this.name);
            int i = 1 / 0;
        }
    }

    public static void main(String[] args) throws IOException {
        new Dog("大傻");
        new Dog("二哈");
        new Dog("三笨");
        System.gc();
        System.in.read();
    }

}

引出问题

  • 从表面上能看出来 finalize 方法的调用次序并不能保证
  • 日志中的 Finalizer 表示输出日志的线程名称,从这我们看出是这个叫做 Finalizer 的线程调用的 finalize 方法
  • 你不能注释掉 System.in.read(),否则会发现(绝大概率)并不会有任何输出结果了,从这我们看出finalize 中的代码并不能保证被执行
  • 如果将 finalize 中的代码出现异常,会发现根本没有异常输出
  • 还有个疑问,垃圾回收时就会立刻调用 finalize 方法吗?
finalize-原理
  1. 对 finalize 方法进行处理的核心逻辑位于 java.lang.ref.Finalizer 类中,它包含了名为 unfinalized 的静态变量(双向链表结构),Finalizer 也可被视为另一种引用对象(地位与软、弱、虚相当,只是不对外,无法直接使用)
  2. 当重写了 finalize 方法的对象,在构造方法调用之时,JVM 都会将其包装成一个 Finalizer 对象,并加入 unfinalized 链表中

  1. Finalizer 类中还有另一个重要的静态变量,即 ReferenceQueue 引用队列,刚开始它是空的。当狗对象可以被当作垃圾回收时,就会把这些狗对象对应的 Finalizer 对象加入此引用队列
  2. 但此时 Dog 对象还没法被立刻回收,因为 unfinalized -> Finalizer 这一引用链还在引用它
  3. FinalizerThread 线程会从 ReferenceQueue 中逐一取出每个 Finalizer 对象,把它们从unfianlized链表断开并真正调用 finallize 方法

  1. 由于整个 Finalizer 对象已经从 unfinalized 链表中断开,这样没谁能引用到它和狗对象,所以下次 gc 时就被回收了
finalize-缺点

无法保证资源释放

FinalizerThread 是守护线程,代码很有可能没来得及执行完,线程就结束了,

无法判断是否发生错误

执行 finalize 方法时,会吞掉任意异常(Throwable)。这是因为在源码中try…catch的catch没有任何代码

内存释放不及时

重写了 finalize 方法的对象在第一次被 gc 时,并不能及时释放它占用的内存,因为要等着 FinalizerThread 调用完 finalize,把它从 unfinalized 队列移除后(两个的内部结构都是队列,都是串行执行的,释放连接类的资源不快),第二次 gc 时才能真正释放内存

这就意味着不能即使释放内存,对象不及时释放就会逐渐移入老年代,导致老年代垃圾积累过多,从而容易 full gc,full gc后释放速度如果荏苒跟不上创建新对象的速度,会导致OOM

常见误区

有的文章提到【Finalizer 线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐】这个显然是错误

FinalizerThread 的优先级为8比普通线程更高,原因应该是 finalize 串行执行慢等原因综合导致

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/915917.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存