Android工程师学习JVM(四)-类加载、连接、初始化、卸载

852 阅读11分钟

前言

在学习JVM这个系列文章中,已经讲解了JVM规范、Class文件格式以及如何阅读字节码、ASM字节码处理。本篇将和大家一起学习类生命周期相关知识。这部分知识对于日常开发一些神奇现象有很重要的作用,类加载器部分对于自定义类加载器非常重要,而自定义类加载器在Android中开发热修复、插件相关过程非常重要

如果你对JVM、字节码、Class文件格式、ASM字节码处理有兴趣的话,可以看之前的文章哈,相信会收获更多哦

Android工程师学习JVM(三)-字节码框架ASM使用

Android工程师学习JVM(二)-教你阅读Java字节码

Android工程师学习JVM(一)-JVM概述

1、类的生命周期

加载:查找并加载类文件的二进制数据

连接:就是将已经读入内存的类的二进制数据合并到JVM运行时环境中去,包含如下几个步骤:

1)验证:确保被加载类的正确性

2)准备:为类的静态变量分配内存,并初始化它们

3)解析:把常量池中的符号引用转换为直接引用

初始化:为类的静态变量赋初始值

使用:运行过程中使用

卸载:当代表类的Class对象不再被引用,不可触达时,Class对象结束生命周期,类在方法区中的数据被卸载,结束生命周期

2、类加载和类加载器

2.1、类加载要完成的功能

1、通过类的全限定名来获取该类的二进制字节流

2、把二进制字节流转化为方法区的运行时数据结构

3、在堆上创建一个java.lang.Class对象,用来封装类在方法区内的数据结构,并向外提供访问方法区内数据结构的接口

2.2、加载类的方式

1、最常见的方式:本地文件加载,如加载jar包

2、动态的方式:将java源文件动态编译成class

3、其他方式:网络下载、从专有数据库中加载

2.3、类加载器

本文类加载器以jdk1.8为例(因为当前Android支持的到jdk1.8),9以后的扩展类加载器调整为平台类加载器了哈~

启动类加载器:负责将<JAVA_HOME>/lib,或者-Xbootclasspath参数指定的路径中的,且是虚拟机识别的类库加载到内存中(是按照名字识别的,比如rt.jar,对于不能识别的文件不予装载)

扩展类加载器:负责加载<JRE_HOME>/lib/ext,或者java.ext.dirs系统变量所指定路径中的所有类库

应用程序类加载器:负责加载classpath路径中的所有类库

注意:父加载器和子加载器并不是继承关系,而是组合关系

这么多类加载器,那么我要加载一个类时,是以什么样的机制交给这些类加载器进行的呢?

2.4、双亲委派模型

案例一:当我们的程序代码首次使用到String类

1、首先是自定义的类加载器收到了加载String类的要求。自己加载的类中没有String,此时不会直接去加载String类,而是委托给它的父加载器加载,也就是AppClassLoader

2、AppClassLoader,没加载过String类,委托给父加载器加载,即ExtensionClassLoader

3、ExtensionClassLoader,没加载过String类,委托给BootstrapClassLoader

4、BootstrapClassLoader也想委托给它的父加载器,谁想自己干活呢是不。可是没办法啊,它没有父加载器了,所以只能自己想办法加载,在自己的类加载路径(<JAVA_HOME>/lib,或者-Xbootclasspath参数指定的路径)中查找,找到了String类,进行加载,然后将Class给ExtensionClassLoader。

5、ExtensionClassLoader又给了AppClassLoader,AppClassLoader又给了自定义类加载器,程序就有String.class可以用啦

注意这里是首次加载String类,如果已经加载过,那么一路委托到BootstrapClassLoader后,BootstrapClassLoader加载过了直接返回String类就可以,不用重新在类路径中加载了

案例二:当我们的程序首次用到类A(用户自定义的类)

1、首先还是自定义类加载器接收到了加载类A的要求。此时委托给AppClassLoader

2、AppClassLoader二话不说委托给ExtensionClassLoader

3、ExtensionClassLoader委托给BootstrapClassLoader

4、BootstrapClassLoader,苦命没得委托,在自己的类加载路径下查找。哦吼,找不到!!!!!!怎么办呢???搞不定谁委托我的谁自己干,跟ExtensionClassLoader类加载器回复找不到

5、ExtensionClassLoader收到BootstrapClassLoader说找不到后。莫得办法自己找找,哦吼,也找不到,回复AppClassLoader找不到

6、AppClassLoader收到ExtensionClassLoader说找不到,开始自己找,可它也找不到。回复自定义类加载器

7、自定义类加载器收到AppClassLoader说找不到后,没得办法,一路没一个靠谱的,自己来找,自定义类加载器在自定的类路径下查找,找到了加载上就可以用。找不到的话就是整个链路都找不到,抛出ClassNotFoundException

至此,类的双亲委派机制,你清楚了吗?

双亲委派机制的一些说明

1、双亲委派对于保证Java程序的稳定运行很重要。(如上文提到的加载String类,按照机制都会用的是BootstrapClassLoader加载的String类)

2、实现双亲委派机制的代码再java.lang.ClassLoader的loadClass()方法中,如果自定义类加载器,除非想要破坏双亲委派,否则推荐覆盖findClass()方法

3、定义类加载器:加载某个类的加载器

初始化类加载器:能成功返回该类Class的类加载器

如String类的定义类加载器是BootstrapClassLoader,初始化类加载器有BootstrapClassLoader、ExtensionClassLoader、AppClassLoader、自定义类加载器等

实操自定义类加载器

我们在代码中写一个TestClass类,全限定名为com.restart.classloader.TestClass

自定义类加载器代码如下:

public class MyClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //查找类,加载为byte字节流
        byte[] data = loadData(name);
        if (data != null) {
            //将byte字节流构建为Class对象,对应前文说的加载流程哦
            return defineClass(name, data, 0, data.length);
        }
        return super.findClass(name);
    }

    private byte[] loadData(String name) {
        try {
         	//将点转换为文件分隔符
            String fileName = name.replace(".", File.separator);
            //在当前路径下的classes目录中查找类
            File tempFile = new File("classes" + File.separator + fileName + ".class");
            if (tempFile.exists()) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                FileInputStream fis = new FileInputStream(tempFile);
                int a = 0;
                while ((a = fis.read()) != -1) {
                    baos.write(a);
                }
                return baos.toByteArray();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;

    }
}

测试代码

public class Main {

    public static void main(String[] args) throws ClassNotFoundException {
        MyClassLoader myClassLoader = new MyClassLoader();
        Class cls = myClassLoader.loadClass("com.restart.classloader.TestClass");
        System.out.println(cls.getClassLoader().toString());
    }

}

运行结果打印出来的ClassLoader是:AppClassLoader。哦吼????为啥不是MyClassLoader

分析:按照上文说的双亲委派机制,当MyClassLoader收到加载TestClass的要求时,首先是委派给了AppClassLoader,一路委派后,最终回到AppClassLoader加载,TestClass是项目中的类,AppClassLoader能加载到就给加载了。于是,这个类就是AppClassLoader加载的。

那我要实现MyClassLoader加载怎么办???将TestClass从java源文件中删除,将刚才编译生成的TestClass.class包括目录com、restart、classloader复制到classes目录下。

再执行一次程序,发现打印出来的ClassLoader是MyClassLoader了。

3、类连接

3.1、验证

类文件结构检查:按照JVM规范规定的类文件结构进行

元数据验证:对字节码描述的信息进行语义分析,保证其符合Java语言规范要求(比如:类的父类是否允许继承)

字节码验证:通过对数据流和控制流分析,确认程序语义是合法的和符合逻辑的,主要是对方法体进行验证

符号引用验证:对类自身以外的信息进行验证,也就是对常量池中的各种符号引用,进行匹配校验

3.2、准备

该过程是为类的静态变量分配内存和初始化

3.3、解析

解析是把常量池中的符号引用转换为直接引用的过程,包括:符号引用:以一组无歧义的符号来描述所引用的目标,与虚拟机的实现无关

直接引用:直接指向目标的指针,相对偏移量,或是能间接定位到目标的句柄,是和虚拟机实现相关的

主要针对:类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符

4、初始化

4.1、类的初始化规则

类的初始化是为类的静态变量赋初始值,或者说执行类构造器的过程。初始化有如下几条规则:

1、如果类还没有加载和连接,就先加载和连接

2、如果类存在父类,且父类没有初始化,就先初始化

3、如果类中存在初始化语句,就依次执行初始化语句

4、如果是接口的话

​ a、初始化一个类的时候,并不会先初始化它实现的接口

​ b、初始化一个接口时,并不会初始化它的父接口

​ c、只有当程序首次使用接口里面的变量或者是调用接口方法的时候,才会导致接口初始化

5、调用ClassLoader类的loadClass方法来装载一个类的时候,不会调用类的初始化,因为这种情况不是主动调用

前四种比较好理解,第5条规则咱们举个例子说明一下

public class TestClass {
    static {
        System.out.println("TestClass init");
    }
}

//使用类加载器加载类
public static void main(String[] args) throws ClassNotFoundException {
      MyClassLoader myClassLoader = new MyClassLoader();
      //加载类
      Class cls = myClassLoader.loadClass("com.restart.classloader.TestClass");
}

实际测试发现,并没有打印出"TestClass init"这个字符串

下面咱们来详细看下,什么时候会执行主动初始化

4.2、类的主动初始化

Java程序对类的使用方式分为:主动使用和被动使用,JVM必须在每个类或接口"首次主动使用"时才会初始化它们。主动使用的情况:

1、创建类实例

2、访问某个类或接口的静态变量

3、调用类的静态方法

4、反射某个类。Class.forName("xxx"),就会执行初始化了

5、初始化某个子类,而父类还没有初始化

6、JVM启动的时候运行的主类

7、如果接口中有default方法,当接口实现类初始化时,接口就会初始化(和上面说的初始化一个类的时候,并不会初始化它实现的接口作对比哦,有default方法的接口是特例)

4.3、类的初始化机制和顺序

咱们先看个案例

public class MyClassA {

    private static MyClassA myClassA = new MyClassA();
    private static int a = 0;
    private static int b;

    private MyClassA() {
        a++;
        b++;
    }

    public static MyClassA getInstance() {
        return myClassA;
    }

    public int getA() {
        return a;
    }

    public int getB() {
        return b;
    }
}

乍一看很平凡的一个单例写法。让我们来看下使用时,a和b的初始值

    public static void main(String[] args) throws ClassNotFoundException {
        MyClassA myClassA = MyClassA.getInstance();
        System.out.println("a == " + myClassA.getA());
        System.out.println("b == " + myClassA.getB());
    }

结果很奇怪,并不是a==1,b==1。而是a==0,b==1

这就跟初始化顺序很有关系了。

1、在连接准备阶段,给静态变量a和b分配内存,并初始化为0。而后到了初始化阶段,按顺序执行静态代码

2、执行MyClassA myclassA = new MyClassA(), 此时会调用构造方法,执行a++,b++,此时a=1,b=1

3、执行int a = 0, 此时a=0,b=1

4、int b,由于没有对b进行赋值,此时依然是a=0,b=1

这样的一个单例结果往往并不是我们想要的,最好是把赋值操作放在实例化之前

5、类的卸载

当代表一个类的Class对象不再被引用,那么Class对象的生命周期就结束了,对应的在方法区中的数据也会被卸载

JVM自带的类加载器装载的类,是不会卸载的,由用户自定义的类加载器加载的类是可以卸载的

6、小结

1、类的生命周期包括加载、连接、初始化、使用、卸载

2、类加载过程就是将数据转换为Class对象的过程,加载过程使用双亲委派机制

3、类连接过程包含验证、解析、准备。其中解析阶段会初始化类的静态变量

4、类初始化过程分主动和被动,只有主动调用才会执行初始化过程

5、类卸载是JVM执行的,一般不需要关心