jvm将class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。这个过程就是jvm的类加载机制。
从被加载到虚拟机内存开始到卸载出内存,共经历如下7个阶段:
上图的加载、验证、准备、初始化和卸载的顺序固定,但解析可能在初始化前也可能在初始化后进行。
加载阶段由虚拟机的具体实现自由把握
加载、验证、准备自然在初始化之前已经完成。
对一个类型的引用分为:主动引用和被动引用
以下8个情况是对一个类型的主动引用,一定会触发初始化(也只有该8种情况会触发初始化),而被动引用不会触发初始化。
java.lang.invoke.MethodHadnle
实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发器初始化。下面的被动引用情景则不会进行初始化:
通过子类引用父类的静态字段,子类不会初始化
public class _01PassiveReference {
public static void main(String[] args) {
//1.直接引用父类的静态字段,只会初始化父类
System.out.println(Son.value);
}
}
class Parent{
static {
System.out.println("parent!");
}
public static int value=3;
}
class Son extends Parent{
static {
System.out.println("son");
}
}
结果:
parent!
3
只初始化了父类
通过数组定义引用该类,该类不会进行初始化
public static void main(String[] args) {
Parent[]parents=new Parent[1];
}
调用一个类的常量不会对该类进行初始化
常量在编译阶段会存入调用类的常量池,未直接引用到该常量所在类
public class _01PassiveReference {
public static void main(String[] args) {
System.out.println(ConstantClass.VALUE);
}
}
class ConstantClass{
static {
System.out.println("constant");
}
public static final int VALUE=1;
}
初始化一个类时,不会初始化其实现的接口
public class _02InterfaceInit {
public static void main(String[] args) {
System.out.println( TestClass.value);
}
}
interface Test{
Thread thred=new Thread(){{
System.out.println("test1");
}};
}
class TestClass implements Test{
public static int value=1;
static {
System.out.println("testClass");
}
}
结果:
testClass
1
未初始化接口
初始化一个接口时,不需要初始化其父接口
public class _02InterfaceInit {
public static void main(String[] args) {
System.out.println(Test2.thred);
}
}
interface Test{
Thread thred=new Thread(){{
System.out.println("test1");
}};
}
interface Test2 extends Test{
Thread thred=new Thread(){{
System.out.println("test2");
}};
}
结果:
test2
Thread[Thread-0,5,main]
只有真正引用到父接口(如父接口中定义的常量)时,才会触发父接口的初始化
public class _02InterfaceInit {
public static void main(String[] args) {
System.out.println(Test2.thred);
}
}
interface Test{
Thread thred1=new Thread(){{
System.out.println("test1");
}};
}
interface Test2 extends Test{
Thread thred=new Thread(){{
System.out.println("test2");
System.out.println(thred1);
}};
}
结果:
test2
test1
Thread[Thread-1,5,main]
Thread[Thread-0,5,main]
过程:
java.lang.Class
对象,作为程序访问方法区中的类型数据的外部接口加载阶段与连接阶段的部分动作交叉进行,但这些部分动作仍属于连接将诶段,两个阶段的开始时间仍保持固定的先后顺序。
确保class文件的字节流中包含的信息符合《java虚拟机规范》的全部约束,且当做代码运行后不会危害虚拟机自身的安全。
主要包括以下四个阶段:
基于二进制字节流进行:
是否以魔数0xCAFEBABE开头
主次版本号是否在当前java虚拟机接受范围内
常量池中是否有不被支持的常量类型(检查常量的tag标志,标志目前为1到20,对应不同的常量类型)
指向常量的各种索引值是否有指向不存在的常量或不符合类型的常量
。。。。。。。。。。。。。。。
通过该阶段验证后,字节流才被允许进入java虚拟机内存的方法区进行存储。
后面三个验证阶段都是基于方法区的存储结构进行,不会再直接读取操作字节流。
对字节码描述的信息进行语义分析,对类的元数据信息进行语义校验:
该类是否有父类(除了java.lang.Object之外所有的类都应有父类)
该类是否继承了不允许被继承的类(被final修饰的类)
若该类不是抽象类,是否实现其父类或接口中要求实现的所有方法
类中字段、方法是否与父类产生矛盾(如覆盖了父类的final字段,或出现了不符合规则的重载等)
。。。。。。。。。。。。。
通过数据流和控制流分析,保证程序语义是合法的符合逻辑的。
该阶段实际是对方法体(Class文件中的Code属性)进行校验,保证该类方法在运行时不会危害虚拟机安全:
版本号大于50的Class文件,使用类型检查的方式来完成该阶段的校验。
所谓类型检查,就是将尽可能多的校验辅助措施移到javac编译器中进行。
具体实现就是为方法体Code属性的属性表添加了一项新属性:StackMapTable
,描述了方法体所有的基本块开始时本地变量表和操作栈应有的状态。
因此字节码验证时,只需检查StackMapTable属性中的记录是否合法,不需根据程序推导其合法性。节省了大量的校验时间。
该阶段发生在解析阶段中-------将符号引用转换为直接引用
可看做是对类自身以外的各类信息进行匹配性校验。包括以下内容:
确保解析行为能正常执行。如未通过符号引用验证,虚拟机会抛出如下相关异常:
java.lang.IllegalAccessError
、java.lang.NoSuchFieldError
、java.lang.NoSuchMethodError
等。
该阶段只有通过和不通过的差别,只要通过就对运行几乎没有影响,因此不是必须要执行的。
当程序的全部代码(自己编写的、第三方包中的等)已被反复使用和验证过,在生产环境就可使用
-Xverify:none参数关闭大部分的类验证,以缩短类加载时间。
正式为类中定义的变量(静态变量等)分配内存并设置类变量初始值。
注意:
public static final.....
)会在该阶段将其初始化为用户指定的值。将常量池中的符号引用替换为直接引用的过程。
符号引用
以一组符号描述引用的目标,可以使任何形式的字面量,只要使用时能无歧义地定位到目标即可。与虚拟机实现的内存布局无关。
直接引用
可直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄,与虚拟机实现的内存布局直接相关。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行。
假设当前类为D,如要把一个未解析过的符号引用为N解析为一个类或接口C的直接引用,那么会进行以下三个解析过程:
[Ljava/lang/Integer
形式,则会按照第一点的规则加载数组元素类型。即假设的 java.lang.Integer,接着虚拟机生成一个代表该数组维度和元素的数组对象查找失败抛出NoSuchFieldError异常。
访问权限验证失败抛出IllegalAccessError
虚拟机将主动权交应用程序。
初始化阶段就是执行类构造器
静态语句块只能访问定义在其之前的变量。定义在其之后的变量,在前面的静态语句块可以赋值,但不能访问,如下:
编译期会给出非法向前引用的提示。
由于父类的
static class Parent{
public static int A=2;
static {
A=1;
}
}
static class Son extends Parent{
public static int B=A;
}
public static void main(String[] args) {
System.out.println(Son.B);//1
}
如果类中无静态语句块也没有对变量的赋值操作,则编译期可以不生成
接口中因为有变量赋值操作,因此也有
ClassLoader负责加载class文件,class文件在文件开头有特定的文件标识(cafe babe),将class文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且ClassLoader只负责class文件的加载,其是否能够运行,由执行引擎Execution Engine决定
任意一个类,都由加载它的类加载器和其自身共同确立其在java虚拟机中的唯一性,每个类加载器都由一个独立的类名称空间。
即比较两个类是否“相等”,必须是在同一个类加载器的前提下才有意义。否则即使两个类源于同一个Class文件,被同一个虚拟机加载,但如果类加载器不同,也必定不相等。
public class _04ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader classLoader=new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
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) {
throw new ClassNotFoundException(name);
}
}
};
Object obj=classLoader.loadClass("com.wml.jvm.classload._04ClassLoaderTest");
System.out.println(obj.getClass());
System.out.println(obj instanceof com.wml.jvm.classload._04ClassLoaderTest);//false
}
}
如上,自定义了一个类加载器,可以加载和它一个路径下的Class文件,我们让它加载自身所在的类com.wml.jvm.classload._04ClassLoaderTest并实例化出对象,与该类本身instance of发现是false,因为一个是使用我们自定义的类加载器,一个是虚拟机应用程序类加载器加载的,所以比较结果一定是不相等的。
模型如下:
启动类加载器
C++实现
不可直接被java程序引用
扩展类加载器
由java实现,可直接在程序中使用扩展类加载器加载class文件
应用程序类加载器
也叫系统类加载器,负责加载用户类路径上所有的类库,可直接在代码中使用之。
如果没有自定义类加载器,则这个就是程序中的默认的类加载器
除启动类加载器外,其他的类加载器都继承自抽象类java.lang.ClassLoader
如下我们打印Object的类加载器,发现是null,因为Object由启动类加载器加载,其是由C++写的,不可直接被java程序引用。
Objecet以及String这些基础类都在rt.jar包中,rt即Runtime,提供了java运行时环境所需的基础类
public static void main(String[] args) {
Object object=new Object();
System.out.println(object.getClass().getClassLoader());//null
我们再定义一个Person类,分别打印其父类加载器和父类加载器的类加载器:
public static void main(String[] args) {
//null
System.out.println(person.getClass().getClassLoader().getParent().getParent());
//sun.misc.Launcher$ExtClassLoader@1b6d3586
System.out.println(person.getClass().getClassLoader().getParent());
//sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(person.getClass().getClassLoader());
由双亲委派结构图可知,Person由应用程序(系统)类加载器加载,打印的是/sun.misc.Launcher$AppClassLoader@18b4aac2
,其父类加载器就是扩展类加载器(sun.misc.Launcher$ExtClassLoader@1b6d3586
),再往上就是启动类加载器(null)
比如加载位于rt.jar包中的类java.lang.Object,不管是采用哪个加载器加载这个类,最终都是委托给顶层的启动类加载,这样就保证了使用不同的类加载器最终都是同样一个Object
如果一个类收到了类加载的请求,它不会自己去加载这个类,
而是把这个请求委派给父类加载器完成,
每个层次的类加载器都是如此,
因此所有的加载请求最终都应该传送到最顶层的类加载器中,
只有当父加载器反馈自己无法完成这个加载请求(搜索范围内没有找到所需类)时,子加载器才会尝试自己去完成加载。
即:每次都将请求自下向上委派到最顶层类加载器,然后再顶向下在各个类加载器搜索所需类,找不到就向下传。
源码实现如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1.检查请求的类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2.没被加载,且父加载器不为空,则调用父加载器的loadClass()方法
c = parent.loadClass(name, false);
} else {
//3. 父加载器为空,则默认使用BootstrapClassLoader启动类加载器作为父加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果父加载器加载失败,抛出ClassNotFoundException异常,说明父加载器无法完成请求
}
if (c == null) {
long t1 = System.nanoTime();
//父加载器无法完成请求,则调用自身的findClass方法进行类加载
c = findClass(name);
............
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
在jdk9后,因为加入了模块化管理,双亲委派的机制被 “ 打破 ”
参考:《深入理解java虚拟机第三版》