JVM之类加载过程

目录

一、引言

二、类加载过程

1.Load 

3.Init

三、案例

1.第1处说明(new关键字与newInstance()方法区别)

2.第 2 处说明(使用类似的方式可获取其他声明如注解、方法等)

3.第 3 处说明(private 成员在类外依然可以修改)

四、类加载器结构

1.最高层Bootstrap

2.第二层Platform ClassLoader(JDK9)

3.第三层Application ClassLoader

五、类加载过程(双亲委派模型)

六、自定义类加载器

1.需要自定义类加载器情形

2.实现自定义类加载器的步骤


一、引言

在冯·诺依曼定义的计算机模型中,任何程序都需要加载到内存才能与 CPU 进行交流。字节码 .class 文件同样需要加载到内存中,才可以实例化类。"兵马未动,粮草先行。" ClassLoader 正是准备粮草的先行军,它的使命就是提前加载 .class 类文件到内存中。在加载类时,使用的是 Parents Delegation Model ,译为双亲委派模型,这个译名有些不妥。如果意译的话,则译作 “溯源委派加载模型”更加贴切。

二、类加载过程

Java 的类加载器是一个运行时核心基础设施模块,如下图所示,主要是在启动之初进行类的 Load LinkInit , 即加载、链接、初始化。

1.Load 

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

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

3.Init

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

三、案例

类加载是一个将 .class 字节码文件实例化成 Class 对象并进行相关初始化的过程。在这个过程中, JVM 会初始化继承树上还没有被初始化过的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。某些类在使用时,也可以按需由类加载器进行加载。

全小写的 class 是关键字用来定义类,而首字母大写的 Class 它是所有 class 的类。这句话理解起来有难度,是因为类已经是现实世界中某种事物的抽象,为什么这个抽象还是另外个类 Class 的对象?示例代码如下:

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 对象,获取私有成员属性对象 Filed (第2处)
        Field privateFiledInOne = one.getDeclaredField("inner");

        // 设置私有对象的属性可以访问和修改 (第3处)
        privateFiledInOne.setAccessible(true);

        privateFiledInOne.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.");
    }
}

1.第1处说明(new关键字newInstance()方法区别

Class 类下的newInstance()在JDK9中已经置为过时,使用getDelaredConstructor().newlnstance()的方式。这里着重说明一下 new 与newInstance 的区别。

new 关键字强类型校验可以调用任何构造方法,在使用 new 操作的时候,这个类可以没有被加载过

而 Class 类下的 newInstance() 方法弱类型只能调用无参数构造方法,如果没有默认构造方法,就抛出InstantiationException 异常,如果此构造方法没有权限访问,则抛出IllegalAccessException 异常。Java 通过类加载器类的实现类的定义进行解耦,所以是实现面向接口编程、依赖倒置的必然选择。

Java中工厂模式经常使用 newInstance() 方法来创建对象,因此从为什么要使用工厂模式上可以找到具体答案:

(1)初始级别,其中 ExampleInterface 是 Example 的接口

    class c = Class.forName("Example");
    factory = (ExampleInterface)c.newInstance(); 

(2)进阶级别,将需要创建对象的类名定义成字符串,作为参数放入forName() 方法中

    String className = "Example";
    class c = Class.forName(className);
    factory = (ExampleInterface)c.newInstance();

(3)变身级别,已经不存在 Example 的类名称,无论 Example 类怎么变化,上述代码不变,甚至可以更换 Example 的兄弟类 Example2、Example3、Example4……,只要他们实现 ExampleInterface 接口就可以

    //从xml 配置文件中获得字符串    
    String className = readfromXMlConfig();
    class c = Class.forName(className);
    factory = (ExampleInterface)c.newInstance();

 从JVM的角度看,我们使用关键字new创建一个类的时候,这个类可以没有被加载。但是使用newInstance()方法的时候,就必须保证:1、这个类已经加载;2、这个类已经连接了。而完成上面两个步骤的正是Class的静态方法forName()所完成的,这个静态方法调用了启动类加载器,即加载java API的那个加载器。

 现在可以看出,newInstance()实际上是把new这个方式分解为两步,即首先调用Class加载方法加载某个类,然后实例化。 这样分步的好处是显而易见的。我们可以在调用class的静态加载方法forName时获得更好的灵活性,提供给了一种降耦的手段。

2.第 2 处说明(使用类似的方式可获取其他声明如注解、方法等)

3.第 3 处说明(private 成员在类外依然可以修改

private 成员在类外是否可以修改?通过 setAccessible( true)操作,即可使用大写 Class 类的 set 方法修改其值。如果没有这一步,则抛出如下异常

Exception in thread "main" java.lang.IllegalAccessException: Class example.ClassTest can not access a member of class example.One with modifiers "private"

 通过以上示例,对于 Class 这个"类中之王",不会有恐惧心理了吧?那么回到类加载中,类加载器是如何定位到具体的类文件并读取的呢?

四、类加载器结构

类加载器类似于原始部落结构,存在权力等级制度。

1.最高层Bootstrap

最高的一层是家族中威望最高的 Bootstrap ,它是在 JVM 启动时创建的,通常由与操作系统相关的本地代码实现,是最根基的类加载器,负责装载最核心的 Java 类,比如 ObjectSystem String 等;它是通过 C/C++ 实现的,并不存在于 JVM 体系内。

2.第二层Platform ClassLoader(JDK9)

第二层是在 JDK9 版本中,称为 Platform ClassLoader ,即平台类加载器,用以加载一些扩展的系统类,比如 XML加密压缩相关的功能类等 ,而 JDK9 之前的加载器是Extension ClassLoader。第二层平台类加载器是通过Java 语言实现

3.第三层Application ClassLoader

第三层是 Application ClassLoader 的应用类加载器,主要是加载用户定义的 CLASSPATH 路径下的类。第三层应用类加载器是通过Java 语言实现

第二、三层类加载器为 Java 语言实现,用户也可以自定义类加载器。查看本地类加载器的方式如下(编译环境JDK7):

    ClassLoader classLoader = ClassTest.class.getClassLoader();
    // 当前正在使用的类加载器    
    System.out.println(classLoader);
    ClassLoader parent = classLoader.getParent();
    // 当前正在使用的类加载器的父加载器
    System.out.println(parent);
    ClassLoader grandparent = parent.getParent();
    // 当前正在使用的类加载器的祖父加载器
    System.out.println(grandparent);

 打印结果如下: 

sun.misc.Launcher$AppClassLoader@146ccf3e
           sun.misc.Launcher$ExtClassLoader@7399f9eb
           null

因为在JDK7环境中,所以第二层打印结果为ExtClassLoader。AppClassLoader 的 Parent 为 Bootstrap,它是通过 C/C++实现的,并不存在于 JVM 体系内,所以输出为 null,类加载器具有等级制度,但是并非继承关系,以组合的方式来复用父加载器的功能,这也符合组合优先原则,详细的双亲委派模型见下文。
 

五、类加载过程(双亲委派模型

低层次的当前类加载器不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个未知类,要非常礼貌地向上逐级询问:"请问,这个类已经加载了吗?" 被询问的高层次类加载器会自问两个问题,第一,我是否已加载过此类?第二,如果没有,是否可以加载此类?只有当所有高层次类加载器在两个问题上的答案均为"否"时才可以让当前类加载器加载这个未知类

如上图所示,左侧绿色箭头向上逐级询问是否已加载此类,直至 Bootstrap ClassLoader ,然后向下逐级尝试是否能够加载此类,如果都加载不了则通知发起加载请求的当前类加载器, 准予加载。在右侧的三个小标签里,列举了此层类加载器主要加载的代表性类库 , 事实上不止于此。通过如下代码可以查看 Bootstrap 所有已经加载的类库:

    URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
    for (URL urL : urLs) {
        System.out.println(urL.toExternalForm());
    }

执行结果如下:

file:/C:/Software/JDK1.7/jre/lib/resources.jar
           file:/C:/Software/JDK1.7/jre/lib/rt.jar
           file:/C:/Software/JDK1.7/jre/lib/sunrsasign.jar
           file:/C:/Software/JDK1.7/jre/lib/jsse.jar
           file:/C:/Software/JDK1.7/jre/lib/jce.jar
           file:/C:/Software/JDK1.7/jre/lib/charsets.jar
           file:/C:/Software/JDK1.7/jre/lib/jfr.jar
           file:/C:/Software/JDK1.7/jre/classes

Bootstrap 加载的路径可以追加,不建议修改或删除原有加载路径 。 在 JVM 中增加如下启动参数,则能通过Class.forName 正常读取到指定类,说明此参数可以增加 Bootstrap 的类加载路径:

-Xbootclasspath/a:/Users/mark/Java/src

 如果想在启动时观察加载了哪个 jar 包中的哪个类,可以增加 -XX:+TraceClassLoading参数,此参数在解决类冲突时非常实用,毕竟不同的 JVM 环境对于加载类的顺序并非是一致的。有时想观察特定类的加载上下文,由于加载的类数量众多,调试时很难捕捉到指定类的加载过程,这时可以使用条件断点功能。比如,想查看 HashMap 的加载过程,在 loadClass 处打个断点,并且在 condition 框内输入var1.equals("java.util.HashMap")条件。

六、自定义类加载器

在明白了类加载器的实现机制后,知道双亲委派模型并非强制模型(我们可以对其中加载路径进行修改删除,而非一定要加载某些包),用户可以自定义类加载器,在什么情况下需要自定义类加载器呢?

1.需要自定义类加载器情形

(1)隔离加载类

在某些框架内进行中间件与应用的模块隔离, 把类加载到不同的环境。比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的 jar 包不会影响到中间件运行时使用的 jar 包

(2)修改类加载方式

类的加载模型并非强制,除 Bootstrap 外,其他的加载并非定要引入,或者根据实际情况在某个时间点进行按需进行动态加载

(3)扩展加载源

比如从数据库、网络 ,甚至是电视机机顶盒进行加载。

(4)防止源码泄露

Java 代码容易被编译和篡改,可以进行编译加密 。那么类加载器也需要自定义,还原加密的字节码。

2.实现自定义类加载器的步骤

实现自定义类加载器的步骤:继承 ClassLoader重写 findClass() 方法调用 defineClass() 方法。一个简单的类加载器实现的示例代码如下:

首先我们定义一个待加载的实体类Customer,我们把生成的Customer.class剪切到D盘目录下D:/Customer.class

public class Customer {
    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

接着我们定义一个自定义类加载器

public class CustomClassLoader extends ClassLoader {
    // 继承ClassLoader类
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 重写findClass方法
        try {
            byte[] result = getClassFromCustomPath(name);
            if (result == null) {
                throw new FileNotFoundException();
            } else {
                // 调用defineClass方法
                return defineClass(name, result, 0, result.length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        throw new ClassNotFoundException();
    }

    private byte[] getClassFromCustomPath(String name) throws IOException {
        // 从自定义路径中加载指定类
        // 这里要读入.class的字节,因此要使用字节流
        FileInputStream fis = new FileInputStream(new File("D:/Customer.class"));
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);

        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1) {
                break;
            }
            by.flip();
            wbc.write(by);
            by.clear();
        }

        fis.close();

        return baos.toByteArray();
    }

}

测试类

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

测试结果

example.CustomClassLoader@4e543c44
           sun.misc.Launcher$AppClassLoader@146ccf3e

注意点,很可能你打印出来的是

sun.misc.Launcher$AppClassLoader@146ccf3e
           sun.misc.Launcher$ExtClassLoader@7399f9eb

那是因为你没有删除项目路径下编译生成的 Customer.class 文件,你的Eclipse或者IDEA自动编译Customer类(此时是用的AppClassLoader编译)后,生成了Customer.class 文件,当你准备用自定义类加载器的时候,根据双亲委任模型,会向上级请示是否已经加载过此类,显然,当询问到 AppClassLoader 加载器时得到的答案为:是。因此此时自定义类加载器变不会生效

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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值