详解jvm 类加载机制

或许对于一个刚刚入行的人来说,编写代码时对于类加载机制的认识不重要,只要遵循相关语法规则即可,但是,对于一个喜欢研究技术或者入行时间长的人来说,理解jvm的类加载机制,对于解决一些日常编码中碰到的bug,尤其重要,举个例子,我们经常碰到ClassNotFoundException这种异常。这时候如果你知道jvm的类加载机制,就很明白为啥出现这种问题了。别急,等我一一介绍。

1. 首先介绍jvm的类加载机制

jvm类加载的整体顺序是:装载,链接(校验,准备),初始化,卸载,链接中的解析则不一定有固定的顺序,他有时候可能会在初始化阶段之后执行,这是为了支持java语言的运行时绑定机制,例如,接口的多实现,但是,一般的解析过程都是在链接阶段,将符号引用转换成直接引用

整个jvm加载过程分为三个小的部分:

    1. 装载:可以查询java.lang.Classloader源代码中的loadclass方法,注意,这个方法抛出一个ClassNotFoundException异常,在这里读者应该稍微明白一点这个异常产生的原因了吧,不懂也没关系,接着往下读,疑惑就会一一解开


       1.1 通过类的全限定名(包名 + 类名完整的路径名),来获取此类的二进制字节流(注意,这里的获取方式有多种,不一定非得从class文件(.class)中获取,也可以从zip包,运行时动态生成,数据库等等)

       1.2 将这个静态存储结构的二进制字节流数据转换成方法区的运行时数据结构

       1.3 在jvm内存中生成一个这个类的java.lang.class的对象,作为方法区这个类的各种类型数据的访问入口(通过这个对象就可以访问到这个类的数据)

    2. 链接阶段

        2.1 校验:这一步骤是为了校验当前class文件的字节流数据是否符合当前jvm对于数据的要求,并且不会对jvm本身产生危害

        2.2 准备:这一阶段主要是给当前class类的变量分配内存并给变量设置初始值,这些变量的内存都分配在方法区中,这一阶段能够获得内存分配的只包括类层次的变量(也就是被static修饰的变量,跟随类对象的生成而获得初始值),实例变量只有在类对象实例化时一起在jvm堆中分配内存空间。

        例如public static int value = 100, 在准备阶段,给将value变量的值设置为int整型的默认值也就是0,在初始化阶段以后再将value的值设置为100,需要注意的是:public static final int value = 100,这种类型的变量,在准备阶段就会将他的值设置为100,

        2.3 解析:主要是jvm将常量池中的符号引用转换成直接引用,符号引用,引用的目标不一定要加载到内存中,直接引用,引用的目标一定要加载到内存中

    3. 初始化阶段

初始化阶段是整个类加载的最后一步了,这个时候才真正的开始执行class中定义的java代码部分,在准备阶段,类变量已经按照变量类型设置默认值,在初始化阶段,才会设置变量的真正值。

注意一下几点:

           3.1 当初始化一个类的时候,如果其有父类,并且父类还没有获得初始化,那么父类先获得初始化,然后再初始化子类

           3.2 当引用了类的静态变量,此时,是不会导致这个类初始化的

           3.3 通过子类引用父类的静态变量,只会导致父类进行初始化,子类不会进行初始化

           3.4 通过java.lang.loader默认的loadclass方法,也不会导致类初始化

2. 回到一开始说的问题

要说明这个问题,首先我们得要介绍一下jvm 的类加载器

jvm提供了三种类加载器:根装载器,扩展类装载器,应用类装载器

根装载器加载jre目录下lib中的jar,例如rt.jar

扩展类装载器加载jre目录下 C:\Program Files\Java\jre1.8.0_171\lib\ext 中的jar

应用程序类装载器负责装载classpath中的类库,也就是第三方的jar依赖


需要注意的是类装载器在装载一个类的时候,遵循一个原则,那就是双亲委派原则,当一个类需要被加载的时候,会首先交给其父类进行装载,按照上图中的,最终会交给根装载器进行装载,只有当父类装载器无法进行装载的时候,才会交给子类装载器进行装载。

那么为什么jvm加载器要采用这种双亲委派原则呢,举个列子:位于rt.jar中的java.lang.String 这个类,当进行加载的时候,不管哪个加载器先加载,最终到会交给最顶层的根装载器进行装载,这样就会保证不同的装载器装载的类都是同一个对象。

现在再来理解一下ClassNotFoundException这个异常

这个异常出现的几个地方:

    1. 调用class的forName,找不到指定路径的类,也就是在使用反射的时候

    2. classloader的findSystemClass方法,找不到指定路径的类

    3. classloader的loadclass方法,找不到指定路径的类

出现这个异常的大部分原因是:

1. 需要加载的类库没有包含在classpath 路径下

2. 使用了重复的类库,且版本不一致,在加载的时候,加载的不是需要的那个版本下的类库,解决方式是删除重复的类库,也可以使用maven中的 exclusions 来排除

<!-- commons-beanutils -->
<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>${commons-beanutils.version}</version>
    <exclusions>
        <exclusion>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- commons-beanutils -->

3. 类的全限定名(包名 + 类名)错了

猜你喜欢

转载自blog.csdn.net/T2080305/article/details/80870492