类的加载、连接和初始化

 一,JVM和类

当我们调用Java命令运行某个Java程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都处于该Java虚拟机进程里。同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区。当Java程序运行结束时,JVM进程结束,该进程在内存中的状态将会丢失。

当系统出现以下几种情况时,JVM进程将被终止。

  • 程序运行到最后正常结束。
  • 程序运行到使用System.exit()或Runtime.getRuntime().exit()代码处结束程序。
  • 程序执行过程中遇到未捕获的异常或错误而结束。
  • 程序所在平台强制结束了JVM进程。

二,类的加载,连接和初始化

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)。当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这3个步骤,所以有时也把这3个步骤统称为类加载或类初始化。

1.类的加载

类加载指的是将类的class文件读入内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。

类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。

通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。

  • 从本地文件系统加载class文件。
  • 从JAR包加载class文件,例如JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
  • 通过网络加载class文件。
  • 把一个Java源文件动态编译,并执行加载。

类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

2.类的连接

当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。

  1. 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
  2. 准备:类准备阶段则负责为类的静态Field分配内存,并设置默认初始值。
  3. 解析:将类的二进制数据中的符号引用替换成直接引用。(符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。)

3.类的初始化

在类的初始化阶段,虚拟机负责对类进行初始化,主要就是对静态Field进行初始化。在Java类中对静态Field指定初始值有两种方式:① 声明静态Field时指定初始值;② 使用静态初始化块为静态Field指定初始值。

JVM初始化一个类包含如下几个步骤。

  1. 假如这个类还没有被加载和连接,则程序先加载并连接该类。
  2. 假如该类的直接父类还没有被初始化,则先初始化其直接父类。
  3. 假如类中有初始化语句,则系统依次执行这些初始化语句。

当执行第2个步骤时,系统对直接父类的初始化步骤也遵循此步骤1~3;如果该直接父类又有直接父类,则系统再次重复这3个步骤来先初始化这个父类……依此类推,所以JVM最先初始化的总是java.lang.Object类。当程序主动使用任何一个类时,系统会保证该类以及所有父类(包括直接父类和间接父类)都会被初始化。

类初始化的时机

当Java程序首次通过下面6种方式来使用某个类或接口时,系统就会初始化该类或接口。

  • 创建类的实例。为某个类创建实例的方式包括:使用new操作符来创建实例,通过反射来创建实例,通过反序列化的方式来创建实例。
  •  调用某个类的静态方法。
  • 访问某个类或接口的静态Field,或为该静态Field赋值。
  • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。
  • 初始化某个类的子类。当初始化某个类的子类时,该子类的所有父类都会被初始化。
  • 直接使用java.exe命令来运行某个主类。当运行某个主类时,程序会先初始化该主类。

除此之外,下面的几种情形需要特别指出。

  • 对于一个final型的静态Field,如果该Field的值在编译时就可以确定下来,那么这个Field相当于“宏变量”。Java编译器会在编译时直接把这个Field出现的地方替换成它的值,因此即使程序使用该静态Field,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该Field的值,如果通过该类来访问它的静态Field,则会导致该类被初始化。
  • 当使用ClassLoader类的loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化。使用Class的forName()静态方法才会导致强制初始化该类。

三,类加载器

类加载器负责将.class文件(可能在磁盘上,也可能在网络上)加载到内存中,并为之生成对应的java.lang.Class对象。

类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。现在的问题是,怎么样才算“同一个类”?正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person、pg、kl)。这意味着两个类加载器加载的同名类:(Person、pg、kl)和(Person、pg、kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

当JVM启动时,会形成由3个类加载器组成的初始类加载器层次结构。

  • Bootstrap ClassLoader:根类加载器。
  • Extension ClassLoader:扩展类加载器。
  •  System ClassLoader:系统类加载器。

Bootstrap ClassLoader被称为引导(也称为原始或根)类加载器,它负责加载Java的核心类。根类加载器非常特殊,它并不是java.lang.ClassLoader的子类,而是由JVM自身实现的。

Extension Classloader被称为扩展类加载器,它负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext或者由java.ext.dirs系统属性指定的目录)中JAR包的类。通过这种方式,就可以为Java扩展核心类以外的新功能,只要我们把自己开发的类打包成JAR文件,然后放入JAVA_HOME/jre/lib/ext路径即可。

System Classloader被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性,或CLASSPATH环境变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以系统类加载器作为父加载器。

类加载机制,JVM的类加载机制主要有如下3种。

  • 全盘负责。所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
  • 父类委托。所谓父类委托,则是先让parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
  • 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

注意:类加载器之间的父子关系并不是类继承上的父子关系,这里的父子关系是类加载器实例之间的关系。

除了可以使用Java提供的类加载器之外,开发者也可以实现自己的类加载器,自定义的类加载器通过继承ClassLoader来实现。

类加载器加载Class大致要经过如下8个步骤。

  1. 检测此Class是否载入过(即在缓存区中是否有此Class),如果有则直接进入第8步,否则接着执行第2步。
  2. 如果父类加载器不存在(如果没有父类加载器,则要么parent一定是根类加载器,要么本身就是根类加载器),则跳到第4步执行;如果父类加载器存在,则接着执行第3步。
  3. 请求使用父类加载器去载入目标类,如果成功载入则跳到第8步,否则接着执行第5步。
  4. 请求使用根类加载器来载入目标类,如果成功载入则跳到第8步,否则跳到第7步。
  5. 当前类加载器尝试寻找Class文件(从与此ClassLoader相关的类路径中寻找),如果找到则执行第6步,如果找不到则跳到第7步。
  6. 从文件中载入Class,成功载入后跳到第8步。
  7. 抛出ClassNotFoundException异常。
  8. 返回对应的java.lang.Class对象。

其中,第5、6步允许重写ClassLoader的findClass()方法来实现自己的载入策略,甚至重写loadClass()方法来实现自己的载入过程。

四,创建并使用自定义的类加载器

JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过继承ClassLoader抽象类,并重写该ClassLoader所包含的方法来实现自定义的类加载器。ClassLoader类有如下两个关键方法。 loadClass(String name, boolean resolve):该方法为ClassLoader的入口点,根据指定的二进制名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。findClass(String name):根据二进制名称来查找类。

如果需要实现自定义的ClassLoader,则可以通过重写以上两个方法来实现,当然我们推荐重写findClass()方法,而不是重写loadClass()方法。loadClass()方法的执行步骤如下:

  1. 用findLoadedClass(String) 来检查是否已经加载类,如果已经加载则直接返回。
  2. 在父类加载器上调用loadClass()方法。如果父类加载器为null,则使用根类加载器来加载。
  3. 调用findClass(String)方法查找类。

从上面步骤中可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂。

在ClassLoader里还有一个核心方法:Class defineClass(String name, byte[] b, intoff, int len),该方法负责将指定类的字节码文件(即Class文件,如Hello.class)读入字节数组byte[] b内,并把它转换为Class对象,该字节码文件可以来源于文件、网络等。defineClass()方法管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性等。不过不用担心,程序员无须重写该方法。事实上,该方法是final型,即使我们想重写也没有机会。

除此之外,ClassLoader里还包含如下一些普通方法。

  • findSystemClass(String name):从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass()方法将原始字节转换成Class对象,以将该文件转换成类。
  • static getSystemClassLoader():这是一个静态方法,用于返回系统类加载器。
  • getParent():获取该类加载器的父类加载器。
  • resolveClass(Class c):链接指定的类。类加载器可以使用此方法来链接类c。读者无须理会关于此方法的太多细节。
  • findLoadedClass(String name):如果此Java虚拟机已加载了名为name的类,则直接返回该类对应的Class实例,否则返回null。该方法是Java类加载缓存机制的体现。
package com.example.demo.ClassLoader;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

// 自定义的ClassLoader,该ClassLoader通过重写findClass()方法来实现自定义的类加载机制。
// 这个ClassLoader可以在加载类之前先编译该类的源文件,从而实现运行Java之前先编译该程序的目标,这样即可通过该ClassLoader直接运行Java源文件。
public class CompileClassLoader extends ClassLoader {

    // 读取一个文件的内容
    private byte[] getBytes(String fileName) {
        File file = new File(fileName);
        long len = file.length();
        byte[] raw = new byte[(int) len];
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            // 一次读取Class文件的全部二进制数据
            int r = fileInputStream.read(raw);
            if (r != len) {
                throw new IOException("无法读取全部文件: " + r + " != " + len);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return raw;
    }

    //定义编译指定Java文件的方法
    private boolean compile(String javaFile) throws IOException {
        System.out.println("CompileClassLoader:正在编译" + javaFile + "...");
        // 调用系统的javac命令
        Process p = Runtime.getRuntime().exec("javac " + javaFile);
        try {
            // 其它线程都等待这个线程完成
            p.waitFor();
        }catch (Exception e){
            System.out.println(e);
        }
        // 获取javac线程的退出值
        int ret = p.exitValue();
        // 返回编译是否成功
        return ret == 0;
    }

    // 重写ClassLoader的findClass方法
    protected Class findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        // 将包路径中的点(.)替换成斜线(/)
        String fileStub = name.replace(".","/");
        String javaFilename = fileStub + ".java";
        String classFilename = fileStub + ".class";
        File javaFile = new File(javaFilename);
        File classFile = new File(classFilename);
        // 当指定Java源文件存在,且class文件不存在,或者Java源文件的修改时间比class文件的修改时间更晚时,重新编译
        if (javaFile.exists() && !classFile.exists() || javaFile.lastModified() > classFile.lastModified()) {
            try {
                // 如果编译失败,或者该class文件不存在
                if(!compile(javaFilename) || !classFile.exists()){
                    throw new ClassNotFoundException("ClassNotFoundException: " +javaFilename);
                }
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        // 如果class文件存在,系统负责将该文件转换成class对象
        if (classFile.exists()) {
            // 将class文件的二进制数据读入数组
            byte[] raw = getBytes(classFilename);
            // 调用ClassLoader的defineClass方法将二进制数据转换成Class对象
            clazz = defineClass(name,raw,0,raw.length);
        }
        // 如果clazz为null,表明加载失败,则抛出异常
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

    // 定义一个主方法
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, ClassNotFoundException {
        // 如果运行该程序时没有参数,既没有目标类
        if (args.length < 1) {
            System.out.println("缺失目标类,请按如下格式运行Java源文件: ");
            System.out.println("java CompileClassLoader ClassName");
        }
        // 第一个参数是需要运行的类
        String progClass = args[0];
        // 剩下的参数将作为运行目标类时的参数,将这些参数复制到一个新数组中
        String[] progArgs = new String[args.length - 1];
        // 原数组 从元数据的起始位置开始 目标数组  目标数组的开始起始位置 要copy的数组的长度
        System.arraycopy(args,1,progArgs,0,progArgs.length);
        CompileClassLoader ccl = new CompileClassLoader();
        // 加载需要运行的类
        Class clazz = ccl.loadClass(progClass);
        // 获取需要运行的类的主方法
        Method main = clazz.getMethod("main", String[].class);
        Object argsArray[] = {progArgs};
        main.invoke(null,argsArray);
    }
}

Java为ClassLoader提供了一个URLClassLoader实现类,该类也是系统类加载器和扩展类加载器的父类(此处是父类,就是指类与类之间的继承关系)。URLClassLoader功能比较强大,它既可以从本地文件系统获取二进制文件来加载类,也可以从远程主机获取二进制文件来加载类。实际上,在应用程序中可以直接使用URLClassLoader来加载类,URLClassLoader类提供了如下两个构造器。 URLClassLoader(URL[] urls):使用默认的父类加载器创建一个ClassLoader对象,该对象将从urls所指定的系列路径来查询并加载类。URLClassLoader(URL[] urls, ClassLoader parent):使用指定的父类加载器创建一个ClassLoader对象,其他功能与前一个构造器相同。一旦得到了URLClassLoader对象之后,就可以调用该对象的loadClass()方法来加载指定类。

package com.example.demo.ClassLoader;

import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Connection;
import java.sql.Driver;
import java.util.Properties;

// 示范了如何直接从文件系统中加载MySQL驱动,并使用该驱动来获取数据库连接。通过这种方式来获取数据库连接,可以无须将MySQL驱动添加到CLASSPATH环境变量中。
public class URLClassLoaderTest {
    private static Connection conn;

    // 定义一个获取数据库连接的方法
    public static Connection getConn(String url, String user, String pass) throws Exception {
        if (conn == null) {
            // 创建一个URL数组
            URL[] urls = {new URL("file:mysql-connector-java-3.1.10-bin.jar")};
            // 以默认的ClassLoader作为父ClassLoader,创建URLClassLoader
            URLClassLoader myClassLoader = new URLClassLoader(urls);
            // 加载MySQL的JDBC驱动,并创建默认实例
            Driver driver = (Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").newInstance();
            // 创建一个设置JDBC连接属性的Properties对象
            Properties props = new Properties();
            // 至少需要为该对象传入user和password两个属性
            props.setProperty("user", user);
            props.setProperty("password", pass);
            // 调用Driver对象的connect方法来取得数据库连接
            conn = driver.connect(url, props);
        }
        return conn;
    }

    public static void main(String[] args) throws Exception {
        System.out.println(getConn("jdbc:mysql://localhost:3306/my_demo?characterEncoding=UTF-8&useSSL=false&useUnicode=true&allowMultiQueries=true&autoReconnect=true&serverTimezone=GMT%2B8",
                "root","123456"));
    }
}

文章内容来源至《疯狂Java讲义(第二版)》

你可能感兴趣的:(类的加载、连接和初始化)