JVM8:引用是什么?强引用,软引用,弱引用,虚引用,ReferenceQueue引用队列;对象生命周期有哪些阶段?创建、应用、不可见、不可达、收集、终结、对象空间重分配;重写finazlie方法弊端

引用类型的对象如何定位到对象?

引用类型的对象如何定位到对象的常规方式有两种,一种叫句柄池访问对象,一种叫直接指针访问对象

句柄池访问对象

请添加图片描述
使用句柄访问对象,首先要了解句柄池是在堆中开辟的一块内存空间作为句柄池,句柄池中有很多很多的句柄,每个句柄之间又包含了:
①指向对象实例的指针:储存了对象实例数据,即属性值结构体的内存地址,对象实例数据一般也在heap中开辟。
②指向对象类型数据的指针:访问类型数据的内存地址(类信息,方法类型信息),对象类型数据一般储存在方法区中。

当局部变量表中的引用类型reference指向句柄池,并且定位到句柄池中间目标对象的句柄,然后再根据该句柄中的句柄信息,找到对象。

优点: reference存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要改变。
缺点: 增加了一次指针定位的时间开销。即会有两次IO操作。

直接指针访问对象

请添加图片描述
直接指针访问方式,局部变量表中的引用类型reference指向对象实例数据,对象实例数据中需要有额外的内存开销需要用来存放指向对象类型数据的指针(来存放对象在方法区的类信息地址),也就是需要开辟出一个额外的空间来存放,这个空间就是Class Pointer。
优点: 节省了一次指针定位的开销。即只有一次IO操作,比句柄池访问对象快一倍。
缺点: 在对象被移动时(如进行GC后的内存重新排列),reference本身需要被修改。

hotsport用的就是直接指针访问的方式对象的引用实际上就是对对象的直接引用,也称为指针。当我们声明一个引用变量时,它实际上是指向对象的指针。
直接指针访问的方式是一种典型的空间换时间的方式,而hotsport大部分场景下用的都是这种场景

引用是什么?

在Java中,引用是一种机制,它们允许通过使用变量来访问对象。引用本质上是指向对象的标识,而不是直接的内存地址。在Java中,开发人员不直接使用指针,而是使用引用来访问和操作对象。Java的高级抽象层次使得开发人员无需关心内存地址,同时提供了垃圾回收机制来管理对象的生命周期。这种高级抽象带来了更安全和稳定的编程环境,有助于简化编程,并提高代码的可维护性。但也有一些限制,如不直接控制内存分配和释放。

当谈到Java中的引用、指针、内存地址时,有一些关键的区别需要注意:
引用(Reference)

  • 引用是指向对象的标识,用于访问和操作对象。Java中的引用是高级抽象的,通常用于在代码中操作对象,而不必考虑底层内存地址。
  • Java引用通常在更高层次的抽象中使用,不需要直接访问内存地址。
  • 引用可以根据对象的生命周期和引用类型的不同,影响垃圾回收的行为。例如,强引用、软引用、弱引用和虚引用在引用对象的生命周期和回收行为上有所不同。

指针(Pointer):

  • 指针是直接指向内存地址的值,允许直接访问内存中的数据。在低级别编程语言中(如C、C++),指针用于在内存中查找数据,需要更多关注内存位置和管理。
  • 指针可以进行算术运算,可以指向任何数据类型,包括基本类型和复杂类型。
  • 由于直接操作内存地址,指针可能导致一些问题,如悬空指针(指向无效内存地址)、内存泄漏等。

内存地址
内存地址是操作系统和底层硬件使用的概念,用于唯一标识计算机内存中的特定位置。然而,在Java中,程序员通常无法直接访问或操作对象的具体内存地址。Java虚拟机(JVM)负责将对象分配到堆内存中,并管理对象的生命周期和内存回收。因此,开发人员无需关注对象的实际内存地址,只需通过引用来访问对象。

关键区别和概念的解释:
引用的作用:引用允许开发人员通过变量名称来访问和操作对象,提供了一种高级抽象,隐藏了底层内存管理的复杂性。这使得开发人员能够更专注于业务逻辑的实现。
内存地址的访问:Java的高级抽象屏蔽了内存地址的直接访问,确保开发人员不会意外地破坏内存的完整性或引发安全问题。
垃圾回收和引用关系:垃圾回收器基于对象之间的引用关系来确定对象是否可达。引用链构成了对象之间的连接,垃圾回收器使用这些链来判断哪些对象可以被安全地回收。

Java中对于引用的定义很传统:如果reference(引用)类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些处于判定中,又或者我们想扔又舍不得的对象就显得无能为力。

问题:我们希望能描述这样一类对象:

  • 当内存空间还足够时,则能保留在内存之中
  • 如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象

很多系统的缓存功能都符合这样的应用场景。那么其实我们可以想到,我们的对象的生命周期中一定会有很多种的状态,然后我们会在各个状态都给出一个相对应的描述。

Java中有几种引用类型,包括强引用、软引用、弱引用和虚引用,它们影响了垃圾回收的行为和对象的生命周期。因此在了解对象的生命周期之前,先了解下对象的几种引用类型。

请添加图片描述

强引用

在Java中最常见的就是强引用。把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它就处于可达状态,不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收,哪怕是内存溢出都不会回收。因此强引用是造成Java内存泄漏的主要原因之一

开发过程中经常使用到的Object obj = new Object();,这种new对象的方式就是强引用,不需要显示的进行声明,它是直接new出来的实例,赋值给了引用变量obj,而obj是GC root,中间没有使用任何Reference的数据去装载它,从GC root出发,整个引用链上除了GC root是引用,其他的链路元素都为对象。

软引用

软引用需要用SoftReference类来实现。软引用是用来描述一些还有用,但是非必须的对象,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时(在发生OOM之前会被回收)、或者存活时间太长,它会被回收。

软引用通常用在对内存敏感的程序中。这个特性使他比较适合做缓存,比如说网页缓存、或者图片缓存。软引用的出现是为了让Java更好的管理内存,市面上的图片框架一般都会用到软引用。这是一种代码层面的优化,优化时要结合业务场景具体分析是否使用软引用。比如阿里的鹰眼,软引用应用的就非常多。

比如商城中你查看商品点开的介绍图片,过几秒之后,对比产品回到之前浏览的这个商品,再次查看介绍图片。
有两种方法去进行实现:
①一种是将它保存在内存当中,整个图片对象的存活时间贯穿着整个生命周期,不进行回收。优点:不回收,每次访问速度很快;弊端:会造成大量的内存浪费。适用于频繁使用的对象。
②另一种方式就是用到的时候加载放到内存,直到内存不足,在OOM之前回收掉。适用于不频繁使用的对象。

软引用的应用场景:

  1. 处理生命周期长的对象
  2. 处理占用内存大的对象
  3. 1和2兼而有之的
  4. 适用于使用并不频繁的对象(使用频繁的干嘛要回收?不回收)
import com.example.jvmdemo.pojo.Worker;

import java.lang.ref.SoftReference;

/**
 * 软引用做缓存
 */
public class SoftReferenceDemo {

    public static void main(String[] args) {
        // 一堆业务代码
        Worker worker = new Worker();
        // 业务代码使用到了Worker实例,Worker是你的业务类

        // ①使用完了a,将它设置为soft引用类型(软引用会在内存不足时回收,发生OOM之前会被回收)
        SoftReference softReference = new SoftReference(worker);
        // ②并且释放强引用(也就是说GC root不再指向堆)
        worker = null;

        // ③下次使用时判断软引用是否有数据
        if (softReference != null) {
            // 没被回收,通过.get()直接获取,获取出来是Object类型,进行强转
            worker = (Worker) softReference.get();// get()之后又会变为强引用,只不过在此之前的这段时间,worker实例并不一直是强引用,而且get相比new而言,消耗资源要少很多,因为worker并没有回收,不需要重新创建
            // 软引用通常会在再次使用到它之后,它会保持一段时间,默认是按照堆的剩余空间计算的,也就是说,它不会立马回收,而是存活一段时间,直到内存不足或者存活时间太长才会被回收
        } else {
            // GC由于内存资源不足,可能系统已回收了a的软引用,
            // 因此需要重新装载。
            worker = new Worker();
            softReference = new SoftReference(worker);
        }
    }
}

软引用可以避免重新创建对象(new对象),而是通过对象的重复创建(get将对象再次变为强引用),来提高程序堆内存的使用效率。get相比new而言,消耗资源要少很多,因为worker并没有回收,不需要重新创建。

软引用也有弊端:它会降低应用的运行效率。因为长时间不回收,假如堆内存长时间卡在80~90%这个样子,而堆内存占用过高会影响程序的运行效率。

弱引用

弱引用需要用WeakReference类来实现。它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,总会回收该对象占用的内存。弱引用主要适用于无法被规范化回收的情况,使用场景非常狭窄,几乎用不到。

import java.lang.ref.WeakReference;

/**
 * 弱引用关联对象何时被回收
 */
public class WeakReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        // 设置100M的缓存数据
        byte[] cacheData = new byte[100 * 1024 * 1024];
        // 将缓存数据给给到弱引用持有
        WeakReference<byte[]> cacheRef = new WeakReference<>(cacheData);
        System.out.println("第一次GC前" + cacheData);
        System.out.println("第一次GC前" + cacheRef.get());

        // 进行一次GC后查看对象的回收情况
        System.gc();
        // 等待GC,睡一会,不确定System.gc()说明时候执行完成,睡一下也未必会回收,但是一般情况下是会的,因为已经通知到了
        Thread.sleep(500);
        System.out.println("未释放强引用,第一次GC后" + cacheData);
        System.out.println("未释放强引用,第一次GC后" + cacheRef.get());

        // 将缓存数据的强引用去除
        cacheData = null;
        System.gc();
        // 等待GC
        Thread.sleep(500);
        System.out.println("释放强引用,第二次GC后" + cacheData);
        System.out.println("释放强引用,第二次GC后" + cacheRef.get());

        /**
         * 输出结果:
         * 第一次GC前[B@a09ee92
         * 第一次GC前[B@a09ee92
         * 未释放强引用,第一次GC后[B@a09ee92
         * 未释放强引用,第一次GC后[B@a09ee92
         * 释放强引用,第二次GC后null
         * 释放强引用,第二次GC后null
         */
    }
}

ReferenceQueue引用队列

查看SoftReference源码,
在这里插入图片描述
查看WeakReference源码,
在这里插入图片描述
可以发现还有一个构造方法是带队列的,ReferenceQueue是干嘛的?有什么作用?
开发者希望得到一个通知,能够告诉用户线程,对象什么时候被GC掉了,那么这个时候,最好的办法就是在引用当中维护一个队列,能够将这个对象及其相关信息放在这个队列中,当对象被GC掉的时候,能够在这个队列中获取这个对象的相关信息,这样能够方便我们对其进行一个再次的处理,比如说GC之后,再次让对象存活,或者干掉做数据清理,都是由开发者去掌控。

ReferenceQueue就是干这个的,当GC释放对象内存的时候,会将引用加入到引用队列引用队列可以配合软引用、弱引用及幽灵引用使用,当引用的对象将要被JVM回收时,会将其加入到引用队列中。

/**
 * 引用队列
 */
public class WeakReferenceQueueDemo {
    public static void main(String[] args) throws InterruptedException {

        Object obj = new Object();
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();

        // 起一个线程来监控队列
        Thread thread = new Thread(() -> {
            try {
                int cnt = 0;
                WeakReference<byte[]> k;
                // 可以通过调用ReferenceQueue类的remove()方法来获取已经被回收的对象的引用
                while ((k = (WeakReference) referenceQueue.remove()) != null) {
                    System.out.println((cnt++) + "回收了:" + k);
                }
            } catch (InterruptedException e) {
                // 结束循环
                System.out.println();
            }
        });
        thread.setDaemon(true);
        thread.start();

        Map<Object, Object> dataMap = new HashMap<>();

        // 循环添加10000次数据
        for (int i = 0; i < 10000; i++) {
            byte[] bytes = new byte[1024 * 1024];
            WeakReference<byte[]> weakReference = new WeakReference<>(bytes, referenceQueue);
            dataMap.put(weakReference, obj);
        }
        System.out.println("map.size->" + dataMap.size());
    }
}

虚引用

虚引用也称为 “幽灵引用” 或者 “幻影引用” ,顾名思义形同虚设,它是最弱的一种引用关系,一个对象被虚引用变量所引用的,完全不会对其生存时间构成影响,该回收回收,无法通过虚引用来取得一个对象实例(即get()获取的是null)。虚引用需要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态,就是通知,使用场景非常狭窄,几乎用不到。

虚引用的特点: 虚引用必须与ReferenceQueue一起使用,当GC准备回收一个对象,如果发现它还有虚引用,就会在回收之前,把这个虚引用加入到与之关联的ReferenceQueue中。

应用场景: 虚引用的主要作用是跟踪对象垃圾回收的状态。仅仅是提供了一种确保对象被finalize 以后,做某些事情的机制。

总结

对比之下,软引用 在框架里用的比较多,弱引用和虚引用无法看到立竿见影的效果,因此在业务代码中,用的很少。强引用不需要new对应的引用对象,其它三种引用需要显示的去new才能够使用。

对象生命周期有哪些阶段?

在Java中,对象的生命周期包括以下几个阶段:

1.创建阶段(Created)

  • 为对象分配存储空间
  • 开始构造对象
  • 从超类到子类对static成员进行初始化
  • 超类成员变量按顺序初始化,递归调用超类的构造方法
  • 子类成员变量按顺序初始化,子类构造方法调用
  • 一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段

2.应用阶段(In Use)

对象至少被一个强引用持有着

3.不可见阶段(Invisible)

不可见阶段就是超出了对象的作用域,比如在方法中定义了一个实例对象:

public class Test {

    public static int test(int a) {
        int result = a + 2;
        Object obj = new Object();
        return result;
    }

    public static void main(String[] args) {
        test(2);
    }
}

Object obj = new Object();obj对象的作用域是test()方法,也就是说,test()方法运行完成,返回了result之后,就超过了obj对象的作用域,即obj这个对象就到达了不可见的状态,这个时候一般建议将obj手动回收,即设置为null,这样做可以帮助JVM及时发现这个对象。

4. 不可达阶段(Unreachable)

对象处于不可达阶段是指该对象不再被任何强引用所持有。
与“不可见阶段”相比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被 JVM 等系统下的某些已装载的静态变量或线程或 JNI 等强引用持有着,这些特殊的强引用被称为”GC root ”。存在着这些 GC root 会导致对象的内存泄露情况,无法被回收。

是否在可达性分析处于不可达时,就表示这个对象是垃圾?
如果是那么一点余地都没有,那么JVM的垃圾回收机制就太呆板了。就算被判定为不可达,对象也并非非死不可,真正宣判的时候,要经历两个阶段,也就是收集阶段。

5. 收集阶段(Collected)

当垃圾回收器发现该对象已经处于“不可达阶段”,并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了 finalize()方法,则会去执行该方法的终端操作。

收集阶段、终结阶段、空间重分配阶段可以看成是一个整体,那就是垃圾回收

模拟重写了finalize()方法的对象的回收

public class FinalizeDemo {

    private static FinalizeDemo save_hook = null;// 类变量

    public void isAlive() {
        System.out.println("还活着");
    }

    /**
     * 一般用来释放非Java资源,如:打开的文件资源、数据库的链接、或者说调用非Java方法,也就是native方法;这些时候需要分配内存,因此可以变相的演唱对象的生命周期
     * <p>
     * 实际写Java代码的过程当中,慎用
     * <p>
     * 并且finalize()方法也只会复活一次,只能延长一次生命周期
     * <p>
     * 而且finalize()能够做的事情finally也可以做,并且能做的更好
     * <p>
     * finalize()在Java源码中用的较多
     */
    @Override
    public void finalize() {
        System.out.println("finalize方法执行");
        FinalizeDemo.save_hook = this;
    }

    public static void main(String[] args) throws InterruptedException {
        save_hook = new FinalizeDemo();// 对象
        // 释放强引用,对象第一次拯救自己
        save_hook = null;
        System.gc();
        // 睡眠等待GC
        Thread.sleep(500);
        if (save_hook != null) {
            save_hook.isAlive();
        } else {
            System.out.println("死了");
        }

        // 再次释放强引用,对象第二次拯救自己
        save_hook = null;
        System.gc();
        // 睡眠等待GC
        Thread.sleep(500);
        if (save_hook != null) {
            save_hook.isAlive();
        } else {
            System.out.println("又死了");
        }
    }
}

重写finazlie()方法弊端

请添加图片描述
这里要特别说明一下:不要重写finazlie()方法!原因有两点:
①会影响JVM的对象分配与回收速度
在分配该对象时,JVM需要在垃圾回收器上注册该对象,以便在回收时能够执行该重载方法;在该方法的执行时需要消耗CPU时间且在执行完该方法后才会重新执行回收操作,即至少需要垃圾回收器对该对象执行两次GC。

因为需要去执行判断是否有必要执行finazlie()方法,而运行方法会消耗CPU的时间,而且GC后会复活,需要等到下次到达收集阶段才会再去判断是否有必要执行finazlie()方法,因此重写了finazlie()方法的对象至少需要两次GC才能回收掉,所以肯定是消耗效率的。

②可能造成该对象的再次“复活”
在finalize()方法中,如果有其它的强引用再次持有该对象,则会导致对象的状态由“收集阶段”又重新变为“应用阶段”。这个已经破坏了Java对象的生命周期进程,且“复活”的对象不利用后续的代码管理。

6. 终结阶段(Finalized)

当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段就是等待垃圾回收器对该对象空间进行回收。

7. 对象空间重分配阶段(De-allocated)

垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。

思考

垃圾是如何回收的呢?
GC是需要消耗线程的,那么势必会去和业务线程去互相竞争线程资源,它们回去抢占CPU的时间片。

假如业务线程和GC线程是一起,那么会怎么样?
比如在收集阶段,GC线程去判断谁是垃圾,从GC root触发,obj指向堆的对象a,堆中对象的指向关系为a→b→c是存活对象,d是准备回收的对象,判断完之后,业务线程抢占到了CPU的时间片,来了一个逻辑,a→b→d,d成为可达对象了,C成为了不可达对象,但是当GC线程再次竞争到CPU资源去执行回收时候,d会被回收掉,而C不会,也就是说,不该回收的被回收了,该回收的没回收掉。该回收的没回收还能忍,大不了下次回收掉,那不该回收的被回收了就忍不了了。怎么解决呢?

改成串行的方式,GC线程和业务线程同一时间只有一个再跑,这就是大名鼎鼎的stop the world,GC线程运行的时候,停掉业务线程的全世界。

汇总

JVM1:官网了解JVM;Java源文件运行过程、javac编译Java源文件、如何阅读.class文件、class文件结构格式说明、 javap反编译字节码文件;类加载机制、class文件加载方式

JVM2:类加载机制、class文件加载方式;类加载的过程:装载、链接、初始化、使用、卸载;类加载器、为什么类加载器要分层?JVM类加载机制的三种方式:全盘负责、父类委托、缓存机制;自定义类加载器

JVM3:图解类装载与运行时数据区,方法区,堆,运行时常量池,常量池分哪些?String s1 = new String创建了几个对象?初识栈帧,栈的特点,Java虚拟机栈,本地方法发栈,对象指向问题

JVM4:Java对象内存布局:对象头、实例数据、对齐填充;JOL查看Java对象信息;小端存储和大端存储,hashcode为什么用大端存储;句柄池访问对象、直接指针访问对象、指针压缩、对齐填充及排序

JVM5:JVM内存模型与运行时数据区的关系,堆为什么分区,分代年龄,Young区划分,Survivor区为什么分为S0和S1,如何理解各种GC:Partial GC、Full GC、Young GC

JVM6:JVM内存模型验证;使用visualvm查看JVM视图;Visual GC插件下载链接;模拟JVM常见错误,模拟堆内存溢出,模拟栈溢出,模拟方法区溢出

JVM7:垃圾回收是什么?从运行时数据区看垃圾回收到底回收哪块区域?垃圾回收如何去回收?垃圾回收策略,引用计数算法及循环引用问题,可达性分析算法

JVM8:引用是什么?强引用,软引用,弱引用,虚引用,ReferenceQueue引用队列;对象生命周期有哪些阶段?创建、应用、不可见、不可达、收集、终结、对象空间重分配;重写finazlie方法弊端

JVM9:STW:stop the world,什么时候会垃圾回收?垃圾收集算法:标记清除算法、标记复制算法、标记整理算法;清除算法的整理顺序:任意顺序,滑动顺序;什么是分代收集算法?

JVM10:JVM参数分类,JVM标准参数,JVM非标准参数,-X参数,-XX参数,其他参数;查看JVM参数,idea控制台输出JVM参数,单位换算;设置JVM参数的常见方式;常用JVM参数及含义

JVM11:垃圾收集器的并发和并行,Serial,Serial Old,ParNew,Parallel Scavenge,Parallel Old,源码分析CMS两种模式,CMS如何定位可达对象?

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值