JVM篇--虚拟机类加载

177 阅读15分钟

真正的大师,永远怀着一颗学徒的心

--无极剑圣·易

开篇词

这是我的JVM第一篇,为了记录自己学习的点滴,将从此刻起,写一些学习笔记记录学习的点滴,希望大家共勉。 我们都知道,在java的世界中,任何事物都可以用对象来表示,所谓万物皆对象,本篇就先从一个对象的朝生夕死慢慢讲起,如果有什么不对的地方,还请大家指出来,谢谢。

对象的创建

我们都说,万物皆对象,那么对象是如何创建。因为所有对象都是依赖于类的,所以就不得不说下它的类加载器,因为一个对象的创建中,我怎么知道这个对象是否合法,是否是我想要的这个对象,总不能本来想要对象A,结果创建出来是对象B 或者是不合法的吧,那就先从类加载器说起。

虚拟机类加载机制

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称 为连接(Linking)。

加载-> 验证->准备->解析->初始化->使用->卸载

类的加载(时机)

首先,一个类他是在什么时候进行加载

  1. 遇到new,getstatic,putstatic,invokestatic这四条指令字节码的时候,如果类型没有进行初始化,则需要先触发其初始化阶段。
  • 使用new关键字实例化对象的时候

  • 读取或者设置一个静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候

  • 调用一个类型的静态方法的时候

  1. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化的话,则需要进行初始化

  2. 当初始化类的时候,如果父类没有进行初始化,则需要触发其父类的初始化。

  3. 当虚拟机启动的时候,用户需要指定一个运行的主类,这个类会先被虚拟机初始化

  4. 当JDK7新加入的动态语言支持的时候,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化

  5. 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有 这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

被动引用

在java虚拟机规范中,有且仅有这六种情况会进行初始化,除此之外,所有的引用类型都不会进行初始化,我们称之为被动引用。

那么怎样的才算是被动引用呢?

案例1.

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

public class SubClass extends SuperClass{
	static{
    	System.out.println("SubClass init!");
    }
}

public class Test{
	public static void main(String[] args){
    	System.out.println(SubClass.value);
    }
}

上述代码运行的时候,会先后输出SuperClass init! 123 不会输出SubClass init! 对于静态字段,只有直接定义这个静态字段的类才会被初始化,因此,通过其子类调用父类的静态变量,只会触发父类进行初始化,不会触发子类的初始化。(但是会触发子类的加载,验证,准备,解析)

案例2.

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

这个案例也没有输出SuperClass init! 这句话,即代表没有进行初始化操作。但是这里触发了Lorg.XXXXXX.SuperClass的类的初始化,对于用户来说这个类明显不是合法的类,因为对用户来说根本就没这个包名。它是由虚拟机自动生成的、直接继承于Object这个类,创建动作由字节码执行newarray触发。

这个类代表了元素类型为SuperClass的一堆数组,数组中应有的属性跟方法都在这个类中。

案例3.

/*** 被动使用类字段演示三:
* 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,
* 因此不会触发定义常量的 类的初始化
**/
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);
   }
}

上述代码运行之后,也不会有“ConstClass init!”输出,这是因为虽然ConstClass虽然引用了HELLOWORLD这个静态成员变量,但是在编译阶段通过常量传播优化,已经将常量值hello world直接存储到NotInitialization类的常量池中了,以后对这个类的常量的引用,直接都转化为对NotInitialization类自身常量的引用。

类的加载(过程)

加载时机说完了,接下来我们就要讲讲类加载的全过程,也就是加载,验证,准备,解析,初始化这五个阶段。

加载

在加载阶段,虚拟机完成了三件事情:

  1. 通过一个类的全限定名来获取此类的二进制字节流(可以从很多地方进行获取二进制字节流)
    • 从zip压缩包中读取,最常见的就是jar
    • 网络中获取,最典型的就是Web Applet
    • 运行时生成动态的类,最常见的就是动态代理生成代理类
    • 其他文件生成,等等
  2. 将这个类所代表的静态存储结构转换成方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang Class对象,作为方法区的这个类的各个数据的访问入口。

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

验证

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚 拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

java是相对安全的编程语言,使用纯粹的java代码的话,无法做到诸如访问数组边界以外的数据、将一个对象转型为它并未实现的类型、跳转到不存在的代码 行之类的事情,如果尝试这样去做了,编译器会毫不留情地抛出异常、拒绝编译。

  1. 文件格式验证(后续会有一张篇幅会详细讲解的)
  • 是否以魔数0xCAFEBABE开头。
  • 主次版本号是否在当前java虚拟机接受范围内
  • 常量池的常量中是否有不被支持的常量类型(检查常量的Tag标志)
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。 .....
  1. 元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要 求,这个阶段可能包括的验证点如下:

  • 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
  • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。 ...
  1. 字节码验证

第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定 程序语义是合法的、符合逻辑的。 这阶段就要 对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害 虚拟机安全的行为,例如:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作 栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况。
  • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
  1. 符号引用验证

最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候

准备

这个阶段其实很好理解,咱们都知道,我们写好的那些类,其实都有一些类变量

比如下面的这个“ReplicaManager”类:

	public class Test1{
    	public static int val;
    }

这个准备工作,其实就是给这个“Test1”类分配一定的内存空间 然后给他里面的类变量(也就是static修饰的变量)分配内存空间,来一个默认的初始值 比如上面的示例里,就会给“val”这个类变量分配内容空间,给一个“0”这个初始值。

解析(这个阶段很复杂,不仅仅是下面的这个解析的过程,还包括了类或者接口的解析,字段的解析,方法的解析等等)

这个阶段干的事儿,实际上是把符号引用替换为直接引用的过程

什么是符号引用?

符号引用其实就是Class文件中以CONSTANT_Class_info、CONSTANT_Fxxxx 等类型出现的常量
其实就是用一组符号来描述所引用的目标,他可以是任何形式的字面量,只要使用的时候无歧义定位到目标即可。
当然,他跟 内存布局是没有关系的

什么是直接引用?

直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄,直接引用和虚拟机内存实现有关,

初始化(核心)

之前说过,在准备阶段时,就会把我们的“Test1”类给分配好内存空间 另外他的一个类变量“val”也会给一个默认的初始值“0”,那么接下来,在初始化阶段,就会正式执行我们的类初始化的 代码了。

public class Test{
	public static int val = 10;
}

如上述代码,可以看到val的值是10,但是这个10赋值的时候是在初始化阶段进行赋值的,前面在准备阶段仅仅是开辟了一个内存空间,然后赋值默认的值为0。

什么时候会初始化一个类?(区分一下跟类的加载)

一般来说,在需要实例化一个类的对象的时候,其最后一步都要执行到初始化这个阶段,像前面那几个被动引用仅仅是到了类的加载验证准备解析这几个阶段。

类加载器

现在相信大家都搞明白了整个类加载从触发时机到初始化的过程了,接着给大家说一下类加载器的概念

因为实现上述过程,那必须是依靠类加载器来实现的

什么是类加载器?

Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节 流”这个动作放到Java虚拟机外部去实现,

以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。

类加载器虽然只用于实现类的加载动作,但是在java程序中所起的作用远超于类加载阶段、对于任何一个类,都是由他的全限定名以及他的类加载器来确定这个类的唯一性。

所以说,如果同一个class文件,被不通过的类加载器加载了,那么他们进行比较的时候,也是不相等的。 这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance() 方法的返回结果,也包括了使用instanceof关键字做对象所属关系判定等各种情况

那么Java里有哪些类加载器呢?简单来说有下面几种:

启动类加载器

Bootstrap ClassLoader,他是所有类加载器的最顶端,他主要是负责加载我们在机器上安装的Java目录下的核心类的,也就是用来加载jdk包下的lib库里面的java文件,所以一旦你的JVM启动,那么首先就会依托启动类加载器,去加载你的Java安装目录下的“lib”目录中的核心类库。而且是Java虚拟机能够 识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类 库加载到虚拟机的内存中。

启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时, 如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可。

扩展类加载器

Extension ClassLoader,这个类加载器其实也是类似的,就是你的Java安装目录下,有一个“lib\ext”目录这里面有一些类,就是需要使用这个类加载器来加载的,支撑你的系统的运行。那么你的JVM一旦启动,是不是也得从Java安装目录下,加载这个“lib\ext”目录中的类。

应用程序类加载器

Application ClassLoader,这类加载器就负责去加载“ClassPath”环境变量所指定的路径中的类,其实你大致就理解为去加载你写好的Java代码吧,这个类加载器就负责加载你写好的那些类到内存里。

自定义类加载器

除了上面那几种之外,还可以自定义类加载器,去根据你自己的需求加载你的类。

双亲委派机制

什么是双亲委派机制

如果一个类加载器收到一个类需要被加载请求的时候,他首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此,所有的加载请求都会达到最顶端的启动类加载器,只有当父类加载器反馈自己无法完成当前的这个类加载请求(也就是在自己搜索范围内找不到这个类),子加载器才会自己去完成。

(注:这里说的父类加载器和子类加载器并不是我们java说的父类子类的继承关系,而是对加载器来说的也就是说 启动类加载器是扩展类加载器的父级关系)

双亲委派机制好处

Java中所有的类加载的过程都具备了一种带有优先级的层次关系,例如,类java.lang.Object,它存放在rt.jar中,无论哪个类加载器需要加载这个类,最终都会由启动类加载器去加载,因此Object类在程序中各个类加载环境中都属于同一个类。反之, 如果没有双亲委派机制,把他放到classpath环境下的时候, 就会出现多个Object类。

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;
        }
    }

这段代码的逻辑清晰易懂:

先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的 loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败, 抛出ClassNotFoundException异常的话,才调用自己的findClass()方法尝试进行加载。

结束语

本章虚拟机类加载就先到这里了,后续要分享一下JVM虚拟机中重要的内存区域是怎么划分的,对象是怎么创建的,以及垃圾回收机制,共勉~

真正的大师,永远怀着一颗学徒的心

--《英雄联盟》无极剑圣·易