JVM虚拟机系列----(四)类加载机制

2017-10-26  本文已影响64人  梦飞成2012

很多做应用层的同学感觉不理解,我们做应用层的,管底层类加载干什么? 它爱什么时候加载什么时候加载? 以前的很多时候我也这么认为,但是后面慢慢的发现,了解类的加载流程,对我们开发有很大帮助,尤其当一些疑难问题百思不得解的时候,不信看看下面的代码:

public class SuperClass {
      static {
          System.out.println("SuperClass Init");
      }

      public static int value = 123;
}


public class SubClass extends SuperClass {
        static {
            System.out.println("SubClass Init");
        }
}


public class Test {
      public static void main(String[] args) {
              System.out.println(SubClass.Value);
      }
}

这段代码的输出结果是:
SuperClass Init
123

是不是感觉有点惊讶!
原因在于:对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态变量,只会出发父类的初始化,而不会引起子类的初始化动作。

下面讲一下虚拟机中类的加载流程:

一、什么是类加载?

类的加载,是指将类的class文件对应的二进制数据读取到内存中,将其放在运行时的方法区中,然后在堆区中创建一个java.lang.Class的对象,用来封装类在方法区的数据结构。类加载的最终产品是位于堆区的Class对象,class对象封装了类在方法区的数据结构,并且向Java程序员提供了访问方法区数据结构的接口。
如下图所示:

类加载结果示意图

类加载并不是某个类“首次主动使用”才进行加载,JVM规范允许类加载器在预料到某个类将要被使用的时候加载它。如果在加载.class 的时候遇到文件缺失或者错误,类加载器必须在程序主动使用该类的时候才报告错误(LinkAgeError错误)。如果这个类一直未被使用,则不会报告这个错误。

加载.class的方式有以下几种:

二、类的生命周期

2.1类的加载步骤:

类从加载到虚拟机内存开始,到被卸载出内存为止。它整个生命周期包括 :加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)
如下图所示:

类加载步骤

加载、验证、准备、初始化和卸载这几个步骤的开始顺序是确定的,类加载的步骤也必须按照这个顺序按部就班的开始,而解析步骤不一定,某些情况下,它可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也叫动态绑定或晚期绑定)。
另外前几个步骤的顺序确定,是指开始顺序确定,而不是按顺序完成或者进行,因为这些阶段通常交叉混合进行的。通常在一个阶段的过程中会激活或者调用另一个阶段。

下面我们分阶段介绍各个步骤:

(1)加载:查找并加载类的二进制数据。

加载是类加载过程的第一个阶段。在加载阶段,需要完成三件事:

连接动作:连接包括 验证、准备、解析三个动作。

(2)验证:确保被加载类的正确性。

验证是连接的第一步,这一步的目的是为了确保class文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全。
验证阶段大概会分为4个阶段 的检验动作:

验证阶段非常重要,但不是必须的。它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone来关闭大部分类的验证,以缩短虚拟机加载的时间。

(3)准备:为类的静态变量分配内存,并初始化其默认值。

准备阶段是正式为类的静态变量分配内存并设置类变量的初始值的阶段,这些内存都将分配在方法区中。
对于该阶段,有几点需要注意的地方:
(i)这时候进行内存分配的仅包括类变量(static)而不包括实例变量,实例变量会在对象实例化时随对象一起分配在Java堆内存区域。
(ii)这里所设置的默认值,通常情况下是数据类型的默认零值(int的0
,boolean的false等,long的0L,对象的Null),而不是Java代码中显示被赋予的值。例如下面代码:
private static int value = 3;
那么,value变量在准备阶段过后,value的值为0,而不是3,因为这时候尚未执行任何Java方法,而把value赋值为3的指令putstatic指令,是在程序编译后,存放于类构造器<cinit>()方法中的,所以将value赋值为3的操作将在初始化阶段采取执行。

这里需要注意几点:

对于常量,即同时被static和final修饰的变量,则会在准备阶段赋自定义的值。
比如,final static int value = 3;
在准备阶段,value的值就变成了3了,这个值是放在方法区的常量池。

(4)解析:把类中的符号引用转变为直接引用

解析阶段是将常量池中的符号引用转变为直接应用的过程。解析动作主要针对类或接口、类方法、字段、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
符号引用就是用一组符号描述目标,可以是任何字面量。
直接引用是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

(5)初始化:初始化阶段为类的静态变量赋予正确的初始值,JVM负责对类变量进行初始化。

在Java中对类变量进行初始化值设置有两种方式:
①声明类变量时指定初始值。
②使用静态代码块为类变量进行初始值设定。

JVM的初始化步骤:

类的初始化时机:
只有当类被主动使用的时候,才会导致类的初始化。
类的主动使用包括以下六种:

(6)使用:使用步骤就是普通的代码运行。

(7)卸载:当JVM虚拟机结束自己生命周期,即卸载类,有以下几种方式:

三、类加载器简介:

先来一段类加载器的代码:

public class ClassLoader {
    public static void main(String[] args) {
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            System.out.println(loader);
            System.out.println(loader.getParent());
            System.out.println(loader.getParent().getParent());
    }
}

运行后输出的结果是:

sun.misc.Launcher$AppClassLoader@643df239
sun.misc.Launcher$ExtClassLoader@92dfa992
null

从上面的输出结果来看,并没有找到ExtClassLoader的父loader,原因是BootStrapClassLoader(引导类加载器)是C语言实现的,找不到一个确定的返回父loader 的方式,所以返回null。

JVM中类加载器的层次结构如下图:

JVM 类加载器结构图

注:这里的父类加载器不是使用继承,而是使用组合方式实现的。

站在Java虚拟机的角度来言,只有两种加载器:启动类加载器和非启动类加载器。
启动类加载器:它使用C++语言实现,是所有其他类是的加载器, 是虚拟机的一部分。
所有其他类的加载器: 这些加载器都独立于虚拟机之外,有Java语言实现,全部继承于java.class.ClassLoader, 这些类加载器需要由启动类加载器加载到内存中,才能加载其他类。

站在Java开发人员的角度,类加载器大致可以分为三类:
启动类加载器:Bootsrap Classloader,负责加载并识别虚拟机的类库(如rt.jar,所有已Java.开头的类,均被其加载),启动类加载器无法被Java程序直接引用。
扩展类加载器:由sun.misc.Launcher$ExtClassLoader实现,负责加载系统变量指定的类库,或者以javax.
开头的类。
应用程序类加载器:由sun.misc.Launcher$AppClassLoader 实现, 负责加载用户类路径下的类, 开发者可以直接使用该类加载器,如果程序员没有自定义,一般使用系统默认的。

应用程序都是由这三个类加载器配合使用的, 如果有必要,我们还可以自定义类加载器,因为JVM自带的类加载器,只懂得从本地文件系统,读取标准的class 文件,自己编写的ClassLoader可以做到以下几点:

3.1JVM加载机制的特点:

(1) 全盘负责:当一个类加载器加载某个class时,该class所依赖的和所引用的class,也由该类加载器负责载入,除非显示的使用另一个类加载器载入
(2) 父类委托:先让父类加载器试图加载该类,只有当父类加载器无法加载该类的时候,才尝试从自己的路径加载该类
(3)缓存机制:缓存机制,将保证所有被加载过的类都将被缓存,当程序中需要使用某个class时候,类加载器首先从缓存区寻找该class,只有缓存区不存在的时候,系统才会读取该类对应的二进制数据,并将其转换为class对象,存入缓存区,这就是修改了class之后,为什么要重启JVM,程序的修改才会生效。(Android程序的热修复也是这个原理,必须重新加载才行。)

3.2类的加载:

类的加载有三种方式:

public class Test {
        static {
                System.out.println("Test 类开始初始化了!");
        }
}

测试代码:

public class ClassLoader {
    public static void main(String[] args) {
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            System.out.println(loader);
            loader.loadclass("Test"); // 不会输出"Test 类开始初始化了!" 语句,因loadclass不会执行初始化步骤
          Class.forName("Test"); // 输出"Test 类开始初始化了!"语句,因为使用class.forName 会调用初始化语句
    }
}

Class.forName 和ClassLoader.loadclass的区别:
Class.forName: 将class加载到JVM中,并进行解释,执行初始化步骤
ClassLoader.loadclass:只干了一件事,将class文件加载到JVM中,不会执行static内容块,只有执行了newInstance之后,才会进行类的初始化步骤。

3.3双亲委派模型:

双亲委派模型
如上图所示,这就是类加载器双亲委派模型的示意图。
双亲委派模型要求,除了顶层的启动类加载器之外,其余的类加载器都应有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承关系实现,而是以组合的关系来复用父类的代码。

双亲委派模型的工作流程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个类加载请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求,最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它搜索的范围没找到这个类),子加载器才会尝试自己去加载。

双亲委派模型的好处,显而易见:


    protected  Class<?>  loadClass(String name,  boolean resolve) {
        Class c = findLoadedClass(name);    // 首先,检查请求的类是否被加载过
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, resolve);
                } else {
                    c = findBootStrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果抛出异常,说明父类加载器无法完成请求
            }

            if (c == null) {
                // 在父类加载器无法完成请求的时候,调用本身的findClass方法进行类加载
                c = findClass(name);
            }
        }
        
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

3.4自定义类加载器:

步骤:

参考资料:

java类的加载机制
Java类加载器(一)——类加载器层次与模型
Java虚拟机类加载机制
http://blog.csdn.net/ns_code/article/details/17881581
https://segmentfault.com/a/1190000005608960
http://www.importnew.com/18548.html
http://zyjustin9.iteye.com/blog/2092131
http://www.codeceo.com/article/java-class-loader-learn.html

上一篇下一篇

猜你喜欢

热点阅读