java虚拟机类加载机制(笔记)

类加载机制:虚拟机把描叙类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。

类加载的时机

类加载的生命周期包含:加载、验证、准备、解析、初始化、使用、卸载。其中,验证、准备、解析3个部分称为链接。
java虚拟机类加载机制(笔记)_第1张图片
虚拟机对于类的初始化阶段严格规定了有且仅有只有5种情况如果对类没有进行过初始化,则必须对类进行“初始化”!

  • 遇到new(实例化对象)、getstatic、putstatic(读取和设置类的静态变量,被final修饰,以在编译期把结果放到常量池的静态字段除外)或者invokestatic(调用一个类的静态方法)这4条指令是,如果类没有初始化,则需要先触发其初始化。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果没有初始化,需要初始化。
  • 当初始化一个类,如果父类没有初始化,需要先触发父类的初始化。
  • 虚拟机启动的时候,用户需要指定一个执行主类(main方法哪个类),虚拟机先初始化这个主类。
  • 如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化

除此之外,所有的引用类的方法都不会触发初始化,称为被动引用。被动引用的例子如下:

  • 子类引用父类的静态字段,不会导致子类的初始化
public class SuperClass {


    static {
        System.out.println(" supser class init");
    }
    public static int value = 123;
}
public class SubClass extends SuperClass {

    static {
        System.out.println(" subclass init ");
    }
}


public class NoInitialization1 {

    public static void main(String[] args) {

        System.out.println(SubClass.value);
    }
}

输出结果:

supser class init
123

  • 通过数组定义来引用类,不会触发此类的初始化
public class NoInitialization3 {

    public static void main(String[] args) {

        SuperClass[] superClasses = new SuperClass[3];
    }
}

没有任何输出
这段代码触发了一个[LSuperClass]的类的初始化阶段,是由虚拟机自动生产的、直接继承java.lang.object的子类,创建动作由字节码指令newarray触发。

  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
public class ConstClass {

    static {
        System.out.println("  Const class init !");
    }

    public static final String HELLOWORLD = "hello world";
}
public class NoInitialization2 {

    public static void main(String[] args) {

        System.out.println(ConstClass.HELLOWORLD);
    }
}

接口的加载过程和类不同,只有在:当一个类初始化的时候,要求其父类全部都已经初始化过了,但是一个接口在初始化时候,并不要求其父接口都完成初始化,只有真正使用父接口(如:引用接口定义的常量)的时候才会初始化。

类加载的过程

类的加载过程也就是加载、验证、准备、解析以及初始化。

加载

注意“加载”和“类加载”的区别。
在加载的阶段,虚拟机只需要完成以下三件事:

  1. 通过类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行数据结构
  3. 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的接口。
    加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机自行定义,虚拟机规范没有规定ci区域的具体数据的格式。
    加载阶段和链接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段没有开始,链接阶段可能已经开始,但这些夹在加载阶段中的动作,仍属于链接阶段的内容,这两个阶段的开始仍然保持着固定的先后次序。
验证

验证是链接的第一步,这一阶段的目的是为了保护class文件的字节流的信息符合虚拟机的要求,并且不会危害虚拟机自身的安全。验证如果检查不符合class文件的格式约束,虚拟机就应抛出一个java.lang.VerifyError异常或者子异常。验证大概分为4个验证动作:文件格式验证、元数据验证、字节码验证、符号引用验证

文件格式验证

验证字节流是否符合class文件格式规范,并且能被当前版本的虚拟机处理。这一阶段可能验证:

  1. 是否以魔数0xCAFFBABE开头
  2. 主、次版本号是否在虚拟机处理范围之内
  3. 常量池的常量中是否有不被支持的常量类型(检查tag)
  4. 指向常量的各种索引值是否有指向不存在的常量或者不符合类型的常量
  5. CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
  6. Class文件各个部分及文件本身是否有被删去的或者附件的其他信息

    这阶段的验证是基于二进制字节流进行的,只有通过这个阶段的验证,字节流才会进入内存中的方法区中进行存储,所以后面的阶段全部是基于方法区的存储进行的,不能直接操作字节流。
元数据验证

对字节码描叙的信息进行语义分析,保证其描叙信息符合java语言规范的要求。这些阶段可能包含验证点如下:

  1. 这个类是否有父类
  2. 这个类的父类是否不允许被继承
  3. 如果这个类不是抽象类,是否实现了其父类或者接口之中要求实现的方法
  4. 类中的字段、方法是否与父类产生矛盾
字节码验证

这一阶段主要目的是通过数据流和控制流分析,确定程序的语义是否合法,是否符合逻辑。这阶段分析对类的方法体进行校验分析,保证被校验类的方法体在运行中不会作出危害虚拟机安全的事件:

  1. 保证任意时刻操作数栈与指令代码序列都能够配合工作
  2. 保证跳转指令不会跳到方法体意外的字节码指令上
  3. 保证方法体体中的类型转换是有效的

在jdk1.6之后javac编译器和java虚拟机,给方法体Code属性的属性表中增加了一项名为“StackMapTable”的属性,这描叙了方法体中所有基本块开始本地变量表和操作栈应有的状态,检查StackMapTable属性中的记录是否合法即可。

符合引用验证

最后一阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,发生在解析阶段。符合引用验证可以看做是对类自身意外的信息进行匹配性校验,需要校验以下内容:
1. 符合引用中通过字符串描述的全限定名是否能够找到对应的类
2. 在指定类中是否存在符合方法的描述符以及简单名称所描述的方法和字段
3. 符合引用中的类、字段、方法的访问属性(private、protected、public、default)是否可被当前类访问
……

准备

准备阶段是正式为类变量(static修饰的)分配内存并设置类变量初始化值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里飞蛾复制通常是数据类型的零值。

  1. 类变量value,public static int value = 123;变量在准备阶段过后的初始值为0而不是123,把value设置成123的putstatic指令,存放在()方法中,把value赋值123,在初始化阶段才执行。
  2. public static final int value = 123;如果类字段的字段属性(ConstantValue)属性,那么准备阶段的值为属性的值。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
#####符号引用和直接引用
符号引用:符号引用是一组符合来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用的字面量形式明确定义在java虚拟机规范的class文件格式中
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
解析动作主要主要针对类或者接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7种引用进行,分别对应CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info7种类型。

类或接口解析
  1. 如果该符号引用不是一个数组类型,那么虚拟机将会把该符号代表的全限定名称传递给调用这个符号引用的类。这个过程由于涉及验证过程所以可能会触发其他相关类的加载
  2. 如果该符号引用是一个数组类型,并且该数组的元素类型是对象。我们知道符号引用是存在方法区的常量池中的,该符号引用的描述符会类似”[java/lang/Integer”的形式,将会按照上面的规则进行加载,虚拟机将会生成一个代表此数组对象的直接引用如果该符号引用是一个数组类型,并且该数组的元素类型是对象。我们知道符号引用是存在方法区的常量池中的,该符号引用的描述符会类似”[java/lang/Integer”的形式,将会按照上面的规则进行加载,虚拟机将会生成一个代表此数组对象的直接引用
  3. 如果上面的步骤都没有出现异常,那么该符号引用已经在虚拟机中产生了一个直接引用,但是在解析完成之前需要对符号引用进行验证,主要是确认当前调用这个符号引用的类是否具有访问权限,如果没有访问权限将抛出java.lang.IllegalAccess异常如果上面的步骤都没有出现异常,那么该符号引用已经在虚拟机中产生了一个直接引用,但是在解析完成之前需要对符号引用进行验证,主要是确认当前调用这个符号引用的类是否具有访问权限,如果没有访问权限将抛出java.lang.IllegalAccess异常
字段解析

对字段的解析需要首先对其所属的类进行解析,因为字段是属于类的,只有在正确解析得到其类的正确的直接引用才能继续对字段的解析。对字段的解析主要包括以下几个步骤:

  1. 如果该字段符号引用(后面简称符号)就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,解析结束
  2. 否则,如果在该符号的类实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果在接口中包含了简单名称和字段描述符都与目标相匹配的字段,那么久直接返回这个字段的直接引用,解析结束否则,如果在该符号的类实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果在接口中包含了简单名称和字段描述符都与目标相匹配的字段,那么久直接返回这个字段的直接引用,解析结束
  3. 否则,如果该符号所在的类不是Object类的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都相匹配的字段,那么直接返回这个字段的直接引用,解析结束否则,如果该符号所在的类不是Object类的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都相匹配的字段,那么直接返回这个字段的直接引用,解析结束
  4. 否则,解析失败,抛出java.lang.NoSuchFieldError异常否则,解析失败,抛出java.lang.NoSuchFieldError异常
    如果最终返回了这个字段的直接引用,就进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常
    经过这些步骤后,对一个字段的解析就到此为止,这里有一个经典的问题:就是父类与子类构造方法调用的问题,比如下面这段代码:
public class Test{
    public static void main(String[] args){
        new C();
    }
}

class A{
    public A(String name){
        System.out.println(name + " A constructor");
    }
}

class B{
    private A = new A("b");
    public B(){
        System.out.println("B constructor");
    }
}

class C extends B{
    private A = new A("c");
    public C(){
        System.out.println("C constructor");
    }
}

输出:

b A constructor
B constructor
c A constructor
C constructor

通过这个例子以及字段解析的过程,我们可以更深刻理解为什么在具有继承关系的类中,为什么总是先加载父类的构造方法以及初始化,然后才调用子类的构造方法以及初始化。

类方法的解析

进行类方法的解析仍然需要先解析此类方法的类,在正确解析之后需要进行如下的步骤:

  1. 类方法和接口方法的符号引用是分开的,所以如果在类方法表中发现class_index(类中方法的符号引用)的索引是一个接口,那么会抛出java.lang.IncompatibleClassChangeError的异常
  2. 如果class_index的索引确实是一个类,那么在该类中查找是否有简单名称和描述符都与目标字段相匹配的方法,如果有的话就返回这个方法的直接引用,查找结束如果class_index的索引确实是一个类,那么在该类中查找是否有简单名称和描述符都与目标字段相匹配的方法,如果有的话就返回这个方法的直接引用,查找结束
  3. 否则,在该类的父类中递归查找是否具有简单名称和描述符都与目标字段相匹配的字段,如果有,则直接返回这个字段的直接引用,查找结束否则,在该类的父类中递归查找是否具有简单名称和描述符都与目标字段相匹配的字段,如果有,则直接返回这个字段的直接引用,查找结束
  4. 否则,在这个类的接口以及它的父接口中递归查找,如果找到的话就说明这个方法是一个抽象类,查找结束,返回java.lang.AbstractMethodError异常(因为抽象类是没有实现的)否则,在这个类的接口以及它的父接口中递归查找,如果找到的话就说明这个方法是一个抽象类,查找结束,返回java.lang.AbstractMethodError异常(因为抽象类是没有实现的)
  5. 否则,查找失败,抛出java.lang.NoSuchMethodError异常否则,查找失败,抛出java.lang.NoSuchMethodError异常
    如果最终返回了直接引用,还需要对该符号引用进行权限验证,如果没有访问权限,就抛出java.lang.IllegalAccessError异常
接口方法的解析

同类方法解析一样,也需要先解析出该方法的类或者接口的符号引用,如果解析成功,就进行下面的解析工作:

  1. 如果在接口方法表中发现class_index的索引是一个类而不是一个接口,那么也会抛出java.lang.IncompatibleClassChangeError的异常
  2. 否则,在该接口方法的所属的接口中查找是否具有简单名称和描述符都与目标字段相匹配的方法,如果有的话就直接返回这个方法的直接引用。查找结束否则,在该接口方法的所属的接口中查找是否具有简单名称和描述符都与目标字段相匹配的方法,如果有的话就直接返回这个方法的直接引用。查找结束
  3. 否则,在该接口以及其父接口中查找,直到Object类,如果找到则直接返回这个方法的直接引用否则,在该接口以及其父接口中查找,直到Object类,如果找到则直接返回这个方法的直接引用
  4. 否则,查找失败否则,查找失败

初始化

到了初始化阶段,虚拟机才开始真正执行Java程序代码,前文讲到对类变量的初始化,但那是仅仅赋初值,用户自定义的值还没有赋给该变量。只有到了初始化阶段,才开始真正执行这个自定义的过程,所以也可以说初始化阶段是执行类构造器方法的过程。那么这个 方法是这么生成的呢?

() 是编译器自动收集类中所有类变量的赋值动作和静态语句块合并生成的。编译器收集的顺序是由语句在源文件中出现的顺序决定的
() 方法与类的构造器方法不同,因为前者不需要显式调用父类构造器,因为虚拟机会保证在子类的() 方法执行之前,父类的 方法已经执行完毕
由于父类的 方法会先执行,所以就表示父类的static方法会先于子类的 方法执行。这点也可以通过下面的代码得到体现:

static class A{
    public static int a = 1;
    static{
        a = 2;
    }
}

static class B extends A{
    public static int b = a;
}

public static void main(String[] args){
    System.out.println(B.b);
}

得到的结果是2而不是1,这就验证了父类的静态方法会先于子类的static方法执行。

类加载器

类与类加载器

类加载器虽然用于实现类的加载,在java程序中起到的作用却不止类的加载阶段。对于任意一个类,都需要由加载器和这个类一同确立其在java虚拟机的唯一性,没有类加载器都有唯一的空间。

package com.own.learn.jdk.cls1.classLoading;

import java.io.InputStream;

public class ClassLoadTest {


    public static void main(String[] args) throws Exception {

        final ClassLoader classLoader = new ClassLoader() {
            @Override
            public Class loadClass(String name) throws ClassNotFoundException {
                try {

                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";

                    final InputStream is = this.getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }

                    final byte[] bytes = new byte[is.available()];
                    is.read(bytes);
                    return defineClass(name, bytes, 0, bytes.length);
                } catch (Exception e) {

                }
                return super.loadClass(name);
            }
        };

        final Object o = classLoader.loadClass("com.own.learn.jdk.cls1.classLoading.ClassLoadTest").newInstance();
        System.out.println(o.getClass());

        System.out.println(o instanceof com.own.learn.jdk.cls1.classLoading.ClassLoadTest);
    }
}

输出:

class com.own.learn.jdk.cls1.classLoading.ClassLoadTest
false

双亲委派模型

从java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),由C++语言实现的,是虚拟机的一部分。另一种是所有其他类加载器,这些类加载器都由java语言的实现按,独立虚拟机外部,并且全部继承抽象类java.lang.classloader。类加载器分为3种:

  1. 启动类加载器(Bootstrap classloader):负责加载存放在\lib目录中,或者被-Xbootclasspath参数所指定的路径中,并按照虚拟机所识别的类库加载到虚拟机内存中。
  2. 扩展类加载器(Extension classloader):这个类加载器由sun.misc.launcher$ExtClassloader实现按,负责\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  3. 应用程序类加载器(Application classloader):这个类加载器由sun.misc.launcher$AppClassLoader实现的。这个类加载器是classloader中的getSystemClassLoader()方法的返回值,也称为系统加载器,负责加载用户路径(classpath)上所指定的类库。

java虚拟机类加载机制(笔记)_第2张图片
类夹杂其之间的这种层次关系,称为类加载器的双亲委派模型。
双亲委派模型工作过程:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个类加载器都是如此,因此所有的类记载器请求最终都应该传送到顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
好处

  1. Java类伴随其类加载器具备了带有优先级的层次关系,确保了在各种加载环境的加载顺序。 比如我们要加载顶层的Java类——java.lang.Object类,无论我们用哪个类加载器去加载Object类,这个加载请求最终都会委托给Bootstrap ClassLoader,这样就保证了所有加载器加载的Object类都是同一个类。如果没有双亲委派模型,那就乱了套了,完全可以搞出Root::Object和L1::Object这样两个不同的Object类。
  2. 双亲委派模型对于java虚拟机的稳定很重要。先检查类是否已经被架子啊过,如果没有加载过则调用父类加载器的loadclass()方法如果父亲类加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载器加载失败,抛出classNotFoundException异常后,再调用自己的findclass()方法进行加载。(每一个类都只会被加载一次,避免了重复加载
 protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

Java自定义类加载器与双亲委派模型

你可能感兴趣的:(jvm,类加载器)