JVM之类加载机制(七)

前言:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校检、转换解析和初始化,最终形成可以直接被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
      与那些编译时需要连接工作的语言(比如C)不同,java语言类型的加载、连接和初始化过程都是在运行期间完成的,这种策略虽然会令类在加载时稍微增加一些性能开销,但为java程序提供高度的灵活性,java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。
比如编写一个面向接口的应用程序,可以等到运行时在指定其实际的实现类;通过就AVAV啊预定义和自定义类加载器,让本地程序在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分,这种组装应用程序的方式目前已经应用于Java程序之中。(比如参数类型是一个接口,只外部其他实际类继承这个接口,就可以动态加载这个类并作为参数)
      类加载的时机:
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段

加载、验证、准备、初始化和卸载这五个阶段顺序确定,类的加载过程必须按着这种顺序按部就班的开始,而解析则不一定;某些情况下可以在初始化阶段之后开始,为了支持java语言的运行时绑定。这里按部就班的开始不是按部就班的进行或完成,是因为这些阶段通常互相交叉混合式进行,比如类加载阶段变成二进制流时会有验证进行,并不是类加载阶段进行结束才会有验证。
      什么情况下执行第一阶段;加载,这点没有进行约束,由虚拟机自由实现,但初始化(加载、验证、准备要在此之前必须开始)规定有且只有五种情况必须立即对类进行初始化:
      1)遇到以下四条指令:
      new(使用new关键字实例化对象)
      getstatic(读取一个类的静态字段(如果被final修饰、已在编译期把结果放在常量池的静态字段除外))
      putstatic(设置一个类的静态字段(如果被final修饰(在准备阶段数据赋值零值时就已经初始化赋值,而是static要等初始化来完成))
      invokestatic(调用一个类的静态方法时)
如果类还没有进行初始化则需要先触发其初始化。
      2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类(类型类)还没有初始化,则需要先触发其初始化。
      3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类初始化(接口则不需要)。
      4)虚拟机启动的时候,用户妖妖指定一个执行的主类(包含main方法的那个类),虚拟机先初始化这个主类。
      5)使用动态语言(1.7)支持时,如果java.lang.invoke.MethodHandle实例解析后的结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法的句柄,并且这个方法句柄对应的类没有 进行初始化则需要先触发其初始化。
上述五种场景行为成为对一个类进行主动引用,除此之外的类的引用方式都不会触发初始化,称为被动引用:
被动引用例子:

public class SuperClass {
   static{
       System.out.println("super class init!");
   }
   public static int value=123;
}
-----------------------------------------------------
public class SubClass extends SuperClass{
   static{
       System.out.println("SubClass init!");
   }
}
-----------------------------------------------------
/*
 * 子类应用父类的静态字段不会初始化子类
 */
public class NotInitialization {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
      System.out.println(SubClass.value);

    }
}
结果:
super class init!
123

对于静态字段,只有直接定义这个字段的类才会被初始化。

/*
 * 使用new来创建实例如果该类型类没有初始化,则会先初始化,**如果其父类没有初始化先初始化父类**
 */
public class Initialization {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        SubClass subClass=new SubClass();   
    }
}
结果:
super class init!
SubClass init!
---------------------------------------------------------
/*
 *获取或设置静态字段也会触发初始化
 */
public class SuperClass {
   static{
       System.out.println("super class init!");
   }
   public static int value=123;
     public static void main(String[] args) {
     // value=1; //赋值静态字段
     System.out.println(value);//获取静态字段
}
}
结果:
super class init!
123

类的加载过程

1、加载
    加载是类加载过程的一个阶段,在此阶段虚拟机需要完成以下事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
相对于类加载过程其他阶段,一个非数组类的加载阶段(准确说是获取二进制字节流的动作)是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器或者用户自定义的类加载器完成。
数组类则是由JVM直接创建,但数组元素(去掉所有维度)类型最终都要靠类加载器去完成。
JVM类加载器分类:
1>引导类加载器(bootstrap class loader):用来加载java核心库(jre/lib/rt.jar),是原生C++实现,并没有继承java.lang.ClassLoader。
2>扩展类加载器(extensions class loader):用来加载java的扩展库(jre/ext/*.jar),JVM的实现会提供一个扩展库目录,该加载器在此目录里查找并加载Java类。
3>系统类加载器(system class loader):根据java的类路径(CLASSPATH)来加载java类。一般来说,java应用的类都是它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
4>自定义类加载器(custom class loader):
用户可以通过继承java.lang.ClassLoader类的方式来实现自己的类加载器,以满足一些特殊需求。
2、验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
大致上完成下面四个阶段的检验动作:
1>文件格式验证:
    是否符合Class文件的规范,并被当前虚拟机版本虚拟机处理。比如:检查魔数0xCAFEBABE、主版本 次版本号是否在虚拟机处理范围之内、常量池中是否有不支持的常量类型(检查常量tag标志)……
只有经过这个阶段的验证,字节流才会进入方法区进行存储,后面的三个阶段则都是基于方法区存储结构进行的,而不是字节流。
2>元数据(类的一些额外信息,类似注解是描述数据的数据)验证:
    对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范(java语法)要求:是否继承了不被允许继承的类(final修饰)、如果不是抽象类,是否实现了父类或接口中要求实现的所有方法……
3>字节码验证:
    是验证过程中最复杂的一个阶段,主要是通过数据流和控制流来分析确定程序语义是合法的、符合逻辑的。第二阶段主要是元数据信息中的数据类型做完效验后,这个阶段是对类的方法体进行校验分析,保证方法运行时不会出错。保证操作数栈的数据类型与指令码都能配合工作,比如操作一个int类型不能是别的类型、跳转指令不会跳转到方法体以外的字节码指令上。
4>符号引用验证:
    这个校验发生在虚拟机将符号引用转为直接引用阶段,这个转化动作将在连接的第三个阶段-解析阶段中发生(再次证明不是类的加载阶段并不是完全分离,即一个阶段完成,再执行另一个阶段):符号引用中通过字符串描述的全限定名是否找到对应的类,以及引用的类中信息是否可以被当前类访问……
验证阶段虽然很重要,但不一定是必要的(与运行期没有影响),如果运行的代码被反复使用和验证过,则可以考虑使用参数-Xverify:none;来关闭大部分类验证,缩短类加载时间。
3、准备
    准备阶段是正式为类变量分配内存并设置类变量初始值(零值或特殊情况下程序员赋值),都是在方法区中进行。这里分配内存的变量仅包括类变量(被static修饰),而不是实例变量,实例变量是在堆中。
比如public static int value=123;准备阶段初始值是0(putstatic存放在类型类初始化()方法中(区别实例化对象初始化())),但对于public static final int value=123;就是123;
这种特殊情况是类字段的字段属性表中存在ConstantValue属性(javac编译阶段生成),则准备阶段就会将value设置为123。
4、解析
    解析阶段是JVM将常量池中的符号引用替换为直接引用的过程。
符号引用:与虚拟机实现的内存布局无关,引用的目标并不一定加载到内存。符号可以是任何字面量,只要可以唯一定位到目标即可。
直接引用:(区别于直接指针:指向堆中的一个地址(对象的))可以是直接指向目标的指针、相对偏移量或间接定位目标的句柄。且引用的目标必定已经在内存中。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用电限定符7类符号引用进行。
对同一个符号进行解析 是非常常见的事情,除了invokedynamic指令外,其它指令虚拟机会对第一次解析结果进行缓存(在运行时常量池中记录直接引用,并把常量标记为已解析状态),从而避免重复解析。而invokedynamic每次都会在调用前解析,也是为了对于动态语言的支持。
4种类引用解析过程:
1>类或接口的解析:
在类D中解析符号引用N为一个类或接口C则需要:
如果C不是一个数组,则虚拟机会把代表N的全限定名传给D的类加载器去加载这个类C。过程中可能会触发其它比如父类或接口的加载,过程中出现失败,则解析过程失败。
如果C是一个数组类型,且元素为对象,N的描述符“[Ljava/lang/Integer”,对于Integer加载如上述步奏所说,D的类加载器去加载,但对于代表数组维度和元素的数组对象由虚拟机直接生成。
上述步骤没有异常,则C已经成为一个有效的类或接口了。但解析前还要进行符号验证,以确保D是否具备对C的访问权限。
2>字段解析:
要解析未被解析过的字段符号引用,首先会解析字段所述的类或接口的符号引用,然后对类或接口C进行搜索:
*如果C本身就包含了简单名称和字段描述符与目标匹配的字段,那么结束返回该字段的直接引用。
*否则如果C实现了接口,则会按照继承关系从下往上搜索,接口包含了简单名称和字段描述符与目标匹配的字段,则结束返回字段的直接引用
*否则 ,按照继承关系从下往上搜索继承关系,查找父类中包含目标字段,如果匹配则结束返回。
(验证访问权限)
*否则,抛异常。
实际应用中,虚拟机编译器更严格,一个字段不能同时出现在接口和父类中,或者自己和父类的接口中。
3>类方法解析
与字段解析一样,首先解析类方法表class_index项中索引的方法所属类的或接口的符号引用,用C表示这个类:
*如果在C是个接口,则抛出异常()
*否则,在类C中查找简单名称和描述符否与目标匹配的方法,如果有则返回这个方法的直接引用,
*否则,在类C中父类中递归查找是否简单名称和描述符与目标匹配的方法,如果偶遇则返回这个方法的直接引用。
*否则,否则在类C实现的接口列表以及他们的父接口中递归查找会否简单名称和描述符与目标匹配的方法,如果存在匹配的方法,则说明C是一个抽象类,结束,抛出异常。
*否则查找失败,抛出异常。
4>接口方法解析
接口方法解析也需要解析方法所属类或接口的符号引用,用C表示这个接口:
*如果在接口的方法表中发现C是一个类则抛出异常。
*否则,在接口C中查找 是否有简单名称和描述符与目标匹配的方法,如果有则返回方法的直接引用。
*否则,在C的父接口中递归查找,直至java.lang.object类为止,如果匹配则返回方法直接引用。
*否则,查找失败,抛出异常。
接口中方法默认是public 所以不存在访问权限问题,不会抛出非法访问异常。
5、初始化
类初始化是类加载过程的最后一个阶段,这个阶段才是开始执行类中定义的java程序代码。通过执行类构造器()方法的过程。
*()是编译器自动搜集所有类变量赋值动作和静态语句块中的语句合并产生。
*()方法与类的构造函数(实例构造器())不一样,它不需要显示(立即)调用父类构造器,因为jvm会保证子类()方法执行前,父类已经执行结束。第一个被执行的是java.lang.object。
*父类()方法先执行,则父类中定义的静态语句块优先于子类赋值操作。

Static class Parent{
   public static int A=1;
   static{
    A=2;
}
static class Sub extends Parent{
   public static int B=A;
}
public static void main(String[] args){
  System.out.println(Sub.B);
}
}
结果:2

由于调用子类静态变量,则需要初始化子类,则要先初始化父类所以是2
如果直接是sub.A则也是2 不过只初始化父类。
*()方法对于类或接口来说并不是必须的,如果类没有静态语句块,也没有变量赋值操作,那么编译器不会为这个类生成()方法。
*接口中虽然没有静态代码块,但有初始化变量操作,因此也会有()方法。与类不同,执行接口()方法,不需要先执行父接口的该方法,只有使用父类定义的变量时,父接口才会初始化,此外接口的实现类在初始化的时候,也一样不会执行接口的()方法。
*虚拟机会保证一个类的()方法在多线程可以正确加锁、同步:多个线程初始化一个类,只有一个线程会执行该方法,其他阻塞,且该线程执行结束后,其他线程不会再执行。

类加载器

类与类加载器
类加载器通过一个类的全限定名来获取描述此类的二进制字节流这个动作实在虚拟机外部实现的,以便让程序自己决定如何获取所需的类。
类的加载器并不只是实现类的加载动作,确定一个类在jvm的唯一性,通过类和它的类加载器共同确定,每一个类加载器都有一个独立的类名称空间。表示:即使是同一个Class文件,同一个虚拟机,如果使用两个类加载器去加载,则这两个类必不相等。

public class ClassLoaderTest {

    public static void main(String[] args) throws InstantiationException, Exception {
        // TODO Auto-generated method stub
       ClassLoader myLoader=new ClassLoader(){

        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            // TODO Auto-generated method stub
            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) {
                // TODO Auto-generated catch block
                throw new ClassNotFoundException(name);
            }

        }

       };
       Object obj=myLoader.loadClass("com.jvm.classfile.ClassLoaderTest").newInstance();//全限定名不包含文件后缀
       System.out.println(obj.getClass());
       System.out.println(obj instanceof com.jvm.classfile.ClassLoaderTest);
    }

}
结果:
class com.jvm.classfile.ClassLoaderTest
false

双亲委派模型
从虚拟机的角度来讲,只存在两种不同的类加载器:
启动类加载器(Bootstrap ClassLoader),这个加载器使用C++语言实现,是虚拟机自身的一部分。
所有其他的类加载器:这些类加载器全部由java实现,并且继承抽象类java.lang.ClassLoader,独立于虚拟机外部。
从java开发人员的角度来看,类加载器还可以划分的更细致一些,绝大部分java程序会使用以下3种系统提供的类加载器:
启动类加载器(bootstrap ClassLoader):将存放在\lib目录中,或者被-Xbootclasspath参数指定的路径中类库加载到虚拟机内存中。启动类加载器无法被java程序直接引用,如果用户自己编写的类加载器需要把加载请求委派给引导类加载器,直接使用NULL即可。
扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录中或者系统变量指定的,开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader):负责加载用户路径(ClassPath)路径上所指定的类加载器,开发者可以直接使用这个类加载器,如果用户没有自己编写类加载器,则系统默认使用这个类加载器。
这里写图片描述
上图展示的类加载器之间的层次关系就称为双亲委派模型,除了顶层的启动类以外,其他类加载器都应该自己的父类加载器。类加载器之间的关系一般不会以继承关系实现,而是使用组合关系来复用父类加载器代码。
工作过程:如果一个类加载器收到类加载的请求,它首先不会主动去加载该类,而是把这个请求委派给父类去完成,父类也如此,直至到顶层启动类加载器来完成。只有父类加载器反馈无法完成这个类加载请求(在自己搜索的范围内没有找到这个类),子类才会去加载。
使用双亲委派模型来组织类加载器之间的关系,有一个好处是java类随着它的类加载器有了优先级关系。比如Object类存在rt.jar中,首先加载,否则用于自己编写一个object类放在classpath中,那么系统将会出现多个不同object类。所以这种模式也保证了java程序的稳定运作。
在ClassLoader中的loadClass()方法中,先检查该类是否已经被加载,如果没有,则判断父类加载器是否为空,如果为空则使用启动类加载器作为父类加载器,否则执行父类loadClass()方法。如果父类加载失败则抛出异常后,调用自己的findClass()方法来加载。

猜你喜欢

转载自blog.csdn.net/qq_26564827/article/details/80298010