1、JVM类的加载

前言

在java中,数据类型分为基本数据类和引用数据类型,基本数据类型由虚拟机预先定义,引用数据类型则需要类的加载。类的加载是把class字节码文件加载到内存中,并最后进行卸载,整个类的声明周期分为如下7个步骤,其中验证、准备、解析又可以合称为链接阶段。
在这里插入图片描述

1、加载

加载可以理解为把字节码文件加载到内存中,通过类的二进制数据流的方式,并在内存中生成加载类的模板对象,也可以叫做class对象,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类的模板对象中,通过这个模板对象可以通过反射获取到类的任何信息。
加载的字节码文件到内存中并生成类的模板对象是包含2层意思的,首先把加载的字节码文件生成对应类的结构存储在方法区中(JDK8以前称为永久代,JDK8之后称为源空间);然后在堆空间生成对应类的class对象。

数组也可以称为对象,数组对象与普通的类的对象创建方式不一样。数组对象本身不是由加载器创建,而是JVM在程序运行时动态创建,但数组元素类型对应的类如果是引用类型的话需要加载器把对应的类加载到内存中。这样,JVM在运行时根据已经加载进来的引用类型+数组维度创建数组对象。

加载阶段主要由三类加载器进行加载Class文件:引导类加载器、扩展类加载器、系统类加载器。
加载器ClassLoader只负责加载Class字节码文件,具体字节码是否可以运行由后续的执行器来决定。加载的类信息存放在方法区的内存空间中,JDK中改为元数据空间中。方法区中还会存放运行时常量池信息,包括字符串、数字常量。
加载是通过类的全限定名获取类的二进制字节流,通过字节流将字节码文件加载到方法区的运行时数据结构中存储,并在内存中生成该类的Class对象,作为方法区该类的各种数据的访问入口。

加载器分为如下4类加载器

  1. Bootstrap ClassLoader
    引导类加载器,也叫启动类加载器,由C/C++语言实现。主要加载java的核心类库,加载的路径包括 JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的类。启动类加载器没有父加载器,不继承ClassLoader,启动类加载器会加载扩展类加载器和应用程序类加载器,并且启动类加载器会指定为扩展类加载器和应用程序类加载器的父加载器。出于安全考虑,BootStrap加载器只能加载包名为java、javax、sun等开头的类。

  2. 扩展类加载器
    扩展类加载器继承自ClassLoader类,父类加载器为启动类加载器。扩展类加载器加载jre/lib/ext目录下的类库,或者java.ext.dirs系统属性指定的目录中加载类库,如果用户床架的jar放在此目录下,该jar也会由扩展类加载器加载。

  3. 应用程序类加载器
    应用程序类加载器继承自ClassLoader类,父加载器为启动类加载器,负责加载环境变量classpath或系统属性java.class.path指定路径下的类库。该加载器为程序中默认的加载器,一般java应用均由该加载器加载完成。通过ClassLoader.getSystemClassLoader()可以得到程序应用类加载器。

  4. 用户自动以加载器
    可以自动以加载器,从指定位置加载类,比如从硬盘指定位置或者网络。

1.1 获取加载器方式

  • 获取指定加载器
 /*获取指定类的ClassLoader*/
public static void testClassLoader1(){
    ClassLoader classLoader = Integer.class.getClassLoader();           //BootStrap Loader
    System.out.println("classLoader : " + classLoader);
    ClassLoader classLoader1 = HostUtils.class.getClassLoader();        //Extension ClassLoader
    System.out.println("classLoader1 : " + classLoader1);
    ClassLoader classLoader2 = ClassLoaderDemo.class.getClassLoader();  //Appliction ClassLoader
    System.out.println("classLoader2 : " + classLoader2);
}

输出结果如下:

classLoader : null
classLoader1 : sun.misc.Launcher$ExtClassLoader@14ae5a5
classLoader2 : sun.misc.Launcher$AppClassLoader@58644d46

Integer由启动类加载器加载器加载,启动类加载器为null,不可以获取;
HostUtils由扩展类加载器加载;
ClassLoaderDemo由应用加载器加载。

  • 获取当前上下文的加载器
public static void testClassLoader2(){
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    System.out.println("classloader : " + classLoader);
}

输出结果如下,说明程序的上下文加载器为Application ClassLoader

classloader : sun.misc.Launcher$AppClassLoader@58644d46
  • 获取父加载器
public static void testClassLoader3(){
    ClassLoader appClassLoader = Thread.currentThread().getContextClassLoader();
    System.out.println("appClassLoader : " + appClassLoader);               //当前线程加载器为Application ClassLoader
    ClassLoader extensionClassLoader = appClassLoader.getParent();
    System.out.println("extensionClassLoader : " + extensionClassLoader);   //Application ClassLoader的父加载器为Extension ClassLoader
    ClassLoader bootStrapClassLoader = extensionClassLoader.getParent();    
    System.out.println("bootStrapClassLoader : " + bootStrapClassLoader);   //Extension ClassLoader的父加载器为BootStrap ClassLoader
}

输出结果如下:

appClassLoader : sun.misc.Launcher$AppClassLoader@58644d46
extensionClassLoader : sun.misc.Launcher$ExtClassLoader@74a14482
bootStrapClassLoader : null
  • 获取系统加载器
public static void testClassLoader4(){
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
    System.out.println("systemClassLoader : " + systemClassLoader);	//系统加载器为Applciation ClassLoader
}

1.2 双亲委派机制
java虚拟机加载class字节码文件时采用的按需加载方式,只有当使用时才会把Class文件加载到内存中,而加载Class文件时采用的就是双亲委派机制,把要加载Class的任务交由父加载器加载。


双亲委派机制原理:如果一个类加载器收到了类加载的请求,该类加载器并不会立即去加载,而是把加载任务先委托给父加载器加载;如果父加载器还有父加载器,则会继续向上委托,一直委托到启动类加载器;如果父加载器可以完成加载Class字节码的任务,就成功加载返回,若需要加载的类不在父加载器加载范围内,子加载器尝试去加载,一直到向下委托到可以加载的加载器。


案例一,比如在自己的工程内新建一个java.lang.Integer类,当用到Integer时,其实并没加载自己工程内的java.lang.Integer,而是加载了rt.jar包下的java.lang.Integer类。当在代码中用到Integer类时,Application ClassLoader会把加载java.lang.Integer的任务向上委托给Extension ClassLoader加载器,Extension ClassLoader加载器又会继续把加载java.lang.Integer的任务继续向上委托给Bootstrap ClassLoader进行加载,恰巧,java.lang.Integer在Bootstrap ClassLoader的加载范围内,因此BootStrap ClassLoader会把rt.jar包下的Integer加载到内存中,而不是由Application ClassLoader把工程中新建的Integer加载到内存中。
案例二,比如在程序中实现了java.lang.Runnable接口,那么该实现类会由Application ClassLoader加载,而接口Runnable会由Bootstrap ClassLoader加载。
案例三,比如在工程中新建java.lang包,然后在该包下随意建一个类,Application ClassLoader会一直向上委托,知道委托到Bootstrap ClassLoader加载器,Bootstrap ClassLoader一检查包名为java.lang,属于自己的加载范围,但是当Bootstrap ClassLoader进行加载时,虚拟机就会包出安全问题,禁止加载自定义包名为java.lang的类。

案例一、案例二、案例三都遵循了沙箱安全机制,类加载时首先加载java的核心源代码,避免硬盘、网络等其他位置传过来一个与java核心类的包名以及类名相同的类被首先加载,保证了对java核心源代码的保护,这就是沙箱安全机制

双亲委派机制优势:

  • 避免重复加载类;
  • 保护程序安全,防止核心API被串改。

1.3 加载class对象相同条件
类加载器把class字节码文件加载到内存中生成Class对象,JVM中表示两个Class对象是否为同一个类存在的两个必要条件:

  1. 类包名必须一致以及类的名字一致;
  2. 加载这个类的ClassLoader必须相同,在JVM中即使两个Class对象来源同一个Class字节码文件,被同一个虚拟机加载,只要加载他们的ClassLoader对象不同,那么这两个Class对象也不相等。

2、链接

类加载子系统的链接阶段主要分为验证、准备和解析三个步骤。

2.1 验证

验证的目的是保证加载的字节码是合法的、符合规范的。
验证的种类的繁多,java虚拟机大致做以下验证
在这里插入图片描述
其中格式验证是和前面加载阶段一起执行的,也就是说在加载的过程中就伴随着执行格式验证了。
另外符号引用验证的目的是验证符号引用所对应的直接引用是否存在。什么意思呢?java字节码文件中用到的类或者字符串等类信息都是指向了常量池中对应类的符号引用,而在运行过程中,常量池中的符号引用指向堆中具体对象地址。

2.2 准备

当一个类验证通过后就进入准备阶段,准备阶段(preparation)为静态变量分配内存,并将其初始化默认值。java类型对应默认值如下所示,其中boolean在底层是通过int形式实现的,0代表false,1代表true,由于int默认为0,因此boolean默认为false。
在这里插入图片描述
注意:

  1. 为基本类型的静态变量赋默认值,不包括static final修饰的静态常量,因为final修饰的变量在编译时就会分配具体的值;
  2. 类的实例的变量在该阶段不会被初始化,类的实例初始化是要被分配到堆上的,类变量分配在方法区中。

如下代码所示,static代码块中首先引用了变量a,然后才对变量a初始化为了3,这种先引用后定义变量方式看似不合理,实际是可执行的。因为在准备阶段,首先会为变量a分配内存,并初始化为0,然后在下面的2.3节(初始化阶段),自上而下,先执行static块,为变量a赋值10,然后又对变量a显示初始化为3,所以最终结果输出为3。但是不可以在显示声明变量a之前引用变量a,否则会报错“非法前向引用”,但可以在声明之前进行赋值。

public class Demo1 {

    static {
        a = 10;
        //System.out.println(a); //在int a变量声明之前不可以直接饮用,可以直接赋值,否则会报错"非法前向引用"
    }

    private static int a = 3;

    public static void main(String[] args) {
        System.out.print(Demo1.a); 		//3
    }
    
}

2.3 解析

准备阶段后就是解析操作(Resolution),解析就是将类、接口、字段和方法在常量池中的符号引用转为直接引用,也就是类、接口、字段和方法在内存中的指针或者偏移量。比如对于下面一段字节码指令而言,#30#31#32等在字节码加载到内存后,需要转换为内存中的直接引用。

 0 new #30 <com/lzj/classes/Persion>
 3 dup
 4 invokespecial #31 <com/lzj/classes/Persion.<init>>
 7 astore_1
 8 aload_1
 9 iconst_2										#常整数2加载到操作数栈中
10 putfield #32 <com/lzj/classes/Persion.id>	##32代表常量池中field字段,把操作数栈顶元素2赋值给field字段
13 return

JVM虚拟机为每个类都准备了一个方法表,方法表中记录了该类的所有方法。当调用一个类的方法时只需要直到这个方法在方法表中偏移量,通过解析操作,符号引用就可以转换为该方法在方法表中的位置,从而调用该方法。

3、初始化

1. Clinit初始化赋值问题
类的子系统的初始化步骤是要是执行类的构造器方法Clinit(),注意是类的构造器方法,不是初始化对象的构造器,Clinit()不需要客户定义初始化,在该阶段会初始化静态变量以及静态代码块,静态变量和静态代码块从上至下的顺序进行初始化。如下示例所示,首先执行的a=3,然后执行的静态代码块static,最后输出的a的值为10。如果类中没有静态变量或者静态代码块,同时也不会有Clinit()类的初始化方法。

public class Demo1 {

    private static int a = 3;
    
    static {
        a = 10;
    }

    public static void main(String[] args) {
        System.out.print(Demo1.a); 		//10
    }
    
}

注意:只有还有静态变量或者静态代码块的类才会有Clinit初始化方法,用来初始化静态变量或者静态代码块的,除此之外还有init构造方法。类中不含有静态变量或者静态代码块的,只有init构造器。总结起来有如下3中方式不会生成Clinit方法:

  • 类中没有生成任何静态变量或者静态代码块;
  • 类中声明了静态变量,但没有对静态变量进行显示赋值或者没有静态代码块执行;
  • 类中包含静态变量,但是final static形式的,在编译期就会编译到字节码文件中,也不会生成Clinit
    如下代码实例,均不会生成Clint方法
public class Demo {
    //场景一
    public int num = 10;
    //场景二
    public static int num2;
    //场景三
    public static final int num3 = 20;
}

并不是说static显示赋值的都是在Clinit阶段进行初始化,final static赋值的都是编译期赋值的,这种观点是片面的。看如下案例

public class Demo {
    public static int num1 = 10;        //Clinit中初始化
    public static final int num2 = 20;  //编译期赋值

    public static Integer num3 = Integer.valueOf(30);       //Clinit中初始化
    public static final Integer num4 = Integer.valueOf(40); //Clinit中初始化
    public static Integer num5 = 0;                         //Clinit中初始化

    public static final String str = "hello";                       //编译期赋值
    public static final String str1 = new String("world");  //Clinit中初始化
}

该java源码编译成JVM字节码指令后,Clinit方法如下所示

 0 bipush 10
 2 putstatic #2 <com/lzj/load/Demo.num1>
 5 bipush 30
 7 invokestatic #3 <java/lang/Integer.valueOf>
10 putstatic #4 <com/lzj/load/Demo.num3>
13 bipush 40
15 invokestatic #3 <java/lang/Integer.valueOf>
18 putstatic #5 <com/lzj/load/Demo.num4>
21 iconst_0
22 invokestatic #3 <java/lang/Integer.valueOf>
25 putstatic #6 <com/lzj/load/Demo.num5>
28 new #7 <java/lang/String>
31 dup
32 ldc #8 <world>
34 invokespecial #9 <java/lang/String.<init>>
37 putstatic #10 <com/lzj/load/Demo.str1>
40 return

通过以上案例,可以得出如下结论

  1. 对于基本数据类型,显示通过static进行赋值的属性,在Clinit进行初始化;显示通过static final进行赋值的的属性,在编译期就会被编译进字节码文件的。
  2. 对于基本数据类型对应的包装类型,不管是通过static还是static final 进行显示初始化的,都是在Clinit阶段进行初始化的。
    但对于String类型的,如果是对static final类型进行显示赋值的字符串字面量形式,则在编译期进行赋值,如果堆static final类型赋值的是new String形式,则在Clinit阶段初始化。

2. Clinit的继承问题

如果一个类A有父类B,且类A内含有静态变量或者静态代码块,会首先加载B的Clinit()方法,对B中的静态变量或者静态代码块进行初始化,然后加载A的Clinit()方法,对A中的静态变量或者静态代码块进行初始化。其次初始化B中init构造器,最后初始化A中的init构造器。

3. Clinit的线程安全问题

Clinit()方法在多线程并发时会加同步锁,只会有一个线程执行Clinit()方法,并且只会有一个线程执行Clinit()方法,其它线程不会再执行Clinit()方法。如下所示

public class ClinitClass {

    static {
        System.out.println("init ClinitClass");
    }
}

public class Demo1 {


    public static void main(String[] args) {
        Runnable run1 = new Runnable() {
            @Override
            public void run() {
                ClinitClass clinitClass1 = new ClinitClass();
            }
        };
        Runnable run2 = new Runnable() {
            @Override
            public void run() {
                ClinitClass clinitClass2 = new ClinitClass();
            }
        };
        Thread t1 = new Thread(run1);
        Thread t2 = new Thread(run2);
        t1.start();
        t2.start();
    }
}

执行main方法,输出如下,证明Clinit方法只执行了一次,也就是说虚拟机只会执行一次类的静态变量或者静态代码块。

init ClinitClass

4. Clinit的主动与被动初始化
一个类加载到内存中,执行Clinit初始化分为两种情况:主动初始化和被动初始化。

4.1 主动初始化
当一个类或接口在使用前必须要进行初始化,也即类加载到内存后,经过加载、验证、准备、解析阶段后,需要执行类的Clinit方法。主动初始化一个类或接口的Clinit方法主要如下几种情况:

4.1.1 创建一个类的实例时,比如通过new方式、或者反射方式、或者克隆方式、或者反序列化方式

以如下java案例所示

public class InitializationTest {
    public static void main(String[] args) {
        Persion persion = new Persion();
    }
}

class Persion{
    static {
        System.out.println("hello Persion");
    }
}

运行该代码,会输出hello Persion,说明在new一个对象时,执行了该类的Clinit初始化。
也可以通过Persion的Clinit方法的字节码可以看出,如下所示,发现Clinit方法正是执行了输出hello Persion的操作。

0 getstatic #2 <java/lang/System.out>
3 ldc #3 <hello Persion>
5 invokevirtual #4 <java/io/PrintStream.println>
8 return

4.1.2 调用类的静态方法时,即当使用字节码的invokestatic指令

以如下java案例所示

public class InitializationTest {
    public static void main(String[] args) {
        Persion.show();
    }
}

class Persion{
    static {
        System.out.println("hello Persion");
    }
    public static void show(){
        System.out.println("this is method of show");
    }
}

执行main方法后,输出如下所示

hello Persion
this is method of show

在调用Persion的静态方法之前先执行了Persion的Clinit初始化。也可以通过字节码可以看出,main方法的字节码如下所示,正是main方法中通过字节码指令invokestatic调用Persion.show方法触发的调用Persion的Clinit初始化方法。

0 invokestatic #2 <com/lzj/load/Persion.show>
3 return

4.1.3 调用类或者接口的静态字段时(static final类型的特殊考虑),比如使用字节码指令getStatic或者putStatic指令
以如下java案例所示

public class InitializationTest {
    public static void main(String[] args) {
        System.out.println(Persion.num);
    }
}

class Persion{
    static {
        System.out.println("hello Persion");
    }
    public static int num = 10;
}

执行main方法输出如下所示

hello Persion
10

说明在调用Persion的静态变量num之前先执行了persion的Clinit初始化方法。也可以通过字节码文件看出,main方法对应的字节码指令如下所示,其中getstatic #3表示调用Persion的静态变量。

0 getstatic #2 <java/lang/System.out>
3 getstatic #3 <com/lzj/load/Persion.num>
6 invokevirtual #4 <java/io/PrintStream.println>
9 return

Persion中的num是static类型的,如果改成final static类型,此时重新执行main方法是不会初始化Persion类的,因为此时num变成编译期把num的值编译进字节码文件中了,可以参考前面Clinit初始化赋值问题章节。

4.1.4 使用java.lang.reflect进行反射时,比如使用Class.forName(…)
以如下java案例所示

public class InitializationTest {
    public static void main(String[] args) throws ClassNotFoundException {
        Class.forName("com.lzj.load.Persion");
    }
}

class Persion{
    static {
        System.out.println("hello Persion");
    }
}

执行main方法,输出如下所示

hello Persion

说明在执行Persion的反射时,先初始化了Persion的Clinit方法。

4.1.5 调用子类的Clinit进行初始化时,如果父类还没初始化,就先调用父类Clinit进行初始化
以如下java案例所示

public class InitializationTest {
    public static void main(String[] args) {
        System.out.println(Man.num);
    }
}

class Persion{
    static {
        System.out.println("hello Persion");
    }
}

class Man extends Persion{
    static {
        System.out.println("hello man");
    }
    public static int num = 10;
}

执行main方法,输出如下所示,

hello Persion
hello man
10

调用子类的Clinit进行初始化时,如果父类还没初始化,就先调用父类Clinit进行初始化。但是对于接口并不满足这个规则,在初始化一个类时,并不会调用该类实现的接口的Clinit方法进行接口初始化;在初始化一个接口时,也不会调用该接口的父接口的Clinit方法进行初始化。因此一个接口不会因为它的子接口或者它的实现类初始化而初始化,而是当程序首次调用了该接口的静态字段时,才会导致该接口的Clinit方法执行初始化。

4.1.6 如果一个接口定义了default方法,那么直接或间接实现该接口的类的初始化,该接口会在实现类初始化之前进行初始化
以如下java案例所示

public class InitializationTest {
    public static void main(String[] args) {
        System.out.println(Man.num);
    }
}

interface Animal{
    public static Thread t = new Thread(){
        {
            System.out.println("this is animal interface");
        }
    };

    public default void show(){
        System.out.println("this is interface default method");
    }
}

class Man implements Animal{
    public static int num = 10;
}

执行该main方法,输出结果如下,发现执行了Animal的Clinit初始化操作。

this is animal interface
10

4.1.7 当虚拟机启动时,会指定先执行的主类,一般是main方法所在的类,该类首先会执行Clinit方法进行初始化
以如下java案例所示

public class InitializationTest {
    static {
        System.out.println("hello InitializationTest");
    }

    public static void main(String[] args) {
        System.out.println("hello main");
    }
}

运行该main方法,输出结果如下,发现在执行main方法之前先执行了Clinit初始化方法。

hello InitializationTest
hello main

另外从Clinit的字节码也可以看出执行了输出hello InitializationTest

0 getstatic #2 <java/lang/System.out>
3 ldc #5 <hello InitializationTest> 	#加载hello InitializationTest字符串
5 invokevirtual #4 <java/io/PrintStream.println>
8 return

4.2 被动初始化
除了上述类的主动使用时调用Clinit进行初始化,其余情况均为被动使用,不会调用调用Clinit进行初始化。也就是说在代码中使用的类,不一定会被加载并初始化,如果不符合上述主动使用的条件就不会执行Clinit初始化。
被动使用主要包括如下场景:

4.2.1 通过子类引用父类的静态变量时不会导致父类初始化
如前面主动使用初始化的场景中,当访问一个类的静态字段时,会导致该类执行Clinit进行初始化。但当通过子类引用父类的静态变量时不会导致父类初始化。如下案例所示

public class NoInitializationTest {
    public static void main(String[] args) {
        System.out.println(Man.name);
    }
}

class Persion{
    public static String name = "persion";
    static {
        System.out.println("this is Persion class");
    }
}

class Man extends Persion{
    static {
        System.out.println("this is Man class");
    }
}

执行main方法输出结果如下

this is Persion class
persion

只执行父类Persion中的Clinit初始化,说明通过子类调用父类中静态变量并不会导致子类初始化,只会父类进行初始化。

4.2.2 通过数组定义类的引用不会导致类的初始化
通过数组定义类的引用不会导致类执行Clinit进行类初始化,如果对数组中对象进行相关操作才有可能导致类初始化。如下案例所示

public class NoInitializationTest {
    public static void main(String[] args) {
        Man[] mans = new Man[3];
    }
}

class Man {
    static {
        System.out.println("this is Man class");
    }
    public static int num = 10;
}

执行main方法,并不会初始化Man,也不会输出this is Man class内容。但是如果main方法修改成如下形式,对数组中的对象进行操作才会可能导致类进行初始化。

public static void main(String[] args) {
    Man[] mans = new Man[3];
    mans[0] = new Man();
}

4.2.3 引用静态的常量或者类常量不会导致类进行初始化

如下案例所示

public class NoInitializationTest {
    public static void main(String[] args) {
        System.out.println(Man.num);
    }
}

class Man {
    static {
        System.out.println("this is Man class");
    }
    public final static int num = 10;
}

执行main方法,只会输出结果10,并不会输出Man静态代码块中内容,说明Man类没有进行初始化。因为num是在编译期就编译到字节码文件中的,调用不用进行初始化。当然也可从Man中的Clinit字节码可以看出:num并没有在Clinit中初始化。

0 getstatic #2 <java/lang/System.out>
3 ldc #3 <this is Man class>
5 invokevirtual #4 <java/io/PrintStream.println>
8 return

4.2.4 调用ClassLoader类的loadClass()方法加载一个类,并不会触发类的Clinit的初始化
以如下案例所示

public class NoInitializationTest {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader.getSystemClassLoader().loadClass("com.lzj.load.Man");
    }
}

class Man {
    static {
        System.out.println("this is Man class");
    }
}

执行main方法后并不会输出this is Man class,说明通过ClassLoader加载Man时,Man并没有进行初始化,这一点是与Class.forName有区别的。

4 类的使用

经过加载、链接、初始化三个步骤后,类已经被加载到内存并被初始化,接下来就可以使用类了,比如可以new出这个类的对象,然后通过对象执行操作。
类的初始化与对象的初始化区别
第3部分详细介绍的都是类的初始化,都是初始化类的静态变量和静态代码块,类的初始化也就是执行了类的Clinit方法;
而对象的初始化是当类已经被加载到内存后执行完类的初始化步骤后才开始执行的。先有了类的初始化才有了对象的初始化。

5 卸载

方法区中的垃圾回收主要分为两部分内容:常量池中废弃的常量和不再使用的类模板。废弃的常量回收很简单,就是没有任何地方引用的就可以被回收 了,下面主要分析类模板是如何卸载的。
以如下案例所示,对于Persion类被加载到内存后,会把Persion的二进制模板存储到方法区中,并在堆中创建一个关于Persion类的Class对象,而通过Persion的Class对象可以获取到加载这个类的类加载器相反通过加载器也可以获取加载的Person的Class对象;通过Persion类的Class对象可以创建关于Persion的实例对象,通过实例对象可以获取对应的Class对象。另外堆中对象都有与之关联的引用变量。图中,双向箭头表示两者可以相互获取的。

在这里插入图片描述
要想卸载Persion类,其实就是要把方法区中关于Persion的二进制模板结构卸载掉。想要卸载Persion的二进制结构,就需要保证没有引用可以达到Persion的Class对象,只断掉图中1所示的线是不够的,因为还可以从Persion对象的引用变量可以获取堆中Persion对象,通过Persion对象又可以获取Persion对象对应的Class对象。因此要想没有任何引用可以达到Persion对象对应的Class对象,需要把栈中下面图中1 、2、3根线全部断掉后(即3个栈中的引用变量),3断掉后才会导致ClassLoader对象有可能被回收,导致5被断掉,2断掉后才会导致Persion对象可能被回收,导致4断掉,最终才能保证没有引用可以到Persion对象对应的Class对象,就可以卸载方法区中的persion的二进制模板了。
在这里插入图片描述

根据上述案例,如果要下载一个类模板需要满足以下3个条件:

  • 在堆中不存在该类的实例以及派生子类的实例对象(在栈中没有该类以及派生子类对象的引用变量,堆中该类的实例以及派生子类的实例对象才有可能被回收);
  • 加载该类的加载器已经被回收,这个条件很难实现,因为一个加载器可能会加载非常多的类,即使一个类执行结束了等待被回收,但还有其它非常多的类在使用,依然不能回收该类加载器;像BootLoader加载器根本不可能卸载,因为它的生命周期是与JVM是一致的。
  • 该类的java.lang.Class对象没有在任何地方使用,无法在任何地方通过反射访问该类的方法。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值