走进JVM-类加载过程

在冯·诺依曼定义的计算机模型中,任何程序都需要加载到内存才能与 CPU进交流。字节码.class 文件同样需要加到内存中,才可以实例化类。"兵马未动,粮草先行"。ClassLoader 正是准备粮草的先行军,它的使命就是提前加载cass类文件到内存中。在类加载时,使用的是 Parents Delegation Model,译为双亲委派楼型,如果意译的话,则译作"溯源委派加载模型"更加贴切。   Java 的类加载器是一个运行时核心基础设施模块,如下图 所示,主要是在启动之初进行类的 Load、Link 和 Init,即加载、链接、初始化。

第一步,Load 阶段读取类文件产生二进制流,并转化为特定的数据结构,初步校验 cafe babe 魔法数、常量池、文件度、是否有父类等,然后创建对应类的 java.lang.Class 实例。

第二步,Link 阶段包括验证、准备、解析三个步骤。验证是更详细的校验,比如final 是否合规、类型是否正确、静态变量是否合理等,准备阶段是为静态变量分配内存,并设定默认值,解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局。

第三步,Init 阶段执行类构造器 方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值。

  类加载是一个将.class字节码文件实例化成 Class 对象并进行相关初始化的过程。在这个过程中,JVM 会初始化继承树上还没有被初始化过的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。某些类在使用时,可以按需由类加载器进行加载。   全小写的class是关键字,用来定义类,而首字母大写的 Class,它是所有 class的类。这句话理解起来有难度,是因为类已经是现实世界中某种事物的抽象,为什么这个象还是另外一个类Class 的对象?示例代码如下:

csharp 复制代码
public class ClassTest {
    // 数组类型有一个魔法属性:length 来获取数组长度
    private static int[] array = new int[3];
    private static int length = array.length;
    //任何小写 class 定义的类,也有一个魔法属性: class ,来获取此类的大写 Class 类对象
    private static Class<One> one = One.class;
    private static Class<Another> another =Another.class;

    public static void main(String[] args) throws Exception {
        // 通过newInstance 方法创建One 和Another 的类对象 (第1处)
        One oneObject = one.newInstance();
        oneObject.call();

        Another anotherObject = another.newInstance();
        anotherObject.speak();


        // 通过one 这个大写的 Class 对象,获取私有成员属性对象 Field (第2处)
        Field privateFieldInOne = one.getDeclaredField("inner");
        // 设置私有对象可以访问和修改 (第3处)
        privateFieldInOne.setAccessible(true);
        privateFieldInOne.set(oneObject, "world changed.");
        // 成功修改类的私有属性 inner 变量值为 world changed.
        System.out.println(oneObject.getInner());
    }

}

class One {
    private String inner = "time files.";

    public void call() {
        System.out.println("hello world.");
    }

    public String getInner() {
        return inner;
    }
}

class Another {
    public void speak() {
        System.out.println("easy coding.");
    }
}

执行结果如下: hello world. easy coding. world changed.

  • 第1处说明:Class类下的newlnstance()在JDK9中已经置为过时,使用getDeclaredConstruclor().newInstance()的方式。这里看重说明一下new与newInstance()的区别。new 是强类型校验,可以调用任何构造方法,在使用new操作的时候,这个类可以没有被加载过。而Class类下的newlinstance()是弱类型,只能调用无参数构造方法,如果没有默认构造方法,就抛出InstantiationException异常;如果此构造方法没有权限访问,则抛出IllegalAccessException 异常。Java通过类加截器把类的实现与类的定义进行解耦,所以是实现面向接口编程、依赖倒置的必然选择。
  • 第2处说明:可以使用类似的方式获取其他声明,如注解、方法等。如下图所示
  • 第3处说明: private 成员在类外是否可以修改? 通过 setAccessible(true)操作即可使用大写Class类的set方法修改其值。如果没有这一步,则抛出如下异常
scala 复制代码
Exception in thread "main" java.lang.IllegalAccessException: class com.linkmiao.iot.demo.test.d202311.ClassTest cannot access a member of class com.linkmiao.iot.demo.test.d202311.One with modifiers "private"
	at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)

通过以上示例,对于Class 这个"类中之王",我们有一定的了解? 那么回到类加载中,类加载器是如何定位到具体的类文件并读取的呢?   类加载器类似于原始部落结构,存在权力等级制度。最高的一层是家族中威望最高的 Bootstrap,它是在JVM 启动时创建的,通常由与操作系统相关的本地代码实现是最根基的类加载器,负责装载最核心的Java 类,比如Object、System、String等;第二层是在JDK9版本中,称为 Platform ClassLoader,即平台类加载器,用以加载些扩展的系统类,比如XML、加密、压缩相关的功能类等,JDK9之前的加载器是Extension ClassLoader; 第三层是Application ClassLoader 的应用类加载器,主要是加载用户定义的CLASSPATH路径下的类。第二、三层类加载器为 Java 语言实现,用户也可以自定义类加载器。查看本地类加载器的方式如下:

ini 复制代码
        // 正在使用的类加载器: jdk.internal.loader.ClassLoaders$AppClassLoader@61064425
        ClassLoader c = TestWhoLoad.class.getClassLoader();
        System.out.println(c);
        // AppClassLoader 的父加载器是 PlatformClassLoader
        ClassLoader c1 = c.getParent();
        System.out.println(c1);
        // PlatformClassLoader 的父加载器是 Bootstrap。它是使用C++ 来实现的,返回null
        ClassLoader c2 = c1.getParent();
        System.out.println(c2);

代码上方的注释内容为JDK11的执行结果。在JDK8环境中,执行结果如下 sun.misc.Launcher$AppClassLoader@14dad5dc

sun.misc.Launcher$ExtClassLoader@6e0be858 null   最高一层的类加载器Bootstrap 是通过C/C++ 实现的,并不存在于JVM体系内所以输出为 null。类加载器具有等级制度,但是并非继承关系,以组合的方式来复用父加载器的功能,这也符合组合优先原则,详细的双亲委派模型如图所示。   低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个未知类,要非常礼貌地向上逐级询问:"请问,这个类已经加载了吗?"被询问的高层次类加载器会自问两个问题:第一,我是否已加载过此类?第二,如果没有,是否可以加载此类?只有当所有高层次类加载器在两个问题上的答案均为"否"时,才可以让当前类加载器加载这个未知类。如上图所示,左侧箭头向上逐级询问是否已加载此类,直至 Bootstrap ClassLoader,然后向下逐级尝试是否能够加载此类,如果都加载不了,则通知发起加载请求的当前类加载器,准予加载。在右侧的三个小标签里,列举了此层类加载器主要加载的代表性类库,事实上不止于此。通过如下代码可以查看 Bootstrap 所有已经加载的类库:

ini 复制代码
        URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (java.net.URL url : urLs) {
            System.out.println(url.toExternalForm());
        }

执行结果如下: file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/resources.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/rt.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/sunrsasign.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jsse.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jce.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/charsets.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/lib/jfr.jar file:/C:/Program%20Files/Java/jdk1.8.0_261/jre/classes Bootstrap 加载的路径可以追加,不建议修改或删除原有加载路径。在JVM 中加如下启动参数,则能通过 Class.forName 正常读取到指定类,说明此参数可以增加Bootstrap的类加载路径:-

bash 复制代码
-Xbootclasspath/a:/users/yangguanbao/book/egsyCoding/byJdk11/src

在学习了类加载器的实现机制后,知道双亲委派模型并非强制模型,用户可以自定义类加载器,在什么情况下需要自定义类加载器呢? (1)隔离加载类。在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包会影响到中间件运行时使用的jar 包。 (2)修改类加载方式。类的加载模型并非强制,除 Bootstrap 外,其他的加载非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载。 (3)扩展加载源。比如从数据库、网络,甚至是电视机机顶盒进行加载。 (4)防止源码泄露。Java 代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义,还原加密的字节码。 实现自定义类加载器的步骤,继承 ClassLoader,重写 findClass0方法,调用defineClass0方法。一个简单的类加载器实现的示例代码如下:

typescript 复制代码
public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] result = getClassFromCustomPath(name);
            if (result == null) {
                throw new FileNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();

        }
        throw new ClassNotFoundException(name);
    }

    private byte[] getClassFromCustomPath(String name) {
        // 从自定义路径中加载指定类
        return null;
    }

    public static void main(String[] args) {
        CustomClassLoader customClassLoader = new CustomClassLoader();
        try {
            Class<?> clazz = Class.forName("One", true, customClassLoader);
            Object obj = clazz.newInstance();
            System.out.println(obj.getClass().getClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行的结果: classloader.CustomClassLoader@5e481248   由于中间件一般都有自己的依赖jar 包,在同一个工程内引用多个框架时,往往被迫进行类的加载。按某种规则jar 包的版本被统一指定,导致某些类存在包路径类名相同的情况,就会引起类冲突,导致应用程序出现异常。主流的容器类框架都会自定义类加载器,实现不同中间件之间的类隔离,有效避免了类冲突。

相关推荐
激流丶11 分钟前
【Kafka 实战】如何解决Kafka Topic数量过多带来的性能问题?
java·大数据·kafka·topic
Themberfue15 分钟前
Java多线程详解⑤(全程干货!!!)线程安全问题 || 锁 || synchronized
java·开发语言·线程·多线程·synchronized·
让学习成为一种生活方式32 分钟前
R包下载太慢安装中止的解决策略-R语言003
java·数据库·r语言
晨曦_子画37 分钟前
编程语言之战:AI 之后的 Kotlin 与 Java
android·java·开发语言·人工智能·kotlin
假装我不帅1 小时前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
南宫生1 小时前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法
神仙别闹1 小时前
基于ASP.NET+SQL Server实现简单小说网站(包括PC版本和移动版本)
后端·asp.net
Heavydrink1 小时前
HTTP动词与状态码
java
ktkiko111 小时前
Java中的远程方法调用——RPC详解
java·开发语言·rpc