类加载机制及双亲委派

所谓的无关性:
我们一直在强调Java的平台无关性,所谓的一次编写,到处运行。
那么究竟是什么造就了这种平台无关性,那就是Class文件,我们注意到编译完java文件后,会产生一个Class文件,而这些Class文件在各种虚拟机中的有相同的存储格式—–字节码。
除了平台无关性,Java语言还引申出来一个语言无关性。也就是说Java虚拟机是与Class这种特定格式的文件格式关联,所以就说明无论哪一种语言,只要经过编译器能转换为这种Class文件,那么就可以在java虚拟机上运行 。像类似于JRuby、Jython、Scala等的语言。

  • 类文件结构:

既然是Class文件造就了平台无关性,我们就有必要研究一下它的内部结构。

Class文件是一组以8位字节为单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件之中,中间没有添加任何分隔符,这使得Class的文件中存储的内容几乎全部都是程序运行所必要的内容。

Class的文件格式由两部分组成:无符号数和表。
无符号数属于基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数。
表是由多个无符号数或者其他表组成的复合数据类型,所有表都以_info结尾,描述具有层次关系的复合结构的数据。
无论是表还是无符号数,当描述多个数据时,经常要使用一个前置的容量计数器加若干个连续的数据项的形式。

具体描述:
Class文件的前4个字节是“魔数”,作用是确定这个文件是不是被这个虚拟机能接收的Class文件。Class的魔数值为0xCAFEBABY。
紧接着的4个字节是Class文件的版本号:第5和第6个字节是此版本号,第7和第8是主版本号。(关于版本号:JDK能向下兼容以前版本的Class文件,但是不能运行以后版本的Class文件。)

版本号之后便是常量池的入口,是Class文件结构中与其他项目关联最多的数据类型。

由于常量池中的常量数量不是固定的,所以常量池的首位一项u2(2个字节)类型的数据,代表常量池的容量计数值。
注意:这个常量计数值是从1开始的,将0空出来是为了以后需要表达:不引用任何一个常量池的项目。

常量池中主要存储字面量和符号引用。字面量类似于Java语言中常量的概念(字符串、final常量值等)。而符号引用包含三种常量:类和接口的全限定名;字段的名称和描述符;方法的名称和描述符。

常量池结束后紧接的2个字节代表访问标志


  • 类加载机制:

我们说了Class文件的结构,但是这些文件都要被加载到虚拟机中后才能使用,虚拟机如何加载这些Class文件。

虚拟机将描述类的数据从Class文件加载到内存,并对这些数据进行校验、解析以及初始化,最终形成可被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。

在Java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的。

类的生命周期:
类被加载到内存以后经过5个阶段被卸载出内存
加载、连接、初始化、使用、卸载。其中连接又分为验证、准备、解析
如图:

类加载机制及双亲委派_第1张图片

如图中所示,类的加载过程必须按部就班的按顺序开始,但是解析过程某些情况会在初始化阶段之后再开始(为了支持Java语言的动态绑定)。

是什么时候要加载类:

1、使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入到常量池的静态字段除外),以及调用一个类的静态方法的时候。

2、使用java.lang.reflect包的方法对类进行反射调用时,如果此类没有初始化,则要触发其初始化。

3、当初始化一个类时,发现其父类还没有进行初始化,则需要先触发其父类的类的初始化

4、当虚拟机启动时,用户需要指定一个要运行的主类(包括main方法的那个类),虚拟机会先初始化这个主类。

5、JDK1.7新增:如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法的句柄所对应的类没有进行过初始化,则需要先触发其初始化。

上面5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用
被动引用实例:

1、通过子类引用父类的静态字段,不会导致子类的初始化
2、通过数组来定义引用类,不会触发此类的初始化
3、常量在编译阶段会存入调用类的常量池,本质上并没有直接引用到定义常
量的类

类加载过程:
加载:
虚拟机在加载要完成的事情:

1、通过一个类的全限定名来获取定义此类的二进制字节流
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区
这个类的各种数据的访问入口。

既然如此那么它从哪里获取此类的二进制字节流,可以从JAR、EAR、WAR等格式中读取;从网络中获取;运行时计算生成,动态代理;从其他文件中读取等等。

验证:
这阶段的目的是为了确保Class文件的字节流是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。大致分为四个阶段
文件格式验证:是否符合当前虚拟机处理的Class文件格式
元数据验证:进行语义分析,确保描述的信息符合Java语言的规范要求(是否继承了被final修饰的类等)
字节码验证:确保程序语义是合法并且符合逻辑的(把对象赋给毫无关联的一个数据类型)。如果一个类的方法体的字节码没有通过字节码验证,那肯定是有问题的。
符号引用验证:对常量池中的各种符号引用的信息进行匹配性校验(能否被访问、通过全限定名能否找到对应类等)

准备:
为类变量分配内存并设置初始值,这些变量所用的内存都在方法区分配。
假设public static int value = 123;在准备阶段过后value的初始值是0而不是123,因为把value赋值为123的动作在初始化时期才会执行。

但是如果是 public static final int value = 123; 在编译时期就会为value生成ConstantValue属性,在准备阶段就会根据ConstantValue将value复制为123。

解析:
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。
符号引用是一组符号来描述所引用的目标(方法名),而直接引用则是直接指向目标的指针(地址)

初始化:
开执行类中定义的java程序代码(字节码)
静态块/静态属性==》非静态块/属性===》构造器

类加载器:
通过类加载器来实现类加载机制。
任意一个类在Java虚拟机中的唯一性由这个类本身和加载它的类加载器确定
两个类是否相等,即使源于同一个Class文件,被同一个虚拟机加载,但只有加载他们的类加载器不同,这两个类就不相等。

通过自己实现的类加载器说明,这两个不相等

public class MyClassLoader {

    public static void main(String[] args) throws Exception {
         ClassLoader myLoader = new ClassLoader(){

            @Override
            public Class loadClass(String name)throws ClassNotFoundException {

                try {
                    String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if(is == null)
                        return super.loadClass(name);

                    byte[] b = new byte[is.available()];
                    is.read(b);

                    return defineClass(name,b,0,b.length);


                } catch (IOException e) {
                     throw new ClassNotFoundException(name);
                }
            }

         };

         //可以看出相同的类由不同的类加载器加载
         Object obj = myLoader.loadClass("com.reflectandClassLoad.MyClassLoader").newInstance();
         System.out.println(obj.getClass().getClassLoader());//com.reflectandClassLoad.MyClassLoader$1@c17164
         System.out.println(MyClassLoader.class.getClassLoader());//sun.misc.Launcher$AppClassLoader@19821f

         //可以看出两个类是否相等,条件是类本身和加载此类的加载器
         System.out.println(obj instanceof com.reflectandClassLoad.MyClassLoader);//false

    }

  • 双亲委派模型:

Java自带的类加载器:
启动类加载器(Bootstrap ClassLoader):这是由C++语言实现的一个加载器,是虚拟机的一部分,随虚拟机启动运行。负责将存放在/lib目录下面或者被-Xbootclasspath参数所指定的路径中的类。
扩展类加载器(Extension ClassLoader):负责加载/lib/ext目录中的,或者被java.ext.dir系统变量指定路径中的所有类库。如果把自己的jar包放到此位置,会首先用这个加载器加载。
应用程序类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)上所指定的类库,如果程序中没有自定义自己的类加载器,这个就是默认加载器。

后两个都是ClassLoader的子类,由纯JAVA语言编写。

这么多类加载器,当需要加载一个类的时候,究竟是谁来加载?
上面说到的三种类加载器,从上到下是父子关系(一般不会是继承,更多是组合),如图所示,这种层次关系,称为类加载器的双亲委派模型。

类加载机制及双亲委派_第2张图片

双亲委派模型是指

如果一个类收到了类加载的请求,不会自己先尝试加载,先找父类加载器去
完成。当顶层启动类加载器表示无法加载这个类的时候,子类才会尝试自己
去加载。当回到最开的发起者加载器还无法加载时,并不会向下找,而是抛
出ClassNotFound异常。(如果类A中引用了类B,Java虚拟机将使用加
载类A的类加载器来加载类B)

我们构造一个场景:java.lang.Object类在rt.jar,按照双亲委派的话它就只能被启动类加载器加载,因而Object在各类的类加载器环境中都是同一个类。
如果不是双亲委派,那么用户在自己的classpath编写了一个java.lang.Object的类,那就无法保证Object的唯一性。所以使用双亲委派,即使自己编写了,但是永远都不会被加载运行。

双亲委派对于Java程序的稳定运作很重要。

破坏双亲委派:
这种双亲委派机制并不是一种强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。
线程上下文类加载器,这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那么这个类加载器就是应用程序类加载器。像JDBC就是采用了这种方式。
这种行为就是逆向使用了加载器,违背了双亲委派模型的一般性原则。

你可能感兴趣的:(深入理解Java虚拟机)