java基础(第三篇)ClassLoader必知必会

本文篇幅稍长,建议收藏慢慢看。

虚拟机类加载机制经常在面试的时候是必考的问题,了解类加载机制对每个java程序员来说都是很重要的,知根知底地写代码,遇到问题的时候才能快速找出问题所在。

虚拟机类加载过程

java是一门解释型语言,虚拟机在运行时将字节码进行动态加载和链接。一个类从被加载到内存开始,到它卸载分7个阶段:

  • 加载(loading)
  • 验证(verification)
  • 准备(preparation)
  • 解析(resolution)
  • 初始化(initialization)
  • 使用(using)
  • 卸载(unloading)

其中,加载验证准备解析初始化类加载的过程。解析阶段和初始化阶段的时间先后并没有强制要求。

加载

通过类的全限定名,从指定来源加载二进制字节码,并转换成jvm可以使用的数据结构,在内存中生成一个代表该类的java.lang.Class对象。

这里的来源可以是文件网络数据库等。

验证

对字节码进行验证,比如是否符合jvm对于字节码的规范,以及对安全性等做检验。

准备

  • 正式为类变量分配内存并设置初始值。
  • 并且只是设置类静态变量的初始值,实例变量在实例初始化时进行赋值,
  • 这里的初始赋值是赋零值,比如boolean 就是设置为false,int 设置为 0,float 设置为0.0f等
  • 对于 public static int value = 666这种情况也是赋零值,value 赋值 为666 的操作是在类的初始化阶段进行
  • 如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰的带赋值的变量,那么在准备阶段,变量value就会被初始化为ConstValue属性所指定的值。
  • 例如:public static final int value = 666,编译时Javac将会为value生成ConstantValue属性,并且这个属性的值为666,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为666。

解析

将字节码常量池中的符号引用转化为直接引用的过程。(这里的常量池以及后面提到的常量池,不是指内存中的常量池,而是指class文件中的某一段,你可以理解成字节码文件中存放各类信息的仓库,供jvm读取)

java源代码在进行javac编译成字节码文件的时候,并没有像c/c++那样,在编译时进行链接,而是在运行时动态链接,因此,class字节码中并没有保存各个变量、方法在内存中的地址布局,由虚拟机动态分配所需内存,然后将这些符号引用解析成如下几种直接引用:

  • 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
  • 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
  • 一个能间接定位到目标的句柄

直接引用是和虚拟机的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

解析主要针对的是类或接口、类的字段、类的方法、接口的方法、方法类型、方法句柄和调用点限定符等符号,下面列出常量池中的符号与目标的对应关系(大概知道有这个东西就行,当然不止这些,我懒得写而已):

 

初始化

类加载过程的最后一个阶段,准备阶段的时候,变量已经赋值过一次初始值,在初始化阶段,代码中通过主观计划初始化类变量,和其他资源,初始化阶段就是执行类构造器方法(<clinit>()方法)的过程。

什么是<clinit>()?

<clinit>()方法是编译器自动收集类中的所有类静态变量的赋值动作(比如public static int value = 666 中的666赋值)和静态初始化块(static代码块)的语句合并产生的方法。

  • 编译器收集这些动作的顺序是根据语句在源文件中的编写顺序决定的,因此你如果按以下顺序写代码是无法通过编译的。
static {
   yu = 6 ;//给变量赋值可以通过编译,
   int ss = yu; //但是这句,引用变量是不能通过编译的
}
public static int yu = 8 ;
  • <clinit>()方法在继承关系中的执行顺序是先执行父类的<clinit>()方法,这样就使得父类的静态代码块先执行于子类的静态代码块。
  • <clinit>()方法对于类和接口来说不是必须的,如果一个类没有静态代码块,也没有静态变量的赋值操作,那么编译器就不会生成此方法。
  • 接口中虽然不能有静态初始化块,但是仍然可以有类静态变量的初始化赋值,因为接口中定义的变量都是public static final的变量,所以,如果接口中定义了变量,编译器也会生产<clinit>()方法。
  • 执行接口的<clinit>()方法不需要先执行父类的<clinit>()方法,只有当使用了父接口中定义的变量时,才需要执行父类的<clinit>()方法,这点和类有所不同。
  • 类只有final修饰的静态变量且没有static块,不会生成<clinit>()方法。
  • 类的<clinit>()方法在多线程中是线程安全的,是通过加锁同步阻塞实现线程安全,因此最好不要在一个类的<clinit>()方法中执行耗时操作,这样会导致多个线程阻塞。

初始化时机:

  • 创建某个类的新实例时:new、反射、克隆或反序列化;
  • 调用某个类的静态方法时;以及使用某个类或接口的静态字段或对该字段赋值时(final字段除外);
  • 调用Java的某些反射方法时
  • 初始化某个类的子类时
  • 在虚拟机启动时某个含有main()方法的那个启动类。

以下情况不会对类进行初始化:

  • 定义数组时:比如MyClass[] mc = new MyClass[10];,这种情况不会对MyClass类进行初始化。
  • 调用的静态方法或者使用的静态变量是继承自父类:比如int value = Sub.value;value 是Sub类继承自SuperClass类的一个静态变量,不会触发对Sub类的初始化,反而会触发父类SuperClass的初始化。
  • 引用final 修饰的静态变量,且该变量在编译时就确定下来,即在声明处已经赋值,而不是在静态初始化块赋值。例如:int value = Sub.value2;,声明处为public static final int value2 = 233;,不会触发Sub类的初始化。

类加载器概念

虚拟机将类的加载过程中获取字节码的工作交给类加载器来完成。

类加载器种类

  • 启动类加载器Bootstrap ClassLoader:加载JRE_HOME/lib下的核心包,该类加载器是用c++写的。
  • 扩展类加载器Extension ClassLoader:加载JRE_HOME/lib/ext目录下的扩展包,也可以通过启动参数-Djava.ext.dirs指定,该类用java编写。对应ExtClassLoader
  • 应用类加载器Application ClassLoader:应用类加载器,加载classpath下的字节码文件,用java编写,对应AppClassLoader这个类,可以通过ClassLoader类的静态方法getSystemClassLoader()获得,所以又叫系统类加载器
  • 自定义类加载器:自定义的类加载器,通过直接或者间接继承抽象的ClassLoader类。

除启动类加载器外,其他类加载器都是直接或者间接地继承了ClassLoader这个抽象类。

父子关系如下:

 

说明:通过ClassLoader的getParent()方法可以获得父类加载器,但这种父子关系并不是继承关系,是通过组合实现的,如果不知道组合是什么可以看一下这篇文章《继承与组合》

ClassLoader的主要方法

  • public Class<?> loadClass(String name) throws ClassNotFoundException:公有调用加载类的入口,内部是调用了loadClass(String name,boolean resolve)方法
  • protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException:通过双亲委托机制实现的加载类的实现。
  • protected Class<?> findClass(String name) throws ClassNotFoundException查找类的方法,自定义类加载器推荐重写此方法。
  • protected final Class<?> defineClass(String name, byte[] b, int off, int len):将二进制字节码转换成Class的一个实例。

推荐自己看一下ClassLoader的源码,可以利用IDE的一些快捷键方便自己,通过debug单步执行,这样查看会清晰很多。

双亲委派机制

介绍:

如果一个类加载器收到加载类的请求,它是首先交给父加载器完成加载,依次委托直到顶层类加载器BootStrap ClassLoader,如果不是该类加载器的职责,它会交给下层去加载,依次往下直到找不到合适的类加载器就抛出ClassNotFoundException异常。

优点:

  • 使得类加载不会重复加载。
  • 具有优先级层次关系,使得java程序稳定运作。
    • 举个栗子,如果我们使用了一个自定义的类全限定名一样的Object类,虽然编译不会报错,但是运行时就会报错,是因为这种双亲委派机制的存在,即使自定义Object,也会委托给最顶层的启动类加载器,该类加载器发现这个类不合法,抛出运行时异常。倘若不是双亲委托,而是系统类加载器或者自定义类加载器加载Object,因为加载过的类会缓存,会导致其他继承了Object类的类受到巨大影响。

破坏双亲委派机制

双亲委派机制并不是强制规范,是可以破坏的,有些场景是需要破坏它的这种委托机制的。比如某些情况基于特定要求重写了loadClass方法。又比如一个经典的情况:

//1
Class.forName("com.mysql.jdbc.Driver");
//2.
DriverManager.getConnection(url);

1和2这两句代码相信你再熟悉不过了,1是通过类全限定名去加载mysql的驱动,一般自定义类是由AppClassLoader加载,而2是由Bootstrap ClassLoader去加载,那么它怎么获得mysql实现的类的实例的?原来,在DriverManager内部实现中,不是使用启动类加载器去加载,而是使用调用了DriverMannager.getConnection方法的类的当前线程类加载器去加载,默认的当前线程上下文类加载器是AppClassLoader,也就是使用了AppClassLoader加载mysql实现类。这给我们有所启发,可以修改当前线程上下文类加载器,然后用当前线程上下文类加载器去加载具体实现类,实现破坏双亲委派机制,这在SPI(服务提供者接口,Service Provider Interface)中很常见。

//修改的方法。
Thread.currentThread().setContextClassLoader(classLoader);

类加载器主要应用

  • 热部署(Hotswap),或者有些时候也叫动态替换、热替换、热更新,我们知道修改jsp文件之后,可以不需要重新启动Tomcat,实际上它就是自定义了一个类加载器实现对字节码的热更新。
  • 对字节码进行加密防止破解
  • ......

下面通过一个小demo,介绍一下怎么自定义类加载器,并通过它加载类。

package wyn.test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
public class DemoTest {
    @Test
    public void classLoaderTest() throws Exception {
        List list = (List) new MyClassLoader().loadClass("wyn.test.MyList").newInstance();
        System.out.println("list.size = " + list.size() + "\nClassLoader:" + list.getClass().getClassLoader());
    }

}
class MyClassLoader extends ClassLoader{
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Path path = Paths.get("D:\\" + name.replaceAll("\\.","\\\\") + ".class");
        byte[] classData = null;
        try {
            classData = Files.readAllBytes(path);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return defineClass(name,classData,0,classData.length);
    }
}

//MyList.java
package wyn.test;
import java.util.*;
public class MyList extends ArrayList{
    @Override
    public int size() {
        return 666;
    }
}

将MyList.java放到D盘,然后cmd下面执行javac -d . MyList.java编译,然后就可以通过IDE执行上面的DemoTest的classLoaderTest方法:
运行结果如下:



作者:Coder_Ring
链接:https://www.jianshu.com/p/92c27f117edc
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值