任何程序都需要加载到内存才能与CPU进行交流,同理, 字节码.class文件同样需要加载到内存中,才可以实例化类。
ClassLoader
的使命就是提前加载.class 类文件到内存中,在加载类时,使用的是Parents Delegation Model(溯源委派加载模型)。
Java的类加载器是一个运行时核心基础设施模块,主要是在启动之初进行类的加载、链接、初始化:
由类加载器执行。
读取类文件(通常在 classpath 所指定的路径中查找,但classpath非必须),查找字节码,从而产生二进制流,并转为特定数据结构,初步校验cafe babe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的java.lang.Class实例。
将已读入内存的类的二进制数据合并到 JVM 运行时环境。
包括验证、准备、解析三步:
执行类构造器 方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值
类加载是一个将.class字节码文件实例化成Class对象并进行相关初始化的过程。
在这个过程中,JVM会初始化继承树上还没有被初始化过的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。
某些类在使用时,也可以按需由类加载器进行加载。
全小写的class是关键字,用来定义类
而首字母大写的Class,它是所有class的类
这句话理解起来有难度,类已经是现实世界中某种事物的抽象,为什么这个抽象还是另外一个类Class的对象?
示例代码如下:
● 第1处说明:
Class类下的newInstance()
在JDK9中已经置为过时,使用getDeclaredConstructor().newInstance()
的方式
着重说明一下new与newInstance的区别
InstantiationException
异常;IllegalAccessException
异常Java 通过类加载器把类的实现与类的定义进行解耦,所以是实现面向接口编程、依赖倒置的必然选择。
● 第2处说明:
可以使用类似的方式获取其他声明,如注解、方法等
● 第3处说明: private 成员在类外是否可以修改?
通过setccessible(true)
,即可使用Class类的set方法修改其值
如果没有这一步,则抛出如下异常:
“加载”是“类加载”(Class Loading)过程的第一步。
JVM主要做如下事情:
java.lang.Class
对象,作为方法区该类的各种数据的访问入口,所以所有类都可以调用 getClass 方法程序在运行中所有对该类的访问都通过这个类对象,也就是这个Class对象是提供给外界访问该类的接口
JVM规范对于加载过程给予了较大的宽松度。一般二进制字节流都从已经编译好的本地class文件中读取,此外还可以从以下地方读取
PRoxyGenerator.generateProxyClass
为特定接口生成形式为"*$Proxy"的代理类的二进制字节流数组也有类型,称为“数组类型”,如:
String[] str = new String[10];
这个数组的数组类型是Ljava.lang.String
,而String只是这个数组的元素类型。
当程序在运行过程中遇到new关键字创建一个数组时,由JVM直接创建数组类,再由类加载器创建数组中的元素类型。
而普通类的加载由类加载器创建。既可以使用系统提供的引导类加载器,也可以由用户自定义的类加载器完成(即重写一个类加载器的loadClass()方法)
加载 -> 链接 -> 初始化
但结束顺序无所谓,因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉
验证阶段比较耗时,它非常重要但不一定必要(因为对程序运行期没有影响),如果所运行的代码已经被反复使用和验证过,那么可以使用-Xverify:none
参数关闭,以缩短类加载时间
保证二进制字节流中的信息符合虚拟机规范,并没有安全问题
虽然Java语言是一门安全的语言,它能确保程序猿无法访问数组边界以外的内存、避免让一个对象转换成任意类型、避免跳转到不存在的代码行.也就是说,Java语言的安全性是通过编译器来保证的.
但是我们知道,编译器和虚拟机是两个独立的东西,虚拟机只认二进制字节流,它不会管所获得的二进制字节流是哪来的,当然,如果是编译器给它的,那么就相对安全,但如果是从其它途径获得的,那么无法确保该二进制字节流是安全的。
通过上文可知,虚拟机规范中没有限制二进制字节流的来源,在字节码层面上,上述Java代码无法做到的都是可以实现的,至少语义上是可以表达出来的,为了防止字节流中有安全问题,需要验证!
通过本阶段验证,才被允许存到方法区
后面的三个验证阶段都是基于方法区的存储结构进行,不会再直接操作字节流.通过上文可知,加载开始前,二进制字节流还没进方法区,而加载完成后,二进制字节流已经存入方法区 而在文件格式验证前,二进制字节流尚未进入方法区,文件格式验证通过之后才进入方法区 也就是说,加载开始后,立即启动了文件格式验证,本阶段验证通过后,二进制字节流被转换成特定数据结构存储至方法区中,继而开始下阶段的验证和创建Class对象等操作 这个过程印证了:加载和验证是交叉进行的
完成两件事情
public static final int value = 123;
准备阶段后 a 的值为 0,而不是 123,要在初始化之后才变为 123,但若被final修饰的常量如果有初始值,那么在编译阶段就会将初始值存入constantValue属性中,在准备阶段就将constantValue的值赋给该字段(此处将value赋为123).
把常量池中的符号引用转换成直接引用的过程。包括:
主要针对:类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符。
真正开始执行类中定义的Java程序代码(或说是字节码),类的初始化就是为类的静态变量赋初始值,初始化阶段就是执行类构造器的过程。
clinit()方法由编译器自动产生,收集类中static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句。
在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化。
Java程序对类的使用方式分为:
JVM必须在每个类或接口“首次主动使用”时才初始化它们,被动使用类不会导致类的初始化。主动使用的场景:
public class Test {
static {
i=0;
System.out.println(i); //编译失败:"非法向前引用"
}
static int i = 1;
}
其他线程虽会被阻塞,只要有一个clinit()方法执行完,其它线程唤醒后不会再进入clinit()方法。同一个类加载器下,一个类型只会初始化一次。
当代表一个类的Class对象不再被引用,那么Class对象的生命周期就结束了,对应的在方法区中的数据也会被卸载。
Jvm自带的类加载器装载的类,是不会卸载的,由用户自定义的类加载器加载的类是可以卸载的。
参考