zoukankan      html  css  js  c++  java
  • jvm-类加载机制

    类加载机制

     

    参考:

     http://www.importnew.com/25295.html

    https://blog.csdn.net/u013256816/article/details/50829596

    https://blog.csdn.net/boyupeng/article/details/47951037

     

     

    如下图所示,JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。

    加载

    加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。

    验证

    这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

    准备

    准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:

    1
    public static int v = 8080;

    实际上变量v在准备阶段过后的初始值为0而不是8080,将v赋值为8080的putstatic指令是程序被编译后,存放于类构造器<client>方法之中,这里我们后面会解释。
    但是注意如果声明为:

    1
    public static final int v = 8080;

    在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080。

    解析

    解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class文件中的:

    • CONSTANT_Class_info
    • CONSTANT_Field_info
    • CONSTANT_Method_info

    等类型的常量。

    下面我们解释一下符号引用和直接引用的概念:

    • 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
    • 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

    初始化

    初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。

    初始化阶段是执行类构造器<client>方法的过程。<client>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证<client>方法执行之前,父类的<client>方法已经执行完毕。p.s: 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<client>()方法。

    注意以下几种情况不会执行类初始化:

    • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
    • 定义对象数组,不会触发该类的初始化。
    • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
    • 通过类名获取Class对象,不会触发类的初始化。
    • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
    • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

    类加载器

    虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了3种类加载器:

    • 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOMElib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
    • 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOMElibext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
    • 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。

    JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

    当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

    采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

    在有些情境中可能会出现要我们自己来实现一个类加载器的需求,由于这里涉及的内容比较广泛,我想以后单独写一篇文章来讲述,不过这里我们还是稍微来看一下。我们直接看一下jdk中的ClassLoader的源码实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    protected synchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        // First, check if the class has already been loaded
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
    • 首先通过Class c = findLoadedClass(name);判断一个类是否已经被加载过。
    • 如果没有被加载过执行if (c == null)中的程序,遵循双亲委派的模型,首先会通过递归从父加载器开始找,直到父类加载器是Bootstrap ClassLoader为止。
    • 最后根据resolve的值,判断这个class是否需要解析。

    而上面的findClass()的实现如下,直接抛出一个异常,并且方法是protected,很明显这是留给我们开发者自己去实现的,这里我们以后我们单独写一篇文章来讲一下如何重写findClass方法来实现我们自己的类加载器。

    1
    2
    3
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

    References

    UNDERSTANDING THE JVM

    public class SSClass
    {
        static
        {
            System.out.println("SSClass");
        }
    }    
    public class SuperClass extends SSClass
    {
        static
        {
            System.out.println("SuperClass init!");
        }
    
        public static int value = 123;
    
        public SuperClass()
        {
            System.out.println("init SuperClass");
        }
    }
    public class SubClass extends SuperClass
    {
        static 
        {
            System.out.println("SubClass init");
        }
    
        static int a;
    
        public SubClass()
        {
            System.out.println("init SubClass");
        }
    }
    public class NotInitialization
    {
        public static void main(String[] args)
        {
            System.out.println(SubClass.value);
        }
    }

    运行结果:

    SSClass
    SuperClass init!
    123

    答案答对了嚒? 
    也许有人会疑问:为什么没有输出SubClass init。ok~解释一下:对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。 
    上面就牵涉到了虚拟机类加载机制。如果有兴趣,可以继续看下去。


    类加载过程

    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。如图所示。 
    这里写图片描述 
    加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。以下陈述的内容都已HotSpot为基准。

    加载

    在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下3件事情:

    1. 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
    3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

    加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

    验证

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

    1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
    2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
    3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
    4. 符号引用验证:确保解析动作能正确执行。

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

    准备

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

        public static int value=123;

    那变量value在准备阶段过后的初始值为0而不是123.因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。 
    至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0.

    解析

    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

    初始化

    类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序制定的主观计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器<clinit>()方法的过程. 
    <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如下:

    public class Test
    {
        static
        {
            i=0;
            System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用)
        }
        static int i=1;
    }

    那么去掉报错的那句,改成下面:

    public class Test
    {
        static
        {
            i=0;
    //      System.out.println(i);
        }
        static int i=1;
    
        public static void main(String args[])
        {
            System.out.println(i);
        }
    }

    输出结果是什么呢?当然是1啦~在准备阶段我们知道i=0,然后类初始化阶段按照顺序执行,首先执行static块中的i=0,接着执行static赋值操作i=1,最后在main方法中获取i的值为1。

    <clinit>()方法与实例构造器<init>()方法不同,它不需要显示地调用父类构造器,虚拟机会保证在子类<cinit>()方法执行之前,父类的<clinit>()方法已经执行完毕,回到本文开篇的举例代码中,结果会打印输出:SSClass就是这个道理。 
    由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。 
    <clinit>()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产<clinit>()方法。 
    接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。 
    虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。

    package jvm.classload;
    
    public class DealLoopTest
    {
        static class DeadLoopClass
        {
            static
            {
                if(true)
                {
                    System.out.println(Thread.currentThread()+"init DeadLoopClass");
                    while(true)
                    {
                    }
                }
            }
        }
    
        public static void main(String[] args)
        {
            Runnable script = new Runnable(){
                public void run()
                {
                    System.out.println(Thread.currentThread()+" start");
                    DeadLoopClass dlc = new DeadLoopClass();
                    System.out.println(Thread.currentThread()+" run over");
                }
            };
    
            Thread thread1 = new Thread(script);
            Thread thread2 = new Thread(script);
            thread1.start();
            thread2.start();
        }
    }

    运行结果:(即一条线程在死循环以模拟长时间操作,另一条线程在阻塞等待)

    Thread[Thread-0,5,main] start
    Thread[Thread-1,5,main] start
    Thread[Thread-0,5,main]init DeadLoopClass

    需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个类加载器下,一个类型只会初始化一次。 
    将上面代码中的静态块替换如下:

            static
            {
                System.out.println(Thread.currentThread() + "init DeadLoopClass");
                try
                {
                    TimeUnit.SECONDS.sleep(10);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }

    运行结果:

    Thread[Thread-0,5,main] start
    Thread[Thread-1,5,main] start
    Thread[Thread-1,5,main]init DeadLoopClass (之后sleep 10s)
    Thread[Thread-1,5,main] run over
    Thread[Thread-0,5,main] run over

    虚拟机规范严格规定了有且只有5中情况(jdk1.7)必须对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

    1. 遇到new,getstatic,putstatic,invokestatic这失调字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
    2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
    3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
    5. 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

    开篇已经举了一个范例:通过子类引用付了的静态字段,不会导致子类初始化。 
    这里再举两个例子。 
    1. 通过数组定义来引用类,不会触发此类的初始化:(SuperClass类已在本文开篇定义)

    public class NotInitialization
    {
        public static void main(String[] args)
        {
            SuperClass[] sca = new SuperClass[10];
        }
    }

    运行结果:(无) 
    2. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化:

    public class ConstClass
    {
        static
        {
            System.out.println("ConstClass init!");
        }
        public static  final String HELLOWORLD = "hello world";
    }
    public class NotInitialization
    {
        public static void main(String[] args)
        {
            System.out.println(ConstClass.HELLOWORLD);
        }
    }

    运行结果:hello world

    一、为什么要使用类加载器?
    Java语言里,类加载都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会给java应用程序提供高度的灵活性。例如:
    1.编写一个面向接口的应用程序,可能等到运行时再指定其实现的子类;
    2.用户可以自定义一个类加载器,让程序在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分;(这个是Android插件化,动态安装更新apk的基础)

    二、类加载的过程

    使用java编译器可以把java代码编译为存储字节码的Class文件,使用其他语言的编译器一样可以把程序代码翻译成Class文件,java虚拟机不关心Class的来源是何种语言。如图所示:

    在Class文件中描述的各种信息,最终都需要加载到虚拟机中才能运行和使用。那么虚拟机是如何加载这些Class文件的呢?
    JVM把描述类数据的字节码.Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。

    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称链接。

    加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类的加载过程必须按照这种顺序开始,而解析阶段不一定;它在某些情况下可以在初始化之后再开始,这是为了运行时动态绑定特性(JIT例如接口只在调用的时候才知道具体实现的是哪个子类)。值得注意的是:这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。

    1.加载:(重点)
    加载阶段是“类加载机制”中的一个阶段,这个阶段通常也被称作“装载”,主要完成:
    1.通过“类全名”来获取定义此类的二进制字节流

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

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

    相对于类加载过程的其他阶段,加载阶段(准备地说,是加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。

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

    2.验证:(了解)

    验证是链接阶段的第一步,这一步主要的目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
    验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

    1.文件格式验证

     验证class文件格式规范,例如: class文件是否已魔术0xCAFEBABE开头 , 主、次版本号是否在当前虚拟机处理范围之内等

    2.元数据验证

    这个阶段是对字节码描述的信息进行语义分析,以保证起描述的信息符合java语言规范要求。验证点可能包括:这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)、这个类是否继承了不允许被继承的类(被final修饰的)、如果这个类的父类是抽象类,是否实现了起父类或接口中要求实现的所有方法。

    3.字节码验证

     进行数据流和控制流分析,这个阶段对类的方法体进行校验分析,这个阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如:保证访法体中的类型转换有效,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但不能把一个父类对象赋值给子类数据类型、保证跳转命令不会跳转到方法体以外的字节码命令上。

    4.符号引用验证

    符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否可被当前类访问。

    3.准备:(了解)

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为:

    public static int value  = 12;

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

    上面所说的“通常情况”下初始值是零值,那相对于一些特殊的情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,建设上面类变量value定义为:

    public static final int value = 123;

    编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。

    4.解析:(了解)
    解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
    符号引用:符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。

    直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

    虚拟机规范并没有规定解析阶段发生的具体时间,只要求了在执行anewarry、checkcast、getfield、instanceof、invokeinterface、invokespecial、invokestatic、invokevirtual、multianewarray、new、putfield和putstatic这13个用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析,所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

    解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。

    1.类、接口的解析

    2.字段解析

    3.类方法解析

    4.接口方法解析

    5.初始化:(了解)

    类的初始化阶段是类加载过程的最后一步,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。在以下四种情况下初始化过程会被触发执行:

    1.遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。生成这4条指令的最常见的java代码场景是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用类的静态方法的时候。

    2.使用java.lang.reflect包的方法对类进行反射调用的时候

    3.当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先出发其父类的初始化

    4.jvm启动时,用户指定一个执行的主类(包含main方法的那个类),虚拟机会先初始化这个类

    在上面准备阶段 public static int value  = 12;  在准备阶段完成后 value的值为0,而在初始化阶调用了类构造器<clinit>()方法,这个阶段完成后value的值为12。

    *类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句快可以赋值,但是不能访问。

    *类构造器<clinit>()方法与类的构造函数(实例构造函数<init>()方法)不同,它不需要显式调用父类构造,虚拟机会保证在子类<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中的第一个执行的<clinit>()方法的类肯定是java.lang.Object。

    *由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句快要优先于子类的变量赋值操作。

    *<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句,也没有变量赋值的操作,那么编译器可以不为这个类生成<clinit>()方法。

    *接口中不能使用静态语句块,但接口与类不太能够的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

    *虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个进程阻塞。

    三、类加载器

    JVM设计者把类加载阶段中的“通过'类全名'来获取定义此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

    1.类与类加载器

    对于任何一个类,都需要由加载它的类加载器和这个类来确立其在JVM中的唯一性。也就是说,两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。

    2.双亲委派模型

    从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。

    从Java开发人员的角度来看,大部分Java程序一般会使用到以下三种系统提供的类加载器:
    1)启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOMElib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器无法被Java程序直接引用。
    2)扩展类加载器(Extension ClassLoader):该加载器主要是负责加载JAVA_HOMElib,该加载器可以被开发者直接使用。
    3)应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

    我们的应用程序都是由这三类加载器互相配合进行加载的,我们也可以加入自己定义的类加载器。这些类加载器之间的关系如下图所示:

    如上图所示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parent Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。


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


    使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。否则的话,如果不使用该模型的话,如果用户自定义一个java.lang.Object类且存放在classpath中,那么系统中将会出现多个Object类,应用程序也会变得很混乱。如果我们自定义一个rt.jar中已有类的同名Java类,会发现JVM可以正常编译,但该类永远无法被加载运行。
    在rt.jar包中的java.lang.ClassLoader类中,我们可以查看类加载实现过程的代码,具体源码如下:

    [java] view plain copy
     
    1. protected synchronized Class loadClass(String name, boolean resolve)  
    2.         throws ClassNotFoundException {  
    3.     // 首先检查该name指定的class是否有被加载  
    4.     Class c = findLoadedClass(name);  
    5.     if (c == null) {  
    6.         try {  
    7.             if (parent != null) {  
    8.                 // 如果parent不为null,则调用parent的loadClass进行加载  
    9.                 c = parent.loadClass(name, false);  
    10.             } else {  
    11.                 // parent为null,则调用BootstrapClassLoader进行加载  
    12.                 c = findBootstrapClass0(name);  
    13.             }  
    14.         } catch (ClassNotFoundException e) {  
    15.             // 如果仍然无法加载成功,则调用自身的findClass进行加载  
    16.             c = findClass(name);  
    17.         }  
    18.     }  
    19.     if (resolve) {  
    20.         resolveClass(c);  
    21.     }  
    22.     return c;  
    23. }  

    通过上面代码可以看出,双亲委派模型是通过loadClass()方法来实现的,根据代码以及代码中的注释可以很清楚地了解整个过程其实非常简单:先检查是否已经被加载过,如果没有则调用父加载器的loadClass()方法,如果父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,则先抛出ClassNotFoundException,然后再调用自己的findClass()方法进行加载。

    3.自定义类加载器

    若要实现自定义类加载器,只需要继承java.lang.ClassLoader 类,并且重写其findClass()方法即可。java.lang.ClassLoader 类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class 类的一个实例。除此之外,ClassLoader 还负责加载 Java 应用所需的资源,如图像文件和配置文件等,ClassLoader 中与加载类相关的方法如下:

     
    方法                                 说明
    getParent()  返回该类加载器的父类加载器。

    loadClass(String name) 加载名称为 二进制名称为name 的类,返回的结果是 java.lang.Class 类的实例。

    findClass(String name) 查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。

    findLoadedClass(String name) 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。

    resolveClass(Class<?> c) 链接指定的 Java 类。


    注意:在JDK1.2之前,类加载尚未引入双亲委派模式,因此实现自定义类加载器时常常重写loadClass方法,提供双亲委派逻辑,从JDK1.2之后,双亲委派模式已经被引入到类加载体系中,自定义类加载器时不需要在自己写双亲委派的逻辑,因此不鼓励重写loadClass方法,而推荐重写findClass方法。

    在Java中,任意一个类都需要由加载它的类加载器和这个类本身一同确定其在java虚拟机中的唯一性,即比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两个类来源于同一个Class类文件,只要加载它的类加载器不相同,那么这两个类必定不相等(这里的相等包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法和instanceof关键字的结果)。例子代码如下:

    [java] view plain copy
     
    1. /** 
    2.      * 一、ClassLoader加载类的顺序 
    3.      *  1.调用 findLoadedClass(String) 来检查是否已经加载类。 
    4.      *  2.在父类加载器上调用 loadClass 方法。如果父类加载器为 null,则使用虚拟机的内置类加载器。 
    5.      *  3.调用 findClass(String) 方法查找类。 
    6.      * 二、实现自己的类加载器 
    7.      *  1.获取类的class文件的字节数组 
    8.      *  2.将字节数组转换为Class类的实例 
    9.      * @author lei 2011-9-1 
    10.      */  
    11.     public class ClassLoaderTest {  
    12.         public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {  
    13.             //新建一个类加载器  
    14.             MyClassLoader cl = new MyClassLoader("myClassLoader");  
    15.             //加载类,得到Class对象  
    16.             Class<?> clazz = cl.loadClass("classloader.Animal");  
    17.             //得到类的实例  
    18.             Animal animal=(Animal) clazz.newInstance();  
    19.             animal.say();  
    20.         }  
    21.     }  
    22.     class Animal{  
    23.         public void say(){  
    24.             System.out.println("hello world!");  
    25.         }  
    26.     }  
    27.     class MyClassLoader extends ClassLoader {  
    28.         //类加载器的名称  
    29.         private String name;  
    30.         //类存放的路径  
    31.         private String path = "E:\workspace\Algorithm\src";  
    32.         MyClassLoader(String name) {  
    33.             this.name = name;  
    34.         }  
    35.         MyClassLoader(ClassLoader parent, String name) {  
    36.             super(parent);  
    37.             this.name = name;  
    38.         }  
    39.         /** 
    40.          * 重写findClass方法 
    41.          */  
    42.         @Override  
    43.         public Class<?> findClass(String name) {  
    44.             byte[] data = loadClassData(name);  
    45.             return this.defineClass(name, data, 0, data.length);  
    46.         }  
    47.         public byte[] loadClassData(String name) {  
    48.             try {  
    49.                 name = name.replace(".", "//");  
    50.                 FileInputStream is = new FileInputStream(new File(path + name + ".class"));  
    51.                 ByteArrayOutputStream baos = new ByteArrayOutputStream();  
    52.                 int b = 0;  
    53.                 while ((b = is.read()) != -1) {  
    54.                     baos.write(b);  
    55.                 }  
    56.                 return baos.toByteArray();  
    57.             } catch (Exception e) {  
    58.                 e.printStackTrace();  
    59.             }  
    60.             return null;  
    61.         }  
    62.     }  

    类加载器双亲委派模型是从JDK1.2以后引入的,并且只是一种推荐的模型,不是强制要求的,因此有一些没有遵循双亲委派模型的特例:(了解)

    (1).在JDK1.2之前,自定义类加载器都要覆盖loadClass方法去实现加载类的功能,JDK1.2引入双亲委派模型之后,loadClass方法用于委派父类加载器进行类加载,只有父类加载器无法完成类加载请求时才调用自己的findClass方法进行类加载,因此在JDK1.2之前的类加载的loadClass方法没有遵循双亲委派模型,因此在JDK1.2之后,自定义类加载器不推荐覆盖loadClass方法,而只需要覆盖findClass方法即可。

    (2).双亲委派模式很好地解决了各个类加载器的基础类统一问题,越基础的类由越上层的类加载器进行加载,但是这个基础类统一有一个不足,当基础类想要调用回下层的用户代码时无法委派子类加载器进行类加载。为了解决这个问题JDK引入了ThreadContext线程上下文,通过线程上下文的setContextClassLoader方法可以设置线程上下文类加载器。

    JavaEE只是一个规范,sun公司只给出了接口规范,具体的实现由各个厂商进行实现,因此JNDI,JDBC,JAXB等这些第三方的实现库就可以被JDK的类库所调用。线程上下文类加载器也没有遵循双亲委派模型。

    (3).近年来的热码替换,模块热部署等应用要求不用重启java虚拟机就可以实现代码模块的即插即用,催生了OSGi技术,在OSGi中类加载器体系被发展为网状结构。OSGi也没有完全遵循双亲委派模型。

    4.动态加载Jar && ClassLoader 隔离问题

    动态加载Jar:

    Java 中动态加载 Jar 比较简单,如下:

    [java] view plain copy
     
    1. URL[] urls = new URL[] {new URL("file:libs/jar1.jar")};  
    2. URLClassLoader loader = new URLClassLoader(urls, parentLoader);  

    表示加载 libs 下面的 jar1.jar,其中 parentLoader 就是上面1中的 parent,可以为当前的 ClassLoader。


    ClassLoader 隔离问题:

    大家觉得一个运行程序中有没有可能同时存在两个包名和类名完全一致的类?
    JVM 及 Dalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个”类”不是由一个 ClassLoader 加载,是无法将一个类的示例强转为另外一个类的,这就是 ClassLoader 隔离。 如 Android 中碰到如下异常

    [java] view plain copy
     
    1. android.support.v4.view.ViewPager can not be cast to android.support.v4.view.ViewPager  

    当碰到这种问题时可以通过 instance.getClass().getClassLoader(); 得到 ClassLoader,看 ClassLoader 是否一样。

    加载不同 Jar 包中公共类:

    现在 Host 工程包含了 common.jar, jar1.jar, jar2.jar,并且 jar1.jar 和 jar2.jar 都包含了 common.jar,我们通过 ClassLoader 将 jar1, jar2 动态加载进来,这样在 Host 中实际是存在三份 common.jar,如下图:

    https://farm4.staticflickr.com/3872/14301963930_2f0f0fe8aa_o.png

    我们怎么保证 common.jar 只有一份而不会造成上面3中提到的 ClassLoader 隔离的问题呢,其实很简单,在生成 jar1 和 jar2 时把 common.jar 去掉,只保留 host 中一份,以 host ClassLoader 为 parentClassLoader 即可。

    最后:

    一道面试题

    能不能自己写个类叫java.lang.System?

    答案:通常不可以,但可以采取另类方法达到这个需求。 
    解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。

    但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。

  • 相关阅读:
    (7)排序之归并排序
    (5)排序之简单选择排序
    (4)排序之希尔排序
    (3)排序之直接插入排序
    (2)排序之快速排序
    (1)排序之冒泡排序
    Python学习
    centos下docker网络桥接
    docker下搭建gitlab
    centos版本7以上网卡名修改
  • 原文地址:https://www.cnblogs.com/xuwc/p/8727486.html
Copyright © 2011-2022 走看看