JVM活学活用——类加载机制

Posted 人生是一场修行

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM活学活用——类加载机制相关的知识,希望对你有一定的参考价值。

类的实例化过程


 有父类的情况

1. 加载父类静态
    1.1 为静态属性分配存储空间并赋初始值
    1.2 执行静态初始化块和静态初始化语句(从上至下)

2. 加载子类静态
    2.1 为静态属性分配存储空间
    2.2 执行静态初始化块和静态初始化语句(从上至下)

3. 加载父类非静态
    3.1 为非静态块分配空间  
    3.2 执行非静态块

4. 加载子类非静态
    4.1 为非静态块分配空间  
    4.2 执行非静态块

5. 加载父类构造器
    5.1 为实例属性分配存数空间并赋初始值
    5.2 执行实例初始化块和实例初始化语句
    5.3 执行构造器内容

6. 加载子类构造器
    6.1 为实例属性分配存数空间并赋初始值
    6.2 执行实例初始化块和实例初始化语句
    6.3 执行构造器内容

 下面看一个例子:

package jvm;

public class InstanceClass extends ParentClass{

    public static String subStaticField = "子类静态变量";
    public String subField = "子类非静态变量";
    public static StaticClass staticClass = new StaticClass("子类");

    static {
        System.out.println("子类 静态块初始化");
    }

    {
        System.out.println("子类 [非]静态块初始化");
    }

    public InstanceClass(){
        System.out.println("子类构造器初始化");
    }

    public static void main(String args[]) throws InterruptedException {
        new InstanceClass();
    }
}

class ParentClass{
    public static String parentStaticField = "父类静态变量";
    public String parentField = "父类[非]静态变量";
    public static StaticClass staticClass = new StaticClass("父类");

    static {
        System.out.println("父类 静态块初始化");
    }

    {
        System.out.println("父类 [非]静态块初始化");
    }

    public ParentClass(){
        System.out.println("父类  构造器初始化");
    }
}

class StaticClass{
    public StaticClass(String name){
        System.out.println(name+" 静态变量加载");
    }
}

按照上面说的规则,先自己想一想,然后再查看答案:

父类 静态变量加载
父类 静态块初始化
子类 静态变量加载
子类 静态块初始化
父类 [非]静态块初始化
父类  构造器初始化
子类 [非]静态块初始化
子类构造器初始化

抛砖引玉之后,结合《深入理解Java虚拟机》看看类加载机制

 

什么是Java类的加载?


  类的加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后再堆区创建一个java.lang.class对象,用来封装类的方法区内的数据结构。类的加载的最终产品是位于堆区的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。在Java语言中,类型的加载、连接、初始化过程都是在程序运行区间完成的。

                

  类加载器并不需要等到某各类被首次主动使用时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

  加载.class文件的方式:

  1.   – 从本地系统中直接加载
  2.    – 通过网络下载.class文件
  3. – 从zip,jar等归档文件中加载.class文件
  4. – 从专有数据库中提取.class文件
  5. – 将Java源文件动态编译为.class文件

 类的生命周期


 

  类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生个生命周期包括:加载,验证,准备,解析,初始化,使用,卸载共7个阶段。其中验证,准备,解析统称为连接。

  其中类的加载过程包括(加载、验证、准备、解析、初始化)五个阶段。这五个阶段中,加载,验证,准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化之后开始,这是为了支持Java的运行时绑定(也成为动态绑定或者晚期绑定).

  另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

 

加载——查找并加载类的二进制数据

  加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

  1.通过一个类的全限定名来的获取定义此类的二进制字节流

  2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

  3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。

  相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

  对于数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由虚拟机直接创建的。但数组类和类加载器仍然有很密切的关系,因为数组类的元素类型最终要靠类加载器去创建。

  加载过程完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class类的对象,这样对象将作为程序访问方法区中的这些类型数据的外部接口。

 

 验证——确保被加载的类的正确性

  验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会虚拟机自身的安全。验证阶段大致会完成4个阶段

的检验工作:

  文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。 

  元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

  字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

  符号引用验证:确保解析动作能正确执行。

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

 

准备——为类的静态变量分配内存,并将其初始化为默认值

  准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1.这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时伴随着对象一块分配到Java堆中

  2.这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

  假设一个类变量的定义为:public static int value = 3;

   那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器<clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。

  这里还需要注意如下几点:

  • 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
  • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;
  • 而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。 · 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

 3.如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

  例:public static final int value = 3;

  编译时Javac将会value生成ConstantValue属相,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3.

  可以理解static final常量在编译期就将其结果放入了调用它的类的常量池中。

 

解析——把类中的符号引用转换为直接引用

 

  解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。

 

  直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

 

初始化

  初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。Java中对类变量进行初始化设定有两种方式:

  1.声明类变量式指定初始值

  2.使用静态代码块为类变量指定初始值

  重点:JVM初始化步骤

 

  1.假如这个类还没有被加载和连接,则程序先加载并连接该类

  2.假设该类的直接父类还没有被初始化,则先初始化其直接父类

  3.假如类中有初始化语句,则系统依此执行这些初始化语句

  

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

  1.   创建类的实例,也就是new的方式
  2.   访问某个类或接口的静态变量,或者对该静态变量赋值
  3.   调用类的静态方法
  4.   反射(如Class.forName(“com.Test”))
  5.   初始化某个类的子类,则其父类也会被初始化
  6.   Java虚拟机启动时被表明为启动类的类(Java Test),直接使用java.ext命令来运行主类。

  

结束生命周期

  在如下几种情况下,Java虚拟机将结束生命周期

  – 执行了System.exit()方法

  – 程序正常执行结束

  – 程序在执行过程中遇到了异常或错误而异常终止

  – 由于操作系统出现错误而导致Java虚拟机进程终止

 

类加载器


 寻找类加载器的例子:

public class TestClassLoadDemo {
    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());
    }
}

 

输出结果:

  从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是Bootstrap Loader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。

  这几种类加载器的层次关系如下图所示:

  

注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。

 

站在Java虚拟机的角度来讲,只存在两种不同的类加载器:

启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分;

所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。

 

站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:

启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\\jre\\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\\jre\\lib\\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

1)在执行非置信代码之前,自动验证数字签名。

2)动态地创建符合用户特定需要的定制化构建类。

3)从特定的场所取得java class,例如数据库中和网络中。

 

JVM类加载机制

全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。

缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类的二进制数据,并将其转换成Class对象,存入缓存区。

这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。

 

类的加载


 类的加载有三种方式:

1.命令行启动应用时候由JVM初始化加载

2.通过Class.forName方法动态加载

3.通过ClassLoader.loadClass()方法动态加载

 

例子:

 1 public class LoadTest {
 2     public static void main(String args[]) throws ClassNotFoundException {
 3         ClassLoader loader = Thread.currentThread().getContextClassLoader();
 4         System.out.println(loader);
 5 
 6         // 使用ClassLoader.loadClass()来加载类,不会执行初始化块
 7         loader.loadClass("jvm.TestClassLoad");
 8 
 9         // 使用Class.forName()来加载类,默认会执行初始化块
10         Class.forName("jvm.TestClassLoad");
11 
12         //使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
13         Class.forName("jvm.TestClassLoad", false, loader);
14     }
15 }

 

要加载的类:

package jvm;

public class TestClassLoad {
    static {
        System.out.println("静态代码块被加载了");
    }
}

输出结果:

 

 

Class.forName()和ClassLoader.loadClass()区别

Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

 

双亲委派模型


   双亲委派的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载类。

双亲委派机制

1.当AppClassLoader加载一个class时,他首先不会自己去尝试加载这个类,而是把类加载请求委托给父类加载器ExtClassLoader去完成。

2.当ExtClassLoader加载一个class时,他首先也不会自己去尝试加载这个类,而是把类加载请求委托给BootStrapClassLoader去完成。

3.如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

 

ClassLoader源码分析:

public Class<?> loadClass(String name)throws ClassNotFoundException {
            return loadClass(name, false);
    }
    
    protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
            // 首先判断该类型是否已经被加载
            Class c = findLoadedClass(name);
            if (c == null) {
                //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
                try {
                    if (parent != null) {
                         //如果存在父类加载器,就委派给父类加载器加载
                        c = parent.loadClass(name, false);
                    } else {
                    //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                        c = findBootstrapClass0(name);
                    }
                } catch (ClassNotFoundException e) {
                 // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

双亲委派模型意义

-系统类防止内存中出现多份同样的字节码

-保证Java程序安全稳定运行

 

破坏双亲委派模型

  1. 第一次破坏是在jdk2之前,用户自定义的类加载器都是重写Classloader中的loadClass方法,这样就导致每个自定义的类加载器其实是在使用自己的loadClass方法中的加载机制来进行加载,这种模式当然是不符合双亲委派机制的,也是无法保证同一个类在jvm中的唯一性的,那么为了保证及时是由不同的类加载器(哪怕是用户自定义的类加载器加载)也是唯一的,java官方在Classloader中添加了findClass方法,用户只需要重新这个findClass方法,在loadClass方法的逻辑里,如果父类加载失败的时候,才会调用自己的findClass方法来完成类加载,这样就完成了符合双亲委派机制。

  2. 第二次的破坏是类似于jndi,jdbc这种服务,因为这种服务需要回调用户的代码,但是对于父类加载器而言是不认识用户的代码的。

    那么这时候java团队使用了一个不太优雅的设计:线程上下文类加载器。这个类加载器可以通过Thread类的setContextClassLoader方法进行设置,如果创建线程时还未设置,它就从父线程继承一个,如果在应用全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

利用这个线程上下文类加载器,jdni去加载需要的spi代码,也就是父类请求子类的加载器去加载。

  1. 第三次的破坏是因为用户对于程序的动态性追求,诸如:代码热替换,模块热部署。
    这时候就诞生了诸如jigsaw和osgi。对于现在的业界来讲,osgi赢得了java模块化的主导权,成为目前业界模块化的标准。而Osgi模块话的关键是他自己的类加载机制:每个程序模块(bundle)都有自己的类加载器,需要更换程序(bundle)的时候,连同类加载器一起替换,以实现代码的热部署

osgi和双亲委派模式不同,他是一个基于网状的互相组合依赖的加载。
Osgi的加载步骤是这样的:

  1. 如果类或者资源是在包java.*中,那么交由父级类加载器代理完成,否则,搜索过程进入第二步。如果父类级类加载器加载失败,那么查找过程结束,加载失败。
  2. 如果类或者资源在启动代理序列(org.osgi.framework.bootdelegation)中定义,那么交由父级代理完成,此时的父级代理有启动参数org.osgi.framework.bundle.parent指定,默认是引导类加载器(bootstrap class loader),如果找到了类或者资源,那么查找过程结束。
  3. 如果类或者资源所在的包是在Import-Package中指定的,或者是在此之前通过动态导入加载的了,那么将请求转发到导出bundle的类加载器,否则搜索继续进行下一步;如果该包在启动参数org.osgi.framework.system.packages.extra中,则将请求转发给osgi容器外部的类加载器(通常是系统类加载器)。如果将请求交由导出类加载器代理,而类或者资源又没有找到,那么查找过程中止,同时请求失败。
  4. 如果包中类或者和资源所在的包由其他bundle通过是使用Require-Bundle从一个或多个其他bundle进行导入的了,那么请求交由其他那些bundle的类加载器完成,按照根据在bundle的manifest中指定的顺序进行查找进行查找。如果没有找到类或者资源,搜索继续进行。
  5. 使用bundle本身的内部bundle类路径查找完毕之后,。如果类或者资源还没有找到,搜索继续到下一步。
  6. 查找每一个附加的fragment的内部类路径,fragment的查找根据bundle ID顺序升序查找。如果没有找到类或者资源的,查找过程继续下一步。
  7. 如果包中类或者资源所在的包由bundle导出,或者包由bundle导入(使用Import-Package或者Require-Bundle),查找结束,即类或者资源没有找到。
  8. 否则,如果类或者资源所在的包是通过使用DynamicImport-Package进行导入,那么试图进行包的动态导入。导出者exporter必须符合包约束。如果找到了合适的导出者exporter,然后建立连接,以后的包导入就可以通过步骤三进行。如果连接建立失败,那么请求失败。
  9. 如果动态导入建立了,请求交由导出bundle的类加载器代理。如果代理查找失败,那么查找过程中止,请求失败

 

自定义类加载器

通常情况下,我们都是直接使用系统类加载器。但是,有的时候,我们也需要自定义类加载器。比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。自定义类加载器一般都是继承自 ClassLoader 类,从上面对 loadClass 方法来分析来看,我们只需要重写 findClass 方法即可。下面我们通过一个示例来演示自定义类加载器的流程:

package com.classloader;

import java.io.*;


public class MyClassLoader extends ClassLoader {

    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace(\'.\', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("E:\\\\temp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.classloader.Test2");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

 

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。由于这里只是演示,我并未对class文件进行加密,因此没有解密的过程。这里有几点需要注意:

1、这里传递的文件名需要是类的全限定性名称,即com.paddx.test.classloading.Test格式的,因为 defineClass 方法是按这种格式进行处理的。

2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。

3、这类Test 类本身可以被 AppClassLoader 类加载,因此我们不能把 com/paddx/test/classloading/Test.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。

 

 

参考:

http://blog.csdn.net/ns_code/article/details/17881581

https://www.cnblogs.com/ityouknow/p/5603287.html

 

以上是关于JVM活学活用——类加载机制的主要内容,如果未能解决你的问题,请参考以下文章

活学活用flex制骰子

活学活用flex制骰子

活学活用flex制骰子

活学活用 screen

活学活用wxPython基础框架

pandas pivot_table 活学活用实例教程