终结 finalize() 和对象引用
一、finalize() 方法
1. 为什么要有 finalize() 方法?
假定你的对象(并非使用new)获得了一块“特殊”的内存区域,由于垃圾回收器只知道释放那些经由 new 分配的内存,所以他不知道该如何释放该对象的这块“特殊”内存,为了应对这种情况,java 允许在类中定义一个 finalize() 的方法。
    protected void finalize(){
    }
2. finalize()方法在何时调用?
一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其 finalize() 方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
finalize() 方法是对象逃脱死亡命运的最后一次机会,如果对象要在 finalize() 方法中成功拯救自己 — 只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this 关键字)赋值给某个类变量或者对象的成员变量,那么它将被移除出”即将回收“的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
3. finalize()的局限性?
finalize() 方法不是 C/C++ 的析构函数,而是 Java 刚诞生时为了使 C/C++ 程序员更容易接受它所做出的一个妥协。
一个对象的 finalize() 方法最多只会被系统自动调用一次。
finalize() 方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,虚拟机调用 finalize() 方法甚至不能保证 finalize() 的逻辑执行完毕。
finalize() 方法内做普通的清除工作是不合适的。 如果一定要进行回收动作,最好自己写一个回收方法 dispose() 方法。应当注意的是如果子类重写了父类的 dispose() 方法,当进行清除动作时,应该先清除子类的,再清除父类的,原因在于:可能子类存在对父类的方法调用。
建议大家可以完全忘掉 Java 语言中有这个方法的存在。
二、对象存活算法
如果 JVM 并未面临内存耗尽的情形,它是不会浪费时间在回收垃圾上的,而进行垃圾回收之前首先要进行判定的就是 —— 对象是否存活,下面介绍几种对象存活算法:
1. 引用计数算法(Reference Counting)
给每个对象都添加有一个引用计数器,当有引用连接至对象时,引用计数加 1;当引用离开作用域或被置为 null 时,引用计数器减1。垃圾回收器会在含有全部对象的列表上,当发现某个对象的引用计数为 0 时,就释放其占有的空间。
客观的说,引用计数算法实现简单,判断效率也很高,在大部分情况下都是一个不错的算法。但是这种算法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该被回收,但引用计数却不为0的情况”。
目前主流的 Java 虚拟机都没有先用引用计数算法来管理内存。
2. 可达性分析算法(Reachability Analysis)
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则判断此对象是不可用的。在 Java 语言中,可作为 GC Roots 的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
主流的商用语言(Java、C#等)都是通过可达性分析算法来判断对象是否存活的。
三、Java 引用类型
在 JDK 1.2 以前,Java 中的引用定义很传统:如果 reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。在这种定义下,一个对象只有引用和没有被引用两个状态。
在 JDK 1.2 之后,Java 将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐变弱。在这种定义下,一个对象的引用状态被大大丰富,虚拟机也根据对象引用状态确定是否对对象进行回收。
强引用
类似 “Object obj = new Object()” 这类的引用,只要强引用还存在,垃圾收集器永远不会掉被引用的对象。
软引用
描述一些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把可回收的软引用对象进行二次回收,如果回收后还没有足够的内存,才会抛出内存溢出异常。
Java 中提供了 SoftReference 类来实现软引用。
软引用还可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收(即对象的强引用被回收,该对象变成了软可及对象),Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。在软引用还没被垃圾回收之前,通过软引用的 get() 方法可以重新获得强引用,相反,如果软引用也被垃圾回收了,该软引用的 get() 方法就会返回 null。
tips:我们可以通过检查 ReferenceQueue 元素的 get() 方法是否返回 null,来判断该软引用是否已经被回收。
弱引用
也是用来描述非必需对象的,可回收的弱引用对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,可回收的弱引用对象都会被回收。
Java 中提供了 WeakReference 类来实现弱引用。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。同样,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。在弱引用还没被垃圾回收之前,通过弱引用的 get() 方法可以重新获得强引用,相反,如果弱引用也被垃圾回收了,该软引用的 get() 方法就会返回 null。
虚引用
最弱的一种引用关系,一个对象是否有虚引用,完全不会对其生存时间构成影响,也无法通过一个虚引用来取得一个对象实例,为一个对象设置虚引用关联的唯一目的就是能够在这个对象被收集器回收时收到一个系统通知。
Java 提供了 PhantomReference 类来实现虚引用。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中,程序员以此来追踪一个对象的回收情况。
四、引用实践
1. 通过软可及对象重获方法实现Java对象的高速缓存
这里的高速缓存是指:当 Java 对象的强引用已经被垃圾回收或被设置成 null 值,但是该对象的软引用还没被回收,这时可以通过软引用的 get() 方法重新获取强引用,避免重新实例化对象。
1.1 Employee
public class Employee {
    private String id;// 雇员的标识号码
    private String name;// 雇员姓名
    private String department;// 该雇员所在部门
    private String Phone;// 该雇员联系电话
    private int salary;// 该雇员薪资
    private String origin;// 该雇员信息的来源
    // 构造方法
    public Employee(String id) {
        this.id = id;
        getDataFromlnfoCenter();
    }
    // 到数据库中取得雇员信息
    private void getDataFromlnfoCenter() {
        // 和数据库建立连接井查询该雇员的信息,将查询结果赋值
        // 给name,department,plone,salary等变量
        // 同时将origin赋值为"From DataBase"
        this.name = "JMCui";
        this.Phone = "15980292662";
        this.salary = 5000;
        this.origin = "From DataBase";
    }
}
1.2 EmployeeCache
public class EmployeeCache {
    // 一个 Cache 实例
    private static volatile EmployeeCache cache;
    // 用于 Cache 内容的存储
    private Hashtable<String, EmployeeRef> employeeRefs;
    // 垃圾 Reference 的队列(当软引用对象被回收的时候,会将 SoftReference 保存到该队列)
    private ReferenceQueue<Employee> queue;
    // 构建一个缓存器实例
    private EmployeeCache() {
        employeeRefs = new Hashtable<>();
        queue = new ReferenceQueue<>();
    }
    // 取得缓存器实例
    public static EmployeeCache getInstance() {
        if (cache == null) {
            synchronized (EmployeeCache.class) {
                if (cache == null) {
                    cache = new EmployeeCache();
                }
            }
        }
        return cache;
    }
    // 以软引用的方式对一个Employee对象的实例进行引用并保存该引用
    private void cacheEmployee(Employee em) {
        // 清除垃圾引用
        cleanCache();
        EmployeeRef ref = new EmployeeRef(em, queue);
        employeeRefs.put(em.getId(), ref);
    }
    // 依据所指定的ID号,重新获取相应Employee对象的实例
    public Employee getEmployee(String id) {
        Employee em = null;
        // 缓存中是否有该Employee实例的软引用,如果有,从软引用中取得。
        if (employeeRefs.containsKey(id)) {
            EmployeeRef ref = employeeRefs.get(id);
            em = ref.get();
        }
        // 如果没有软引用,或者从软引用中得到的实例是null,重新构建一个实例,
        // 并保存对这个新建实例的软引用
        if (em == null) {
            em = new Employee(id);
            System.out.println("Retrieve From EmployeeInfoCenter. ID=" + id);
            cacheEmployee(em);
        }
        return em;
    }
    // 清除那些所软引用的Employee对象已经被回收的EmployeeRef对象
    private void cleanCache() {
        EmployeeRef ref;
        while ((ref = (EmployeeRef) queue.poll()) != null) {
            employeeRefs.remove(ref._key);
        }
    }
    // 继承SoftReference,使得每一个实例都具有可识别的标识。
    // 并且该标识与其在 HashMap 内的key相同。
    // 如果 SoftReference 对象没有被回收,则通过 SoftReference.get() 可以返回强引用对象,否则返回 null。
    private class EmployeeRef extends SoftReference<Employee> {
        private String _key;
        public EmployeeRef(Employee em, ReferenceQueue<Employee> q) {
            super(em, q);
            _key = em.getId();
        }
    }
    public static void main(String[] args) {
        EmployeeCache employeeCache = getInstance();
        Employee employee1 = employeeCache.getEmployee("11111");
        System.out.println("employee1:" + employee1.getName());
        employee1 = null;
        Employee employee2 = employeeCache.getEmployee("11111");
        System.out.println("employee2:" + employee2.getName());
        employee2 = null;
        System.gc();
        Employee employee3 = employeeCache.getEmployee("11111");
        System.out.println("employee3:" + employee3.getName());
    }
}
2. WeakHashMap 实践
WeakHashMap 的 key 是一个弱引用的对象,当一个键对象被垃圾回收器回收时,那么相应的值对象的引用会从Map中删除。
public class WeakMapManager {
    public static class Element {
        private String ident;
        public Element(String id) {
            ident = id;
        }
        @Override
        public String toString() {
            return ident;
        }
        @Override
        public int hashCode() {
            return ident.hashCode();
        }
        @Override
        public boolean equals(Object obj) {
            return obj instanceof Element && ident.equals(((Element) obj).ident);
        }
    }
    public static class Key extends Element {
        public Key(String id) {
            super(id);
        }
    }
    public static class Value extends Element {
        public Value(String id) {
            super(id);
        }
    }
    public static void main(String[] args) {
        int size = 20;
        // 该数组的作用,仅仅只是维护一个强引用
        Key[] keys = new Key[size];
        Map<Key, Value> map = new WeakHashMap<>();
        for (int i = 0; i < size; i++) {
            Key k = new Key(Integer.toString(i));
            Value v = new Value(Integer.toString(i));
            if (i % 3 == 0) keys[i] = k;
            map.put(k, v);
        }
        System.gc();
        System.out.println(map);
    }
}
从打印结果可以看出,当执行 System.gc() 方法后,垃圾回收器只会回收那些仅仅持有弱引用的 Key 对象,id 可以被 3 整除的 Key 对象持有强引用,因此不会被回收。
参考资料:
2. 《Thinking in Java(3th)》
3. 《深入理解 Java 虚拟机》
终结 finalize() 和对象引用的更多相关文章
- 终结 finalize()和垃圾回收(garbage collection)
		1.为什么要有finalize()方法? 假定你的对象(并非使用new)获得了一块"特殊"的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以他不知道该如何释放该对象 ... 
- 垃圾回收GC:.Net自己主动内存管理 上(三)终结器
		垃圾回收GC:.Net自己主动内存管理 上(三)终结器 垃圾回收GC:.Net自己主动内存管理 上(一)内存分配 垃圾回收GC:.Net自己主动内存管理 上(二)内存算法 垃圾回收GC:.Net自己主 ... 
- CLR回收非托管资源
		一.非托管资源 在<垃圾回收算法之引用计数算法>.<垃圾回收算法之引用跟踪算法>和<垃圾回收算法之引用跟踪算法>这3篇文章中,我们介绍了垃圾回收的一些基本概念和原理 ... 
- java  面试大全
		一.CoreJava 部分: 基础及语法部分: 1.面向对象的特征有哪些方面? [基础] 答:面向对象的特征主要有以下几个方面: 1)抽象:抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地 ... 
- cir from c# 托管堆和垃圾回收
		1,托管堆基础 调用IL的newobj 为资源分配内存 初始化内存,设置其初始状态并使资源可用.类型的实列构造器负责设置初始化状态 访问类型的成员来使用资源 摧毁状态进行清理 释放内存//垃圾回收期负 ... 
- .NET中的垃圾回收
		目录 l 导言 l 关于垃圾回收 l 垃圾回收算法 m 应用程序根(Application Roots) l 实现 m ... 
- Object类方法简介二
		在学了Object类前面的三个常用方法后,又遇到它的另外三个方法——clone().finalize().getClass(),这三个方法不经常使用,但因为在学习过程遇到了,就简单的对它们的使用做一个 ... 
- Java垃圾回收与内存
		好久没看关于java的书了, 最近, 看了James Gosling的<<Java程序设计语言>>, 做了一些读书笔记. 这部分是关于垃圾回收的. 1.垃圾回收 对象是使用ne ... 
- 【C# .Net GC】清除非托管类型(Finalize终结器、dispose模式以及safeHandler)
		总结 1.一般要获取一个内核对象的引用,最好用SafeHandle来引用它,这个类可以帮你管理引用计数,而且用它引用内核对象,代码更健壮 2.托管中生成并引用非托管,一但非托管和托管中的引用断开(托管 ... 
随机推荐
- canvas在vue中的应用
			使用cavas可以绘制各种图表.生成二维码.制作H5小游戏. 生命周期 canvas应该在mounted的生命周期中初始化,在updated中是无效的. export default { mounte ... 
- 团队项目之Scrum4
			小组:BLACK PANDA 时间:2019.11.24 每天举行站立式会议 提供当天站立式会议照片一张 2 昨天已完成的工作 2 基本实现web富文本编辑功能 后台的编辑接口已经基本完成,还有一些b ... 
- Spring学习的第二天
			第二天总共学习了以下内容: spring中的ioc常用注解: 案例使用xml方式和注解方式实现单表的CRUD操作(但还是需要xml配置文件,并不是纯注解的配置): 改造基于注解的Ioc案例,使用纯注解 ... 
- PC上装VM上装虚拟机
			1.虚拟机网卡选择桥接模式 2.查看本PC机的网络 3.到/etc/sysconfig/network-scripts,修改网卡,vi ifcfg-ens33 4.重新/etc/init.d/netw ... 
- September 08th, 2019. Sunday, Week 37th.
			A heavy drew refreshed the earth at night. 夜晚厚重的露水滋养着大地. From Leo Tolstoy. Today is the White Drew D ... 
- pytorch 中的Variable一般常用的使用方法
			Variable一般的初始化方法,默认是不求梯度的 import torch from torch.autograd import Variable x_tensor = torch.randn(2, ... 
- Spring Boot可执行Jar包运行原理
			目录 1. 打可执行Jar包 2. 可执行Jar包内部结构 3. JarLauncher 4. 简单总结 5. 远程调试 Spring Boot有一个很方便的功能就是可以将应用打成可执行的Jar.那么 ... 
- [Spring cloud 一步步实现广告系统] 13. 索引服务编码实现
			上一节我们分析了广告索引的维护有2种,全量索引加载和增量索引维护.因为广告检索是广告系统中最为重要的环节,大家一定要认真理解我们索引设计的思路,接下来我们来编码实现索引维护功能. 我们来定义一个接口, ... 
- Java操作数据库——使用JDBC连接数据库
			Java操作数据库——使用JDBC连接数据库 摘要:本文主要学习了如何使用JDBC连接数据库. 背景 数据持久化 数据持久化就是把数据保存到可掉电式存储设备中以供之后使用.大多数情况下,特别是企业级应 ... 
- 使用Navicat Premium 比较PostgreSql数据库 dev环境与test环境差异
			Navicat Premium 功能很强大,支持不同数据库客户端的连接,并且使用工具可以生成两个库差异的sql脚本,方便dev与test环境表结构同步,具体操作方法如下 单击运行,实现两个库中模式表结 ... 
