美文网首页
JVM类加载机制

JVM类加载机制

作者: 后山野鹤 | 来源:发表于2020-02-28 00:07 被阅读0次

    这节主要从覆盖JDK的类开始学习JVM的类加载机制。Java和JVM的类加载机制类似,但JVM的类加过程稍有些复杂。
    在学习本节内容前,我门首先看几个面试题
    1、我们能够通过一定的手段,覆盖HashMap类的实现吗?
    2、有哪些地方打破了Java的类加载机制?
    3、如何加载一个远程的.class文件?怎样加密.class文件?
    关于类加载,很多人知道有双亲委派机制,但这明显不够,你还需要知道一些能打破这个机制的例子。在日常工作中,也有大量的相关应用,我们会理论联系实践综合的分析这些问题。

    类加载过程

    并不是说把一个文件修改成.class后缀,就能够被JVM识别。类的加载过程非常复杂,主要过程有:加载、验证、准备、解析、初始化。我们应该了解它背后的原理和要做的事。

    加载

    加载的主要作用是将外部的.class文件加载到JVM的方法区,找到并加载类的二进制数据,比如jar包里或者war包里找到它们。

    验证

    并不是任何.class文件都能加载,那样太不安全且容易受到恶意代码的攻击,因此需要增加一层验证。验证阶段在JVM整个类加载过程中占了很大一部分,不符合规范的将抛出java.lang.VerifyError错误。像一些低版本的JVM是无法加载一些高版本的类库的,就是在这个阶段完成。

    准备

    从这个阶段开始,将为一些变量分配内存,并将其初始化为默认值。此时实际对象还没有分配内存,所以这些动作是在方法区上进行的

    解析

    解析在类加载中是非常重要的一环,是将符号引用替代为直接引用的过程。符号引用是一种定义,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。直接引用的对象都存在内存中。解析阶段负责把整个类激活,串成一个可以找到彼此的网,过程非常重要,这个过程主要分为以下几类

    1、类或接口的解析
    2、类方法解析
    3、接口方法解析
    4、字段解析

    我们来看下经常发生的异常,就与这个阶段有关
    1、java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错
    2、java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误
    3、java.lang.NoSuchMethodError 找不到相关方法时的错误
    解析过程保证了相互引用的完整性,把继承与组合推进到运行时。

    初始化

    真正开始执行一些字节码。
    引入一个面试题
    public class Test {
    static int a =0;
    static {
    a=1;
    b=1;
    }
    static int b=0;
    public static void main(String[]args) {
    System.out.println(a);
    System.out.println(b);
    }
    }
    正确输出结果是1 0
    a和b唯一的区别就是他们的static代码块的位置。
    引出一个规则:static语句块只能访问到定义在static语句块之前的变量,所以下面的代码是无法通过编译的
    static {
    b=b+1;
    }
    static b=1;
    第二个规则:JVM会保证在子类的初始化执行之前,父类的初始化方法已经执行完毕
    所以,JVM第一个被执行的类初始化方法一定是java.lang.Object。另外也意味着父类中定义的static语句块要优先于子类的
    <cinit> 与<init>方法有什么区别?
    主要是为了让你明白类的初始化和对象的初始化之间的差别
    public class A {
    static {System.out.println("1");}
    public A() {
    System.out.println("2");
    }
    }
    public class B extends A{
    static {
    System.out.println("a");
    }
    public B() {
    System.out.println("b");
    }
    public static void main(String[]args) {
    A ab =new B();
    ab = new B();
    }
    }
    输出结果:1 a 2 b 2 b
    结果分析:static 字段和static代码块属于是类的,在类的加载的初始化阶段就已经被执行。类的字节码信息被存放在方法区。在同一个类加载器下,只存在一份,因此static代码只会执行一次,对应的是<cinit> 方法。而在new新对象时,都会调用它的构造方法就是<init>,用来初始化对象的属性,每次创建的时候都会执行。因此上面代码中static方法只执行一次,对象的构造方法执行两次

    类的加载器

    整个类加载过程任务非常繁重,类加载器做的就是上面的几个步骤。类加载器中有不同等级的加载器,协作保证了JVM的安全性

    几个类加载器

    Bootstrap ClassLoader

    加载器中的最底层,任何类的加载行为,都要经它访问。它的作用是加载核心类库,也就是rt.jar、resource.jar、charset.jar等。当然这些jar包的路径是可以指定的,可以通过参数来设置,-Xbootclasspath,这个加载器是C++编写的,随着JVM启动。、

    App ClassLoader

    程序中java类的默认加载器,有时候也叫做System ClassLoader。一般用来加载classpath下的其他jar包和.class文件,我们写的代码会首先尝试使用这个类加载器进行加载。

    Custom ClassLoader

    自定义加载器,支持一些个性化的扩展功能。

    双亲委派机制

    它的含义是除了顶层的启动类加载器以外,其余的加载器,在加载之前,都会委派给他的父加载器进行加载。这样一层层向上传递,直到祖先们都无法胜任,它才会真正的加载。
    类加载的双亲委派机制,双亲在哪里?明明是单亲(单继承)
    看下图(User ClassLodaer 就是上文中提到的Custom ClassLodaer)


    111111.jpeg

    上图可以看到,除了启动类加载器,每个加载器都有一个parent,并没有所谓的双亲。只是大家都普遍这么叫,所以不要被迷惑了
    通过查看ClassLoader的loadClass方法,具体的加载过程是首先使用parent尝试进行类加载,parent失败后才轮到自己。同时这个方法时可以被覆盖的,也就是双亲委派机制并不一定生效。这样处理的好处在于Java类有了一定的优先级的层次划分关系。比如Object类,这个毫无疑问应该交给最上层的加载器进行加载,即使你覆盖了它,最终也是由系统默认的加载器进行加载的。
    如果没有了双亲委派模型,就会出现多个不同的Object类,应用程序就会一片混乱。

    一些自定义的类加载器

    下面来聊聊打破双亲委派机制的一些自定义类加载器的案例

    案例一:tomcat

    tomcat进行war包进行分布程序,其实就是违反了双亲委派机制原则,简单看一下tomcat类加载器的层次结构


    微信图片_20200227234711.png

    对于一些需要加载的非基础类,会由一个叫做WebAppClassLoader的类加载器优先加载。等它加载不到的时候,再交给上层的ClassLoader进行加载。这个加载器用来隔绝不同应用的.class文件,比如两个应用,可能会依赖同一个不同版本的第三方文件,它们是相互没有影响的。
    如果在同一个JVM里,运行着不兼容的两个版本,当然是需要自定义加载器才能完成的。
    那么tomat如何打破双亲委派机制的?
    可以看出图中的WebAppClassLoader,它加载自己目录下的.class文件并不会传递给父类的加载器,但是它却可以使用SharedClassLoader所加载的类,实现了共享和分离的功能

    案例二:替换JDK的类

    如何替换JDK中的类
    例如当原生的HashMap类,无法满足我们的需要,需要修改的时候,就必须要使用到Java的endorsed技术。我们需要将自己的HashMap类打包成一个jar包,然后放在-Djava.endorsed.dirs指定的目录中。但是java.lang包下面的除外,因为这些都是特殊保护的。
    因为上面提到的双亲委派机制,是无法直接在应用中替换JDK原生类。但是有时候又不得不进行一下增强、替换,所以Java团队提供了endorsed技术,用于替换这些类。这个目录下的jar包,会比rt.jar中的文件,优先级更高,可以被最先加载到

    通过本节的学习,了解到了Java类加载,经过加载、验证、准备、解析、初始化几个过程,每一个过程都职责明确。Java类加载器是非常重要的知识点,也是面试常考的。通过此次的学习,认识到类加载是遵循双亲委派机制的,但并不是绝对的,也是可以打破这个规则的,类加载器通过开放的API,让加载过程更加灵活。无论是远程存储字节码,还是将字节码进行加密,这些都是业务需求,要做到这些,实现一个自定义累加器就可以了。因此Java的类加载器的相关知识是非常重要的!

    相关文章

      网友评论

          本文标题:JVM类加载机制

          本文链接:https://www.haomeiwen.com/subject/qjovqhtx.html