Java类加载篇


Java类加载

这部分知识比较深入底层,将重点介绍类加载和反射,会提到JDK动态代理、AOP,反射等诸多知识点。当调用Java命令允许程序时,该命令会启动多个线程,它们都处于该Java虚拟机进程里。所有线程、变量处于同一个进程里,它们都使用JVM进程的内存区。当出现以下情况,进程将终止:

  • 程序结束
  • 使用System.exit()Runtime.getRuntime().exit()代码
  • 未捕获到异常
  • 强制结束进程
public class A{
    public static int a = 6;
}
public class Test1{
    public static void main(String[] args){
        A a = new A();
        a.a++;
        System.out.println(a.a);
    }//输出7
}
public class Test2{
    public static void main(String[] args){
        A b = new A();
        System.out.println(b.a);
    }//输出6
}

上述代码表明,不同的进程之间是不会共享资源的,运行Test1Test2是运行两次进程,所以第二次依然重新初始化A类。

类的加载

当需要使用某个类时,系统会通过加载、连接、初始化三个步骤完成对类的初始化类加载指的是类加载器将类的class文件读入内存,并为之创建一个java.lang.Class对象。类加载器通常由JVM提供,也称系统类加载器,除此之外,开发者可以通过继承ClassLoader基类来创建类加载器。使用不同类加载器,可以从不同来源加载类的数据通常有以下来源:

  • 本地加载class文件
  • JAR包中加载class文件
  • 网络加载
  • 把一个Java源文件动态编译并加载
类的连接

创建了Class对象后,系统将二进制数据合并到JRE中,这一过程可分为三个阶段:

  1. 验证:验证加载的类是否有正确的内部结构
  2. 准备:为类变量分配内存,并设置默认值
  3. 解析:将类的二进制数据中的符号引用替换成直接引用
类初始化

在初始化阶段,JVM负责对类进行初始化,主要对类变量进行初始化。初始化有两种方式:

  • 声明变量时就指定初始值
  • 使用静态代码块指定初始值

JVM初始化一个类是按照一定规则进行的,如下:

  • 如果没有加载和连接,则先加载并连接该类
  • 如果父类没有初始化,则优先初始化其父类
  • 如果有初始化语句,则系统先执行初始化语句

所以JVM总是最先初始化java.lang.Object类,并顺着继承链依次加载并初始化类。程序通过以下6种方式来使用某个类和接口时,系统就会初始化该类或接口:

  • 创建类实例。包括:使用new关键字;反射创建;反序列化创建。
  • 调用类方法。
  • 访问类变量或赋值。
  • 反射强制创建某个类或接口对应的java.lang.Class对象。(如果还未初始化该类)
  • 初始化某类的子类。
  • 使用java.exe命令运行某个类。

对于final型的类变量,如果该类变量的值在编译时就确定下来,那么这个类变量相当于“宏变量”。因此使用静态类变量也不会导致该类初始化,相当于使用常量。如果final修饰的变量不能确定下来,必须等到运行时确定,则将使该类初始化。

类加载器

类加载器负责将.class文件加载到内存中,并生成一个java.lang.Class实例,一旦一个类被加载到JVM中,同一个类就不会被再次载入。所谓同一个类指的是有唯一标识的类,唯一标识是用全限定类名作为载入标识。在JVM中一个类用其全限定类名和其类加载器作为其全限定类名,例如:(类、package、加载器实例)。

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

  • Bootstrap ClassLoader:根类加载器(不是Java实现,一般无法访问)
  • Extension ClassLoader:扩展类加载器
  • System ClassLoader:系统类加载器

Bootstrap ClassLoader也叫引导(启动)类加载器,负责加载Java核心类。它不是java.lang.ClassLoader的子类,而是由JVM自己实现。

public class BootstrapTest{
    public static void main(String[] args){
        URLS[] u = sun.misc.Launcher.getBootstrapClassPath().getURLS();
        //遍历输出根类加载器的全部URL
        for(int i=0;i<u.length;i++){
            System.out.println(u[i].toExternalForm());
        }
    }
}

Extension ClassLoaderExtClassLoader(sun.misc.Launcher$ExtClassLoader)的实例,负责加载JRE的扩展目录中的JAR包的类,通过这种方式可以为Java扩展核心类以外的功能,只要把开发的类打包成JAR包即可,然后放入JAVA_HOME/jre/lib/ext路径即可。

System ClassLoader,应用类加载器AppClassLoader的实例,它负责在JVM启动时加载来自命令-classpathCLASSPATH环境变量所指定的JAR包和类路径。可以通过调用ClassLoadergetSystemClassLoader()方法来获取系统类加载器。

系统类加载器是当前路径,扩展类加载器的加载路径是JAVA_HOME/jre/lib/ext,所以可以说其父加载器为null也不为过,getParent()方法返回null,但根加载器可以作为其父加载器。

类加载机制

加载机制有三种:
  • 全盘委托:当某加载器加载一个类时,该类所引用和依赖的其他类也将由该类加载器负责加载。
  • 父类(双亲)委托:先让父类加载器加载class,如果无法加载则从自己的类路径中加载。
  • 缓存机制:保证所有已加载过的类都被缓存,当需要使用某个类时,加载器先从缓存区加载该类,如不存在才会加载对应的二进制数据,并将其转换成Class对象存入内存。(所以修改某个类后需重启JVM才会生效)

一个类加载器查找classresource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了,直接返回;如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。

JDK搜索类的方式

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是所谓的//继承的关系,是一个包含的关系),虚拟机内置的类加载器Bootstrap ClassLoader本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器,但是访问ExtClassLoader的父加载器返回null

当一个ClassLoader实例需要加载某个类时,它会试图在亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给AppClassLoader进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的实例对象。

好处:因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要让子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心API中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrap ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String类,除非你改变JDK中ClassLoader搜索类的默认算法。

类加载步骤
  1. 检查是否有载入过。
  2. 如果父类加载器不存在,则使用根类加载器来载入类并返回对应java.lang.Class对象,否则执行第5步;如果存在则使用父类加载器去加载类并返回Class对象,不成功则执行第3步。
  3. 当前加载器从与它相关的类路径中寻找,找到就执行第4步,否则执行第5步。
  4. 从文件中载入Class,成功后同样返回Class对象。
  5. 抛出ClassNotFoundException异常。

JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器。同时,即使用户自定义类加载器不指定父类加载器,那么同样可以加载到/lib下的类。自定义类加载器不指定父类加载器是默认系统类加载器为父类加载器,按照双亲委派加载。若强制父类加载器为null,则其他加载器就可能不被加载。

加载流程为:系统类加载器–>扩展类加载器–>启动类加载器,强制设置parentnull时关系就已经断了 源代码中走findBootstrapClassOrNull(name)加载

创建自定义类

JVM中除根加载器外,其他都是ClassLoader抽象类的子类实例,开发者可以扩展其子类并重写所含方法来实现自定义类加载器。类加载器有两个关键方法:

  • loadClass(String name,boolean resolve):类加载器的入口点,根据指定名称加载类,并获取Class对象。
  • findClass(String name):根据指定名称查找类。

如果需要实现自定义类加载器,则可以通过重写以上两个方法来实现。loadClass()方法的执行步骤如下:

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

重写finsClass()方法可以避免覆盖默认类加载器的双亲委托、缓存机制,在ClassLoader里还有一个核心方法Class defineClass(String name,byte[] b,int off,int len),负责将类的字节码文件读入字节数组byte[] b内,并把它转化为Class对象。defineClass()方法管理JVM的许多复杂的实现,不能被重写,因为是final的。除此外还有一些普通方法如下:

  • findSystemClass(String name):从本地文件系统装入文件,存在就用defineClass()方法将字节码转换成Class对象。
  • static getSystemClassLoader():返回系统类加载器
  • getParent():获取父类加载器。
  • resolveClass(Class<?> c):链接指定类。
  • findLoaderClass(String name):如果已加载该类,则返回Class实例,否则返回null
使用自定义类的好处

它能用来实现以下功能:

  • 执行代码前自动验证数字签名
  • 根据提供的密码解密代码,避免反编译
  • 动态加载类
  • 把数据以字节码形式加载到应用中

URLClassLoader实现类

该类是SystemClassLoaderExtClassLoader的父类,注意不同上面的所谓“父类”,这里是类之间的继承关系。有如下两个构造器:

  • URLClassLoader(URL[] urls):使用默认父类加载器创建一个ClassLoder对象,并从指定路径加载类。
  • URLClassLoader(URL[] urls,ClassLoader parent):不同上面的构造器,它可以指定一个父类加载器加载类。

获得URLClassLoader对象后,就可以调用loadClass()方法来加载指定类。

private static Connection coon;
public static Connection getConn(String url,String user,String pass) throws Exception{
    if(conn = null){
        URL[] urls = {new URL("file:mysql-connector-java-5.1.30-bin.jar")};
        URLClassLoader myClassLoader = new URLClassLoader(urls);
        //加载MySQL的JDBC驱动,并创建一个实例
        Driver driver = (Driver)myClassLoader.loaderClass("com.mysql.jdbc.Driver").newInstance();
        //创建一个设置JDBC连接属性的properties对象
        Properties props = new Properties();
        props.setProperty("user",user);
        props.setProperty("password",pass);
        conn = driver.connect(url,props);//连接数据库
    }
    return conn;
}
public static void main(String[] args) throws Exception{
    System.out.println(getConn("jdbc:mysql://localhost:3306/mysql"),"root","123456");
}

文章作者: 流浪舟
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 流浪舟 !
评论
 上一篇
Java反射篇 Java反射篇
Java反射对象在运行时会有两种类型,编译时类型和运行时类型,例如:String a = new Name(),编译时为String,运行时为Name。为了准确知道该对象的类型,可以通过instanceof()方法,但是在什么都不知道的情况
2020-10-21
下一篇 
Java注解篇 Java注解篇
Java注解从Java5开始,Java增加对元数据的支持,也就是Annotation,不是一般的注释。这些标记在编译、类加载、运行时被读取,并执行相应处理。通过使用注解,开发人员在源文件中嵌入一些补充信息,进而代码分析和部署工具可以通过这些
2020-10-16
  目录