概述
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始
化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载时机:
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载
(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化
(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个
部分统称为连接(Linking).
在加载阶段:JVM规范中没有进行约束, 这点可以交给虚拟机的具体实现来自由把握.
在初始化阶段:
JVM严格规范了有且只有以下5种情况必须立即进行初始化(初始化前,必须经过加载,验证解析,准备阶段)
1.使用new实例化对象时,读取(getstatic)和设置(putstatic)类的静态变量,静态非字面值常量时,调用静态方法(invokestatic)时
2.对内进行反射调用时, 如果类没有进行过初始化,则需要先触发其初始化
3.当初始化一个类时,如果父类没有进行初始化,首先要初始化父类.
4.启动程序所使用的main方法所在类
5.当使用1.7的动态语言支持时
以上5种称为主动引用,除此之外的引用被称为被动引用
被动引用常见情况:
- 调用父类的静态字段,只会初始化父类,不会触发子类的初始化
- 定义对象数组和集合,不会触发该类的初始化
- 类A引用类B的static final 常量不会导致类B初始化(注意静态常量必须是字面值常量,否则还是会触发B的初始化)
- 通过类名获取对象,不会触发类的初始化(System.out.println(Person.class));
- 通过Class.forName加载指定类时,如果指定参数initialize为false,也不会初始化该类
- 通过ClassLoader默认的loadClass方法,也不会触发初始化.
类加载过程:
加载流程: 加载->连接(验证->准备->解析)->初始化->使用->卸载
加载
在加载阶段 , JVM需要完成3件事情
- 查找并加载类的二进制数据(Class文件)
- 静态存储结构转化为方法区运行时数据结构:类的类信息
- Java堆生产:Class文件对应的类实例(相当于一个句柄),去访问方法区
加载.class文件的方式:
- 从本地系统中直接加载
- 通过网络下载class文件
- 将java源文件动态编译为.class文件
- …
验证
确保加载的类信息是正确的
- 文件格式验证
- 验证Class文件的标示:魔数(默认为:CAFE BABE)
- 验证Class文件的版本号
- 验证常量池(常量类型,常量类型数据结构是否正确,UTF8是否符合标准)
- Class文件的每个部分(字段表,方法表)是否正确
- 元数据验证(父类验证,继承验证,接口验证,final验证)
- 这个类是否有父类,是否实现了父类的抽象方法,是否重写了父类的final方法,是否继承了被final修饰的类等等
- 字节码验证(指令验证)
- 符号引用验证(通过符号引用是否能找到类,方法,字段)
- 通过符合引用能找到对应的类和方法
,符号引用中类、属性、方法的访问性是否能被当前类访问等等。
- 通过符合引用能找到对应的类和方法
准备
类变量:一般称为静态变量
实例变量:当对象被实例化时,实例变量就被跟着被确定
为类的静态变量(static)分配内存空间并赋予初始值(默认值) ,这些变量所使用的内存都将在方法区中进行分配.
例:static int value = 2;初始化值为0.
这时候尚未开始执行任何Java方法,而把value赋值为2的putstatic指令是程序被编译后,存放于类构造器< clinit>()方法之中,所以把value赋值为2的动作将在初始化阶段才会执行。
而 static final int value = 2;
对应到常量池ConstantValue,在准备阶段必须赋值为2.
数据类型对应的零值
解析
将符号引用转化为直接引用
直接引用: 指向目标的指针或者偏移量
主要涉及:类,接口,字段,方法等.
匹配:简单名字+描述符 同时满足
类或接口的解析
public class A{
C data; //C有可能是个类,或接口
}
- 如果C不是一个数组类型 , 那么会将代表N的权限定名传递给A去加载这个类C . 若在加载C的过程中出现了异常,则解析失败.
- 如果C是一个数组类型 ,并且数组元素类型为对象 (Integer[] data), 则按照 1中的方式先加载数组元素类型 ,接着由虚拟机生成一个代表此数组维度和元素的数组对象.
- 如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认A是否具备对C的访问权限。
如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常。
字段的解析:
class A extends B implements C,D{
private int a;
}
- 先在本类去找有没有简单名称和字段描述符匹配的字段,有则返回字段的引用 , 查找结束.
- 如果类实现了接口,递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束.
- 如果不是继承Object类, 再去搜索父类 , 有则返回.
- 否则 , 查找失败
查找可能会有异常
如果找到,没有权限:java.lang.IllegalAccessError 如果失败:java.lang.NoSuchFieldError
类方法的解析:
class A extends B implements C,D{
public void inc();
}
类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
- 在本类里面去找有没有匹配的方法
- 父类去找
- 接口列表去找方法(如果找到则代表本类是抽象类)
如果找到,没有权限:java.lang.IllegalAccessError 如果失败:java.lang.NoSuchMethodError
接口方法的解析:
与类方法解析不同,如果在接口方法表中发现class_index中的索引C是个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
- 在本类找有没有匹配的方法
- 父类接口去递归查找
如果失败:java.lang.NoSuchMethodError
由于接口中的所有方法默认都是public的,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛出java.lang.IllegalAccessError异常。
初始化
java虚拟机对类进行初始化,对静态变量赋予正确值, 真正开始执行类中定义的Java程序代码.
< init>:类的实例构造器
< clinit>:由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并.(如果没有静态变量,静态块,则没有clinit)
在类中:
< clinit>()方法与类的构造函数(或者说实例构造器< init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<
clinit>()方法执行之前,父类的< clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<
clinit>()方法的类肯定是java.lang.Object。 由于父类的<
clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,
在接口中:
接口中不能有静态块 ,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成< clinit>()方法。
但接口与类不同的是,执行接口的< clinit>()方法不需要先执行父接口的< clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的< clinit>()方法。
虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的< clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>()方法完毕 .
Class A{
static int i = 2;
static{
sout;
}
int n;
}
类的加载方式(整个过程)
隐式加载
- 创建类对象
- 使用类的静态域
- 创建子类对象
- 使用子类的静态域
在JVM启动时,BootStrapLoader会加载一些JVM自身运行所需的class
在JVM启动时,ExtClassLoader会加载指定目录下一些特殊的class
在JVM启动时,AppClassLoader会加载classpath路径下的class,以及main函数所在的类的class文件
显式加载
ClassLoader.loadClass(className),只会加载和链接,不会进行初始化
Class.forName(String name,boolean initialize,ClassLoader loader),使用loader进行加载和链接,根据initialize参数决定是否初始化
类加载器:
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性 , 每一个类加载器,都拥有一个独立的类名称空间。在比较两个类是否“相等”时 ,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
//sun.misc.Launcher
//..........
/..........
var1 = Launcher.ExtClassLoader.getExtClassLoader();
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
//..........
问题: ExtClassLoader和AppClassLoader有什么关系?
ExtClassLoader是AppClassLoader的Parent
//代表同时只有一个load来加载class,保证class的唯一性
findBootStrapClass(String name): //方法
synchronized(getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(class);
if(c == null) {
......
if(parent != null) {
//递归调用
} else {
findBootStrapClass(name); //native方法 JNI调用
}
}
}
- BootStrapClassLoader(C语言编写): JAVA_HOME
/lib/rt.jar - ExtClassLoader: JAVA_HOME/lib/ext/javax.* 或"java.ext.dirs"指向的目录
- AppClassLoader:自定义的类,负责将系统类路径(CLASSPATH)中指定的类库加载到内存中
- 用户自定义类加载器: 流,网络,数据库
双亲委派机制(安全)
如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把请求委托给父类加载器去完成,依次向上.因此,所有的类加载请求最终都会被传递到顶层的启动类加载器中.
只有当父加载器在他的搜索范围中没有找到所需的类时,既无法完成加载,自加载器才会尝试自己去加载该类.
父类能加载的,不给子类加载
这种机制有如下好处:
- 可以保证java核心类库的安全,即保证由引导类加载器加载的类不能被用户随便替换,不能自己定义一个java.lang.String 的类来替换java核心类库的java.lang.String类,否则会抛出ClassCastException。
- 避免类的重复加载.
加载顺序: 自顶向下 AppClassLoader -> Extension ClassLoader -> BootStrap ClassLoader
检查顺序: 自底向上