无知的我正在复盘JVM。。。
笔记特点是
- 重新整理了涉及资料的一些语言描述、排版而使用了自己更容易理解的描述。。
- 提升了总结归纳性
- 同样是回答了一些常见关键问题。。
- 虚拟机
- JVM 内存结构
- 内存结构-内存划分
- 内存结构-会发生内存溢出的区域
- 内存结构-方法区、永久代、元空间
- JVM 内存参数
- 堆内存,按大小设置
- 堆内存,按比例设置
- 元空间内存设置
- 代码缓存内存设置
- JVM 垃圾回收
- 垃圾回收-标记清除法
- 垃圾回收-标记整理法
- 垃圾回收-标记复制法
- 垃圾回收-GC 与分代回收算法
- 垃圾回收-三色标记
- 垃圾回收-并发漏标
- 垃圾回收-垃圾回收器
- JVM 内存溢出
- 内存溢出-误用线程池导致的内存溢出
- 内存溢出-查询数据量太大导致的内存溢出
- 内存溢出-动态生成类导致的内存溢出
- JVM 类加载
- 类加载-三个阶段
- 类加载-对双亲委派的误解
- JVM 四种引用
- 强引用
- 软引用(SoftReference)
- 弱引用(WeakReference)
- 虚引用(PhantomReference)
- 代码演示虚引用
- 代码演示弱引用
- 代码演示Cleaner
- JVM finalize
- finalize-理解
- finalize-原理
- finalize-缺点
-
执行 javac 命令编译源代码为字节码
-
执行 java 命令
- 创建 JVM,调用“类加载子系统”加载 class,将类的信息存入方法区 //@类信息包括 类的继承关系、类上注解、类名、类的成员变量、方法字节码、 *** 作引用的其他符号等等
- 创建 main 线程,存放该线程到 JVM 虚拟机栈,开始执行 main 方法代码
- 如果遇到了未见过的类,会继续触发类加载过程,同样会存入方法区
- 当需要创建对象时,会使用堆内存来存储对象
- 当不再使用的对象(没有任何被调用)、内存不足时,垃圾回收器回收其内存
- 调用方法时,方法内的局部变量、方法参数所使用的是 JVM 虚拟机栈中的栈帧内存 //@栈帧内存 就是帧形式的空间;占用空间为1M
- 调用方法时,先要到方法区获得到该方法的字节码指令,由解释器将字节码指令解释为机器码执行 //这是因为cpu无法识别字节码,只能识别机器码
- 调用方法时,会将要执行的指令行号读到程序计数器 //这样是为了,当发生了线程切换后、想要恢复时就可以从中断的位置继续 //@线程切换的机制?当前线程不会一直占用cpu,而是间接地交给其他线程占用
- 对于非 java 实现的方法调用,存放在本地方法栈 ;通过本地方法接口,来调用 *** 作系统提供的本地库//@非 java 实现的方法? 需要 *** 作系统的函数来实现的,如hashcode()); 又叫做 本地方法
- 对于热点方法(就是调用频繁的代码),或者频繁的循环代码,由 JIT 即时编译器将这些代码编译成机器码缓存起来,提高执行性能
JVM 内部线程分类——根据线程是否私有
线程私有 //@每一个线程创建相同的独立的内容
- 程序计数器
- 虚拟栈
线程共享 //@每一个线程使用同一个内容
- 堆
- 方法区
内存结构-会发生内存溢出的区域其他说明
加粗字体代表了 JVM 虚拟机组件
对于 Oracle 的 Hotspot 虚拟机实现,不区分虚拟机栈和本地方法栈
- 出现 OutOfMemoryError 的情况
- 堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收
- 方法区内存耗尽 – 加载的类越来越多。比如说很多框架都会在运行期间动态产生新的类 //在实际场景中,很少见。这是因为一般设置的内存等于物理内存
- 虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时
- 出现 StackOverflowError 的区域
- 虚拟机栈内部-方法调用次数过多。这是因为每次调用方法都会占用1M内存 且 栈空间有限;//比如说方法递归调用未正确结束、反序列化 json 时循环引用
内存结构-方法区、永久代、元空间不会出现内存溢出的区域 – 程序计数器。这是因为其占用内存一直不变
方法区
- 是 JVM 规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
- //@“定义” 是指,实现方法可以有多种,不固定
方法实现的方式
- 永久代是 Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)
- 元空间是 Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间
- 当第一次使用某个类时,存储类信息到元空间 并 在堆中创建对应的类对象
- 根据类对象,创建实例,以此可以访问元空间的类信息
回收元空间中的类信息的过程
- 当所有实例对象不被使用时,GC就会释放他们所占用的内存。此时满足了可以释放类加载对象的条件
- 释放类加载对象,对应的类信息也会随之释放
解释:
- -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 - 完全优化的机器码
线程内存设置
JVM 垃圾回收 垃圾回收-标记清除法官方参考文档
- https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE
标记清除法过程
- 找到 GC Root 对象。即那些一定不会被回收的对象。如正执行方法内,局部变量引用的对象、静态变量引用的对象
- 标记阶段:沿着 GC Root 对象的引用链找,标记它直接或间接引用到的对象
- 清除阶段:释放未加标记的对象占用的内存
执行速度的决定因素
标记速度与存活对象线性关系
清除速度与内存大小线性关系
最大缺点
是会产生内存碎片。这是因为当需要大片连续空间时,这些内存碎片成为了阻碍。比如说申请数组空间
在实际应用中
已经没有垃圾回收器使用“标记清楚算法”了
垃圾回收-标记整理法标记整理法过程
- 标记阶段、清理阶段与标记清除法类似
- 多了一步整理的动作。集中存活的对象到一端,没有空隙。// 这样可以避免内存碎片产生
特点
- 标记速度与存活对象线性关系
- 清除与整理速度与内存大小成线性关系
- 缺点是性能上较慢
- 在实际场景中,适合老年代的垃圾回收
标记复制法过程
- 将整个内存分成两个大小相等的区域——from 和 to。其中 to 总是处于空闲,from 存储新创建的对象
- 标记阶段。与前面的算法类似
- 在找出存活对象后,会将它们从 from 复制到 to 区域 //这样的结果是 自然完成了碎片整理
- 复制完成后,交换 from 和 to 的位置即可
特点
- 标记与复制速度与存活对象成线性关系
- 缺点是会占用成倍的空间
- 在实际场景中,老年代不适合使用该算法,但适合新生代。这是因为老年代存活对象太多
GC 的目的
在于实现无用对象内存自动释放,减少内存碎片、加快分配速度
回收区域
是堆内存,不包括虚拟机栈。这是因为当方法被调用完时,它会自动被回收
判断无用对象的方法
使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
GC 具体的实现
称为垃圾回收器
GC 大都采用了分代回收思想
- 理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收
- 前者是新生代,后者是老年代。新生代采用标记复制法、老年代一般采用标记整理法
- 根据 GC 的规模可以分成 Minor新生代 GC,Mixed混和 GC,Full所有 GC
记录对象的标记状态
- 黑色 – 已标记
- 灰色 – 标记中
- 白色 – 还未标记
三色标记-过程
- 起始的三个对象还未处理完成,用灰色表示
- 该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色
- 依次类推
- 沿着引用链都标记了一遍
- 在最后,如图标记的白色对象,即为垃圾
并发漏标问题-机制
当比较先进的垃圾回收器(老年代)进行并发标记时,而且 当用户线程修改了对象的引用时 //能够运行的线程有两种 垃圾回收线程、用户线程
并发漏标问题-两种解决办法
- Incremental Update 增量更新法。
- 被赋值的对象就会被记录下来;在重新标记阶段再确认一遍
- //CMS 垃圾回收器采用
- 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 回收阶段过程 - 新生代回收
- 初始时,所有区域都处于空闲状态
- 创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象
- 当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程
- 复制完成,将之前的伊甸园内存释放
- 随着时间流逝,伊甸园的内存又有不足
- 将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代
- 释放伊甸园以及之前幸存区的内存
G1 回收阶段过程 - 并发标记与混合收集
- 当老年代占用内存超过阈值后,触发并发标记,这时无需暂停用户线程
- 并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来)。
- 混合收集阶段中,参与复制的有 eden、survivor、old,下图显示了伊甸园和幸存区的存活对象复制
- 下图显示了老年代和幸存区晋升的存活对象的复制
- 复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集
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对象而是成为局部对象
-
加载
-
将类的字节码载入方法区,并创建类.class 对象
-
如果此类的父类没有加载,先加载父类
-
加载是懒惰执行
-
-
链接
- 验证 – 验证类是否符合 Class 规范,合法性、安全性检查
- 准备 – 为 static 变量分配空间,设置默认值
- 解析 – 将常量池的符号引用解析为直接引用
-
初始化
- 静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个
方法,在初始化时被调用 - static final 修饰的基本类型变量赋值,在链接阶段就已完成
- 初始化是懒惰执行
- 静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个
类加载-对双亲委派的误解验证手段
- 使用 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 以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了
-
普通变量赋值即为强引用,如 A a = new A();
-
通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收
-
例如:SoftReference a = new SoftReference(new A());
-
如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象
-
软引用自身需要配合引用队列来释放
-
典型例子是反射数据
-
例如:WeakReference a = new WeakReference(new A());
-
如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象
-
弱引用自身需要配合引用队列来释放
-
典型例子是 ThreadLocalMap 中的 Entry 对象
-
例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);
-
当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理 //必须配合引用队列一起使用
-
典型例子是 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”没有被回收
代码演示Cleanerpackage 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 方法进行处理的核心逻辑位于 java.lang.ref.Finalizer 类中,它包含了名为 unfinalized 的静态变量(双向链表结构),Finalizer 也可被视为另一种引用对象(地位与软、弱、虚相当,只是不对外,无法直接使用)
- 当重写了 finalize 方法的对象,在构造方法调用之时,JVM 都会将其包装成一个 Finalizer 对象,并加入 unfinalized 链表中
- Finalizer 类中还有另一个重要的静态变量,即 ReferenceQueue 引用队列,刚开始它是空的。当狗对象可以被当作垃圾回收时,就会把这些狗对象对应的 Finalizer 对象加入此引用队列
- 但此时 Dog 对象还没法被立刻回收,因为 unfinalized -> Finalizer 这一引用链还在引用它
- FinalizerThread 线程会从 ReferenceQueue 中逐一取出每个 Finalizer 对象,把它们从unfianlized链表断开并真正调用 finallize 方法
- 由于整个 Finalizer 对象已经从 unfinalized 链表中断开,这样没谁能引用到它和狗对象,所以下次 gc 时就被回收了
无法保证资源释放
FinalizerThread 是守护线程,代码很有可能没来得及执行完,线程就结束了,
无法判断是否发生错误
执行 finalize 方法时,会吞掉任意异常(Throwable)。这是因为在源码中try…catch的catch没有任何代码
内存释放不及时
重写了 finalize 方法的对象在第一次被 gc 时,并不能及时释放它占用的内存,因为要等着 FinalizerThread 调用完 finalize,把它从 unfinalized 队列移除后(两个的内部结构都是队列,都是串行执行的,释放连接类的资源不快),第二次 gc 时才能真正释放内存
这就意味着不能即使释放内存,对象不及时释放就会逐渐移入老年代,导致老年代垃圾积累过多,从而容易 full gc,full gc后释放速度如果荏苒跟不上创建新对象的速度,会导致OOM
常见误区
有的文章提到【Finalizer 线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐】这个显然是错误
FinalizerThread 的优先级为8比普通线程更高,原因应该是 finalize 串行执行慢等原因综合导致
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)