概叙
在JDK9中新增了Cleaner类,该类的作用是用于替代finalize方法,更有效地释放资源并避免内存泄漏。
在JEP260提案中,封装了大部分Sun
包内部的API之余,还引入了一些新的API,其中就包含着Cleaner
这个工具类。Cleaner
承担着替换finalize
方法的作用,为了解决finalize
方法的性能问题、安全问题以及不可靠。
在JDK1.2中,就已经有Cleaner这个类的内部实现了,不过是在sun
包中实现的。由于是内部类不建议在生产代码中直接使用。不过sun包下的Cleaner类和lang
包下的Cleanr类的功能是类似的。
在Java中,Cleaner
是java.lang.ref.Cleaner
类的一个实例,它用于在垃圾收集器确定对象不再可达时执行清理动作。通常,这在处理本地资源(如文件句柄或数据库连接)时非常有用,确保资源在对象不再使用时能够得到释放。
使用Cleaner
的典型场景是结合使用java.lang.ref.PhantomReference
和java.lang.ref.ReferenceQueue
。你可以创建一个PhantomReference
到你想要跟踪的对象,并在创建时指定一个ReferenceQueue
和一个Cleaner
。当PhantomReference
被入队时,Cleaner
会执行一个清理方法,你可以在这个方法中释放资源。
Cleaner类
注意:在JDK1.9以上版本可使用
在Java程序中提供有GC的垃圾回收机制,如果发现堆内存不足时一定要进行垃圾回收以释放内存空间,但如果某些对象在回收前需要做一些处理,可以通过覆写Object类中的finalize()方法来实现这种回收前的处理。
-
finalize()方法的定义:
@Deprecated(since="9")
protected void finalize() throws Throwable { }
finalize()方法实际上从JDK1.0时就开始提供,但一直到JDK1.9后才发现此方法成为了不推荐使用的方法,同时这个方法上所抛出的一个Throwabke类型的异常(Erroe和Exception),在对象回收时可能会出现各种问题,但不影响回收!
public class Member {
public Member() {
System.out.println("诞生!");
}
@Override
protected void finalize() throws Throwable { //可能抛错误,也可能抛异常
System.out.println("回收!");
throw new Exception("我真的还想在活500年");
}
}
public class Demo {
public static void main(String[] args) {
Member mem=new Member(); //实例化对象
mem=null; //垃圾,不被引用
System.gc();//手动进行gc操作
System.out.println("太阳照常升起,一代更比一代强");
}
}
执行结果:
诞生!
太阳照常升起,一代更比一代强
回收
回收对象前要先finalize(),降低了内存回收的效率,而且它不能保证被及时执行,或者可能造成该对象的在次复活。
jdk1.9后出现新的替代者:java.lang.ref.Cleaner类。此种清理方式会启动一个新的清理线程,并且基于AutoCloseable接口实现资源释放。
Cleaner 类的主要方法和属性
Cleaner类中,一共就三个外部方法,一个简单的工具类。
其中,在调用create
方法时,就会新建一条线程,用于监听目标对象是否已经被回收。监听的逻辑则是用到了虚引用以及引用队列,在虚引用中,要是一个对象变成不可达后,在GC前会将该对象的虚引用放入引用队列中。详细的步骤以及逻辑可以看这篇文章【Java引用规范】虚引用以及引用队列。
如何创建和使用 Cleaner 对象
- 使用
Cleaner.create()
创建Cleaner对象。 - 调用
cleaner.register()
方法,传入监听的对象以及回收后要执行的逻辑。其中,逻辑中不能带有监听对象的引用,否则对象将永远无法被回收。
Cleaner 优点和局限性
Cleaner 类相比 Finalizer 和 PhantomReference 的优势
Cleaner 类和手动调用Close方法的区别
Cleaner 类的潜在问题和限制
- 每注册一个Cleaner类,就会新开一条线程用于监听目标对象是否已经进入到引用队列。直到目标对象被回收后,新线程才结束。
- Cleaner回收时间点无法控制。
- 不能替换所有的资源释放,必要时还是需要显式执行Close方法。
- 无法控制传入的回收执行逻辑,可能导致性能问题。
应用场景
在JDK1.2中,就已经有这个类的内部实现了,不过是在sun包中实现的。由于是内部类不建议在生产代码中直接使用。不过sun包下的Cleaner类和lang包下的Cleanr类的功能是类似的。
Cleaner在JDK中最典型的实现就是堆外内存的回收。我们申请到一个堆外内存后,是无法手动将该堆外内存进行显示的回收的,只能等待JVM来自动回收该内存。
其中,自动回收的操作就是使用到了Cleaner工具类,在DirectByteBuffer的构造方法中,申请到堆外内存后,就会将堆外内存地址、申请容量以及实际内存大小传入到Deallocator类中进行空间的回收。
Deallocator类集成了Runnable接口,在run方法中就会将对应地址的堆外内存回收。
示例1:传统的对象回收
- li不进行手动回收
class Book{
//无参构造
public Book(){
System.out.println("【构造】用心编写了一本优秀的原创技术图书!");
}
@Override
protected void finalize() throws Throwable {
System.out.println("【析构】图书使用完毕,可以销毁!");
}
}
public class Application {
public static void main(String[] args) {
Book book = new Book();//创建实例化对象
book = null;//断开堆内存的指向,变为垃圾空间
}
}
运行结果:
【构造】用心编写了一本优秀的原创技术图书!
- 调用gc()进行手动回收
如果不进行手动的gc()回收,则需要等待自动回收,自动回收的时间是不可控的!!
class Book{
//无参构造
public Book(){
System.out.println("【构造】用心编写了一本优秀的原创技术图书!");
}
@Override
protected void finalize() throws Throwable {
System.out.println("【析构】图书使用完毕,可以销毁!");
}
}
public class Application {
public static void main(String[] args) {
Book book = new Book();//创建实例化对象
book = null;//断开堆内存的指向,变为垃圾空间
//如果不进行手动的gc()回收,则需要等待自动回收,自动回收的时间是不可控的!!
System.gc();//进行垃圾回收
}
}
运行结果如下:
【构造】用心编写了一本优秀的原创技术图书!
【析构】图书使用完毕,可以销毁!
- 手动抛出异常
finalize()方法代码修改如下:(其他代码不变)
protected void finalize() throws Throwable {
System.out.println("【析构】图书使用完毕,可以销毁!");
throw new Exception("这本图书还有用,不能销毁!!");//手动抛出异常
}
运行结果如下:
【构造】用心编写了一本优秀的原创技术图书!
【析构】图书使用完毕,可以销毁!
以上的这种程序做法是在JDK1.9以前提供的处理形式,但这样的做法一直以来都存在严重问题。
如果在finalize()里面出现一些线程的死锁操作,那么就可能会造成垃圾回收的失败,同时也会产生严重的线程阻塞问题
解决方法:在JDK1.9之后,启动了一个专属的回收线程----Cleaner类
示例2:cleanable.clean();//释放时进行垃圾清除
import sun.misc.Cleaner;
class Book implements Runnable{ //设计一个回收线程
//无参构造
public Book(){
System.out.println("【构造】用心编写了一本优秀的原创技术图书!");
}
public void read(){
System.out.println("【读书】认真学习!");
}
@Override
public void run() { //真正的回收由线程来完成!!!
System.out.println("【析构】图书使用完毕,可以销毁!");
}
}
class BookCleaner implements AutoCloseable{ //必须实现AutoCloseable接口
private static final Cleaner cleaner = Cleaner.create();//创建一个回收对象
private Cleaner.Cleanable cleanable;
public BookCleaner(Book book){
this.cleanable = cleaner.register(this,book);//注册一个回收线程
}
@Override
public void close() throws Exception {
this.cleanable.clean();//释放时进行垃圾清除
}
}
public class Application {
public static void main(String[] args) {
Book book = new Book();
try(BookCleaner bc = new BookCleaner()){
book.read();//可以在中间进行一些对象的处理操作
}catch (Exception e){}
}
}
运行结果如下:
【构造】用心编写了一本优秀的原创技术图书!
【读书】认真学习!
【析构】图书使用完毕,可以销毁!
package cn.mldn.demo;
import java.lang.ref.Cleaner;
class Member implements Runnable {
public Member() {
System.out.println("诞生!");
}
@Override
public void run() { // 清除线程
System.out.println("回收!");
}
}
class MemberCleaning implements AutoCloseable { // 实现清除的处理
private static final Cleaner cleaner = Cleaner.create() ; // 创建一个清除处理
private Cleaner.Cleanable cleanable ;
public MemberCleaning(Member member) { // 注册待清除对象
this.cleanable = cleaner.register(this, member) ; // 注册使用的对象
}
@Override
public void close() throws Exception {// 自动关闭并释放
this.cleanable.clean(); // 启动清理线程
}
}
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Member mem = new Member() ;// 实例化对象
System.gc(); // 手工进行GC调用
try (MemberCleaning mc = new MemberCleaning(mem)){/ 中间可以执行一些相关的代码
} catch (Exception e) {}
System.out.println("太阳照常升起,地球照样转动,一代更比一代强!");// 不受影响继续执行
}
}
执行结果
诞生!
太阳照常升起,一代更比一代强
回收
新版本要求启动一个线程单独清除回收,防止延迟处理,保证性能,但是也不能保证垃圾被及时回收。
最好的方式是用完即使用try-with-resource机制显示释放或者放入资源池重用。
图解-java对象的生命周期
经过以上的分析基本已经清楚对象的创建以及回收处理的操作,以下是对Java中对象的生命周期流程
- 创建阶段:每当使用关键字new就表示要开辟新的堆内存空间,同时每一个新的对象实例化时都需要去执行类中的构造方法,构造方法的目的是为了类中成员属性的初始化
- 应用阶段:利用指定的对象名称可以直接进行类之中的方法的调用处理
- 不可见阶段:如果现实某一个方法内部有一个对象,则该方法执行完毕后该对象将不再使用
- 不可达阶段:某一块堆内存已经不再有任何的栈内存所指向,那么这块空间将成为垃圾空间
- 收集阶段:JVM会自动的进行此块垃圾空间的标记,标记之后将准备通过GC回收释放,JDK1.8及以前的版本均使用finalize()方法,JKD1.9及以后的版本推荐使用CLeaner来完成
- 释放阶段:JVM重新回收垃圾的堆内存空间,供后续新对象使用