美文网首页
JVM的类加载机制

JVM的类加载机制

作者: 大大大大大先生 | 来源:发表于2018-01-31 01:28 被阅读19次

    类的生命周期

    image.png

    其中,加载,验证,准备,初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序开始,而类的解析不一定,类的解析可能在初始化阶段之后再开始,这是为了支持Java语言的动态绑定

    Java的动态绑定和静态绑定

    在Java中,当你调用一个方法时,可能会在编译时期(compile time)解析(resolve),也可能实在运行时期(runtime)解析,这全取决于到底是一个静态方法(static method)还是一个虚方法(virtual method)。如果是在编译时期解析,那么就称之为静态绑定(static binding),如果方法的调用是在运行时期解析,那就是动态绑定(dynamic binding)或者延迟绑定(late binding)。Java是一门面向对象的编程语言,优势就在于支持多态(Polymorphism)。多态使得父类型的引用变量可以引用子类型的对象。如果调用子类型对象的一个虚方法(非private,final or static),编译器将无法找到真正需要调用的方法,因为它可能是定义在父类型中的方法,也可能是在子类型中被重写(override)的方法,这种情形,只能在运行时进行解析,因为只有在运行时期,才能明确具体的对象到底是什么。这也是我们俗称的运行时或动态绑定(runtime or dynamic binding)。另一方面,private static和final方法将在编译时解析,因为编译器知道它们不能被重写,所有可能的方法都被定义在了一个类中,这些方法只能通过此类的引用变量进行调用。这叫做静态绑定或编译时绑定(static or compile time binding)。所有的private,static和final方法都通过静态绑定进行解析。这两个概念的关系,与“方法重载”(overloading,静态绑定)和“方法重写”(overriding,动态绑定)类似。动态绑定只有在重写可能存在时才会用到,而重载的方法在编译时期即可确定(这是因为它们总是定义在同一个类里面)

    总而言之,其区别如下:

    ①静态绑定在编译时期,动态绑定在运行时期。

    ②静态绑定只用到类型信息,方法的解析根据引用变量的类型决定,而动态绑定则根据实际引用的的对象决定

    ③在java中,private static 和 final 方法都是静态绑定,只有虚方法才是动态绑定

    ④多态是通过动态绑定实现的。

    类在何时会被初始化

    • 遇到new(创建对象), getstatic(读取静态字段),putstatic(设置静态字段),invokestatic(执行静态方法)的指令的时候,类才会被初始化
    • 使用Java类中的反射,对类进行反射调用
    • 当初始化子类的时候发现父类还没被初始化,这时候会先初始化父类,然后再初始化子类
    • JVM启动的时候Java程序的入口main方法所在的类
    • Java的动态语言支持:MethodHandler中有涉及到static字段的读取或者设置,static方法的调用,这些也需要类进行初始化,JDK1.7中,Java的方法也能过当作方法的参数进行传递,类似于c语言中的函数指针
    • 只有以上这几种情况才可能主动触发类的初始化,其他的情况,类的初始化都是被动的
      例如:
    public class SubClass extends SuperClass {
    
        static {
            System.out.println("I am sub class!");
        }
    }
    
    public class SuperClass {
    
        public static int value = 9;
    
        static {
            System.out.println("I am super class!");
        }
    }
    
    public class DemoMain {
    
        public static void main(String[] args) {
            System.out.println("test:" + SubClass.value);
        }
    
    }
    

    SuperClass中定义了一个static字段value,在程序运行的时候通过子类区调用value,这时候只会对SuperClass进行初始化,而不会对SubClass初始化,运行结果如下:

    I am super class!
    test:9
    

    所以对于静态字段的引用,虽然是SubClass.value,但是由于value是static的,并且是在SuperClass中声明的,所以只会初始化SuperClass,当使用SubClass.value来访问value的时候,这时候是跟SubClass无关的,父类中的static字段或者static方法是可以被子类覆盖的,如果子类中有相同的声明,那么对于这个子类就会默认覆盖掉父类中对应的声明,如果子类中没有相同的声明,那么就直接继承自父类中的相关方法或者变量,但是static方法不具有多态,因为多态是对于一个对象来说才有多种形态,对于类来说没有多态的概念,而static方法就是相对于类而言的,因为static方法不具备多态特性,举个例子:

    A是父类,B继承自A
    
    A b = new B();
    b.callMethod();
    这就是b对象的多态,A中声明了callMethod方法,B中也可能也声明了callMethod方法,那么在b对象调用callMethod方法的时候执行的是B对象中重写的callMethod方法,这是在运行期间才知道callMethod方法要调用的是B的方法,多态就是Java中的动态绑定,在运行期间才能确定具体调用的是哪一个方法
    

    类加载过程

    • 通过一个类的全限定名来获取定义此类的二进制流,这句话的意思是:java文件经过编译后会生成class文件,class文件只是字节码,这个机器无法识别,所以在类加载的时候需要先把class字节码转换成二进制的格式,这样机器才能识别
    • 将前面由class字节码转换过来的二进制字节流代表的静态存储结构转化成方法区的运行时数据结构,这里我的理解是,一个类的信息(类全限定名,类中的方法,字段等)是存储在方法区中的,这里就是把二进制字节流识别里面的一些类信息结构,然后存储到指定的方法去(这里其实说的很模糊)
    • 在内存中生成一个Class对象来作为方法区这个类的访问入口

    在类的加载过程中,数组的加载不需要类加载器来加载,而是java虚拟机直接创建的,但是数据中的元素如果还是一个类对象,那么这个元素对应的类也需要类加载器来加载;在生成Class对象的时候需要注意,Class对象其实是一个对象,但是它是存在方法区中的,正常情况下Class对象的引用存在JVM栈中,而对象实际分配的内存空间是在堆内存中

    类的验证

    • 类的验证是为了确保class文件的字节流中的信息符合当前虚拟机的要求,并不会导致虚拟机出现崩溃,如果验证失败了那么JVM会抛出一个java.lang.VerifyError异常或者其子类,我曾经就遇到过这么一个异常,比如在Android4.0的系统上ReflectiveOperationException这个异常类,但是在Android6.0上就有这个类,那么在6.0下面编译成功的apk放到4.0系统上运行,如果是如下代码就会在apk安装启动的时候就会报这个VerifyError异常:
    try {
                // ...
            } catch (ReflectiveOperationException e) {
                
            }
    

    但是如果修改成下面这种,在程序不运行到这里的时候不会崩溃,一切正常,一旦程序运行到这里,那么就会报NoClassDefFoundError异常:

    System.out.println("" + ReflectiveOperationException.class);
    

    这个说明在程序运行到这里的时候JVM才去加载ReflectiveOperationException这个类,但是加载失败,而前面一个是程序一启动运行的时候,JVM会去加载引用ReflectiveOperationException的这个类,并且加载成功了,然后就会对引用了ReflectiveOperationException的这个类进行验证,说明JVM在解析try catch语句的时候是有所不同的

    类验证主要做了以下几种验证:

    • 文件格式验证,例如魔数是否正确, 常量池中的常量是否有不被支持的常量类型(检查常量tag标志)等
    • 元数据,这个其实就是验证语法的合法性,例如abstract修饰的父类方法是否被子类实现等
    • 字节码验证,这个很复杂,暂时不了解,也不想了解
    • 符号引用的验证,符号中的字符串描述的全限定名是否能找到对应的类,类中是否能找到指定的方法字段,符号引用中的类,字段,方法的访问性(private,protected,public)是否能够被成功访问,如果不行就会抛出NoSuchFieldError,NoSuchMethodError,IllegalAccessError

    类的准备阶段

    • 准备阶段是为类变量分配内存并设置初始值,这些变量所使用的内存都是在方法区中分配的,但是要注意,准备阶段所分配内存的变量都是被static修饰的,如果是实例变量或者其他局部变量,那会随着对象的实例化在堆内存中分配,假设:
    public static final int value = 9;
    

    那么在类准备阶段后,由于value变量被static修饰,那么value的初始值会为0,所以value就可以被其他类访问,当类初始化之后,执行了<clinit> ()方法,value的值才是9

    类的解析

    • 解析就是把class文件中的符号引用转换成直接引用,符号引用就是类的全限定名,可以是任意的字面量,引用的目标不一定已经加载到内存中,而直接引用就是直接指向目标的指针,引用对象一定需要已经被加载到内存中;Java中的多态(动态绑定)其实就是跟类的解析有关,类的解析可能发生在程序运行期间(类初始化之后),因为对于多态来说在类的加载,验证,准备过程中并不知道实际要调用哪一个对象的方法,只有在执行代码的时候才知道实际需要执行哪一个对象的方法

    类初始化

    • 类初始化是类加载过程的最后一步了,初始化其实就是执行构造器的过程,构造器是JVM自动生成的,它是去自动搜集类的变量,静态代码块中的语句合并产生的
    • <clinit>()和类的构造函数不同,JVM会保证子类执行<clinit>()方法之前会先执行父类的<clinit>()方法,不需要想构造函数一样需要显式的调用父类的构造函数,所以Object的<clinit>()一定是最先执行的
    • <clinit>()不是必须的,如果类中没有对变量进行赋值操作,也没有静态代码块,那么就没有<clinit>()方法
    • 虚拟机会保证<clinit>()方法在多线程的环境下同步执行,所以如果多线程同时去初始化一个类,那么同一个时刻只有一个线程去执行<clinit>()方法,其他线程都会等待,如果在一个类<clinit>()方法中有耗时人物,可能造成多线程阻塞,例如在静态代码块中执行耗时操作

    类加载器

    • 类加载器(ClassLoader)注意一点,比较两个类是否“相等”的前提必须是这两个类必须是同一个类加载加载的才有意义,如果A类是由ClassLoader1加载,同时A类也由ClassLoader2加载了一次,那么ClassLoader1加载的A和ClassLoader2加载的A不属于同一个类,虽然都来自同一个Class文件,但是由于是不同的类加载器加载的所以依然是两个独立的类
    • 双亲委派模型,双亲委派模型的工作过程是如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类的加载器去完成,每一个层次的类加载器都是如此,因此所以类加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈无法完成这个加载请求的时候,子类加载器才会尝试自己去加载,使用双亲委派模型来进行类加载的一个好处就是确保类加载的唯一性,例如Object对象被启动类加载器加载过了,那么只要是java.lang.Object这个类全限定名来加载的都会去顶层的类加载器中找到已经加载成功的Object对象,确保了Object的唯一性,避免不同的ClassLoader多次加载同一个类全限定名的类

    相关文章

      网友评论

          本文标题:JVM的类加载机制

          本文链接:https://www.haomeiwen.com/subject/ktfdaxtx.html