java三个引用类型

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java三个引用类型相关的知识,希望对你有一定的参考价值。

参考技术A 四种引用类型
所以在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱。

一,强引用
Java中默认声明的就是强引用,比如:

Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null; //手动置null
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了

二,软引用
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。

下面以一个例子来进一步说明强引用和软引用的区别:
在运行下面的Java代码之前,需要先配置参数 -Xms2M -Xmx3M,将 JVM 的初始内存设为2M,最大可用内存为 3M。

首先先来测试一下强引用,在限制了 JVM 内存的前提下,下面的代码运行正常

public class TestOOM

public static void main(String[] args)
testStrongReference();

private static void testStrongReference()
// 当 new byte为 1M 时,程序运行正常
byte[] buff = new byte[1024 * 1024 * 1];


但是如果我们将

byte[] buff = new byte[1024 * 1024 * 1];
替换为创建一个大小为 2M 的字节数组

byte[] buff = new byte[1024 * 1024 * 2];
则内存不够使用,程序直接报错,强引用并不会被回收

接着来看一下软引用会有什么不一样,在下面的示例中连续创建了 10 个大小为 1M 的字节数组,并赋值给了软引用,然后循环遍历将这些对象打印出来。

public class TestOOM
private static List<Object> list = new ArrayList<>();
public static void main(String[] args)
testSoftReference();

private static void testSoftReference()
for (int i = 0; i < 10; i++)
byte[] buff = new byte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<>(buff);
list.add(sr);


System.gc(); //主动通知垃圾回收

for(int i=0; i < list.size(); i++)
Object obj = ((SoftReference) list.get(i)).get();
System.out.println(obj);





打印结果:

我们发现无论循环创建多少个软引用对象,打印结果总是只有最后一个对象被保留,其他的obj全都被置空回收了。
这里就说明了在内存不足的情况下,软引用将会被自动回收。
值得注意的一点 , 即使有 byte[] buff 引用指向对象, 且 buff 是一个strong reference, 但是 SoftReference sr 指向的对象仍然被回收了,这是因为Java的编译器发现了在之后的代码中, buff 已经没有被使用了, 所以自动进行了优化。
如果我们将上面示例稍微修改一下:

private static void testSoftReference()
byte[] buff = null;

for (int i = 0; i < 10; i++)
buff = new byte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<>(buff);
list.add(sr);


System.gc(); //主动通知垃圾回收

for(int i=0; i < list.size(); i++)
Object obj = ((SoftReference) list.get(i)).get();
System.out.println(obj);


System.out.println("buff: " + buff.toString());


则 buff 会因为强引用的存在,而无法被垃圾回收,从而抛出OOM的错误。

如果一个对象惟一剩下的引用是软引用,那么该对象是软可及的(softly reachable)。垃圾收集器并不像其收集弱可及的对象一样尽量地收集软可及的对象,相反,它只在真正 “需要” 内存时才收集软可及的对象。

三,弱引用
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。
我们以与软引用同样的方式来测试一下弱引用:

private static void testWeakReference()
for (int i = 0; i < 10; i++)
byte[] buff = new byte[1024 * 1024];
WeakReference<byte[]> sr = new WeakReference<>(buff);
list.add(sr);


System.gc(); //主动通知垃圾回收

for(int i=0; i < list.size(); i++)
Object obj = ((WeakReference) list.get(i)).get();
System.out.println(obj);


打印结果:

可以发现所有被弱引用关联的对象都被垃圾回收了。

四,虚引用
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。

public class PhantomReference<T> extends Reference<T>
/**
* Returns this reference object's referent. Because the referent of a
* phantom reference is always inaccessible, this method always returns
* <code>null</code>.
*
* @return <code>null</code>
*/
public T get()
return null;

public PhantomReference(T referent, ReferenceQueue<? super T> q)
super(referent, q);


那么传入它的构造方法中的 ReferenceQueue 又是如何使用的呢?

五,引用队列(ReferenceQueue)
引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

Java内存分析

Java内存主要分为三个部分:

    • 存放new的对象和数组

    • 可以被所有的线程共享,不会存放别的对象引用

    • 存放基本变量类型(会包含这个基本类型的具体数值)

    • 引用对象的变量(会存放这个引用在在堆里面的具体地址)

  • 方法区

    • 可以被所有的线程共享

    • 包含了所有的class和static变量

类加载到内存的流程

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化:

  1. 加载(Load):这是由类加载器(ClassLoader)执行的。通过一个类的全限定名来获取其定义的二进制字节流(class文件字节码),将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,根据字节码在Java堆中生成一个代表这个类的java.lang.Class对象。

  2. 链接(Link):将Java类的二进制代码合并到JVM的运行状态之中,包括三步:

    • 验证:确保加载的类信息(class文件中的字节流包含的信息)符合JVM规范,没有安全方面的问题;

    • 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配;

    • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

  3. 初始化(Initalize):到了此阶段,才真正开始执行类中定义的Java程序代码。

    • 执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器);

    • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化;

    • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步;

用代码来分析一下这个过程:

public class Test05 {
    /*
        1.加载到内存,会产生一个类对应的Class对象
        2.链接,链接结束后 m = 0
        3.初始化
            <clinit>(){
                System.out.println("AA类静态代码块");
                m = 300;
                m = 100;
            }
            m = 100;
     */
    public static void main(String[] args) {
?
//        System.out.println(AA.m);
        /*
            输出结果:
                AA类静态代码块
                100
         */
?
        AA aa = new AA();
        System.out.println(aa.m);
        /*
            输出结果:
                AA类静态代码块
                AA类无参构造函数初始化
                100
         */
?
    }
}
class AA {
?
    static {
        System.out.println("AA类静态代码块");
        m = 300;
    }
    static int m = 100;
    public AA(){
        System.out.println("AA类无参构造函数初始化");
    }
}

所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此Java程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。这一点与许多传统语言都不同。动态加载使能的行为,在诸如C++这样的静态加载语言中是很难或者根本不可能复制的。

在进行类加载的时候,类加载器会先检查这个类的Class对象是否已经加载,如果还未加载,则默认的类加载器就会根据类名查找.class文件(这里不一定都是从文件系统中获取,可以从数据库读取字节码或者接受网络中传输过来的字节码)。这些字节码在被加载的时候,会检查验证保证它们没有被破坏。一旦某个类的Class对象被载入内存,它就会被用来创建这个类的所有对象。

通过上面的过程我们还可以直到,static变量在链接阶段就已经有默认值了,所以在类没有的被初始化的时候,就可以调用static变量了。

这里要解释一下三个概念,字面量、符号引用、直接引用:

  • 字面量:用于表达源代码中一个固定值的表示法,比如Java中的常数1,2,..等,或者说字符串"abcd" "java"等

  • 符号引用:以一组符号来描述所引用的目标, 符号可以是任何形式的字面量, 只要使用时能够无歧义的定位到目标即可。例如, 在Java中, 一个Java类将会编译成一个class文件. 在编译时, Java类并不知道所引用的类的实际地址, 因此只能使用符号引用来代替. 比如org.simple.People类引用了org.simple.Language类, 在编译时People类并不知道Language类的实际内存地址, 因此只能使用符号org.simple.Language来表示Language类的地址。

  • 直接引用可以是以下三种:

    • 直接指向目标的指针.( 指向方法区中类对象, 类变量和类方法的指针);

    • 相对偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到实例方法字节码或者实例变量字节码的起始位置;

    • 一个间接定位到对象的句柄;

符号引用通常是设计字符串的——用文本形式来表示引用关系。直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

通过上面的Object类的getClass方法我们可以取得该类已经被实例化了的(new 运算的)对象的类的引用,这个引用指向的是Class类的对象。比如,我们看如下代码:

// 创建User对象,并调用getClass方法获取类的引用
User user = new User();
System.out.println(user.getClass());
/*
    输出结果为:class 反射.User
*/
  // 通过反射获取类User的Class对象
Class c1 = Class.forName("反射.User");
System.out.println(c1); 
// 输出结果:class 反射.User

getClass方法返回的就是当前对象的运行时类,就是实际创建当前对象的那个类的引用。

类初始化

什么时候会发生类初始化
  • 类的主动引用(一定会发生类的初始化)

    • 当虚拟机启动,先初始化main方法所在的类

    • new一个类的对象

    • 调用的类的静态成员(除了final常量)和静态方法

    • 使用java.lang.reflect包的方法对类进行反射调用

    • 当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类

  • 类的被动引用(不会发生类的初始化)

    • 当访问一个静态域的时候,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化;

    • 通过数组定义类引用,不会触发此类的初始化;

    • 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)

用代码展示一下:

public class Test06 {
    static {
        System.out.println("加载Main");
    }
?
    public static void main(String[] args) throws ClassNotFoundException {
        // 1.主动引用
        //Son son = new Son();
        /*
        输出结果:
            加载Main
            父类被加载
            子类被加载
         */
        
        // 2.反射也会产生主动引用
        //Class.forName("反射.Son");
        /*
        输出结果:
            加载Main
            父类被加载
            子类被加载
         */
        
        // 3.调用的类的静态成员(除了final常量)和静态方法会初始化类
        // 但是,当访问一个静态域的时候,只有真正声明这个域的类才会被初始化
        // 所以下面这个语句的输出结果为:
        /*
            加载Main
            父类被加载
            100
         */
        // System.out.println(Son.a);
?
        // 通过数组定义类引用,不会导致类的初始化
        //Son[] array = new Son[5];
        /*
            加载Main
         */
        
        // 引用常量也不会导致类的初始化
        //System.out.println(Son.c);
        /*
            加载Main
            100
         */
    }
}
?
class Father{
    static {
        System.out.println("父类被加载");
    }
    static int a = 100;
}
?
class Son extends Father{
    static {
        System.out.println("子类被加载");
    }
    static final int c = 100;
}

类加载器

  • 类加载器的作用:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。

  • 类缓存:标准的Java SE加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。

JVM规范定义了如下类型的类加载器:

  • 引导类加载器:用C++编写的,是JVM自带的类加载器,负责Java平台核心库(rt.jar包),用来装载核心类库。该加载类无法直接获取。

  • 扩展类加载器:负责jre/lib/ext目录下的jar包或-D java.ext.dirs指定目录下的jar包装入工作库;

  • 系统类加载器:负责java -classpath 或 -D java.class.path所指的目录下的类与jar包装入工作,是最常用的加载器。

实际用代码来调用一下加载器:

public static void main(String[] args) throws ClassNotFoundException {
    //获取系统类的加载器
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
    System.out.println(systemClassLoader);
    /*
        sun.misc.Launcher$AppClassLoader@18b4aac2
     */
?
    // 获取系统类加载器的父类加载器-->扩展类加载器
    ClassLoader parent = systemClassLoader.getParent();
    System.out.println(parent);
    /*
        sun.misc.Launcher$ExtClassLoader@677327b6
     */
?
    //获取扩展类加载器的父类加载器-->根加载器(C/C++)
    ClassLoader parent1 = parent.getParent();
    System.out.println(parent1);
    /*
        null
     */
?
    // 测试当前类是哪个加载器加载的
    ClassLoader classLoader = Class.forName("反射.Test07").getClassLoader();
    System.out.println(classLoader);
?
    // 测试JDK内置的类是谁加载的
    classLoader = Class.forName("java.lang.Object").getClassLoader();
    System.out.println(classLoader);
?
    // 如何获得系统类加载器可以加载的路径
    System.out.println(System.getProperty("java.class.path"));
    /*
    /usr/java/jdk1.8.0_231/jre/lib/charsets.jar:
    /usr/java/jdk1.8.0_231/jre/lib/deploy.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/cldrdata.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/dnsns.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/jaccess.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/jfxrt.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/localedata.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/nashorn.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/sunec.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/sunjce_provider.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/sunpkcs11.jar:
    /usr/java/jdk1.8.0_231/jre/lib/ext/zipfs.jar:
    /usr/java/jdk1.8.0_231/jre/lib/javaws.jar:
    /usr/java/jdk1.8.0_231/jre/lib/jce.jar:
    /usr/java/jdk1.8.0_231/jre/lib/jfr.jar:
    /usr/java/jdk1.8.0_231/jre/lib/jfxswt.jar:
    /usr/java/jdk1.8.0_231/jre/lib/jsse.jar:
    /usr/java/jdk1.8.0_231/jre/lib/management-agent.jar:
    /usr/java/jdk1.8.0_231/jre/lib/plugin.jar:
    /usr/java/jdk1.8.0_231/jre/lib/resources.jar:
    /usr/java/jdk1.8.0_231/jre/lib/rt.jar:
    /home/fym/文档/JavaProject/production/learn_java:
    /home/fym/文档/JavaProject/learn_java/src/lib/commons-io-2.6.jar:
    /usr/local/idea-IU-193.5233.102/lib/idea_rt.jar
     */
?
    /*
        双亲委派机制:
            当我们自己定义一个类的时候,JVM会自己一层一层从用户类加载器到扩展类加载器
            再到引导类加载器的找有没有同名的包,如果有的话则不会调用我们自己写的类,
            而是去用JVM自己的。这是为了保证安全。
     */
}

可以看出系统类加载器加载的也有我们项目路径和IDE的Jar包。

以上是关于java三个引用类型的主要内容,如果未能解决你的问题,请参考以下文章

java引用类型的类型转换规则是啥?

java中的引用数据类型是啥意思?

JAVA的基本数据类型和引用数据类型的区别

java 判断一个对象是值类型还是引用类型

Java内存分析

Java 引用数据类型问题