虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化, 最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。
在Java语言里,类型的加载、连接和初始化过程都是在程序需运行期间完成的。Java 里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。
类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括:加载(Loading) 、 验证(Verification) 、 准备(Preparation) 、 解析(Resolution) 、 初始化(Initialization) 、 使用(Using) 和卸载(Unloading) 7 个阶段。
其中验证、准备、解析 3 个部分统称为连接(Linking) 。
加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种 顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开 始,这是为了支持 Java 语言的运行时绑定( 也称为动态绑定或晚期绑定) 。
虚拟机规范则是严格规定了有且只有 5 种情况必须立即对类进行“初始化”( 而加载、验证、 准备自然需要在此之前开始) :
除了以上几种方式外,通过其他方式引用类都不会触发初始化。
比如:
通过子类引用父类的静态字段,不会导致子类初始化
通过数组定义来引用类,不会触发此类的初始化
//不会触发SuperClass和SubClass的初始化
SuperClass[] sc = new SubClass[10];
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不 会触发定义常量类的初始化。
public class ConstClass {
static{
System.out.println("ConstClass init");
}
public static final String CONST = "const";
}
public static void main(String[] args) {
//不会触发ConstClass的初始化
System.out.println(ConstClass.CONST);
}
当一个类在初始化的时候,要求其父类全部都已经初始化过了,但是一个接口在初始化时, 并不要求其父类接口全部完成初始化,只有在真正使用到父接口的时候( 如引用接口中定义 的常量) 才会初始化。
在加载阶段,jvm需要完成以下三件事:
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中, 方法区怎么存储由JVM自己定义。然后在内存中实例化一个java.lang.Class类的对象( 并没有 明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但 是存放在方法区里面) 。 加载阶段和连接阶段的部分内容( 如一部分字节码文件格式验证动作) 是交叉进行的,加载 阶段尚未完成,连接阶段可能已经开始。
验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符 合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致上会完成下面 4 个阶段的检验动作:
文件格式验证
这一阶段要验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
元数据验证
这一阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
字节码验证
这一阶段的主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证
这一阶段的主要目的是确保解析动作能正常执行。其校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段———解析阶段中发生。
如果所运行的全部代码(包括自己编写的及第三方包中的代码)都已经被反复使用和验证过,那么在实施阶段就可以考虑使用-Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备阶段是正式为类变量分配内存并设置类变量的初始值的阶段,这些变量所用的内存都将在方法区中进行分配。
这个阶段进行内存分配的仅包括类变量( 被 static 修饰的变量) ,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
这里所说的初始值“通常情况”下是数据类型的零值。
//value在准备阶段过后的初始值为0,而不是123,把value赋值为123的动作将在初始化阶段(方法中)才会执行。
public static int value=123;
特殊情况:如果类字段的字段属性表中存在 ConstantValue 属性,那在准备阶段变量 value 就会被初始化为 ConstantValue 属性所指定的值。
假设上面类变量 value 的定义变为: public static final int value= 123;
编译时 Javac 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 123。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关,引用的目标不一定已经加载到内存中。
直接引用:直接引用和虚拟机的布局是相关的,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的。如果有了直接引用,那么直接引用的目标一定被加载到了内存中。 其可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。
解析动作主要针对类或接口、字段解析、类方法解析、接口方法解析、方法类型解析、方法 句柄解析和调用点限定符 7 类符号引用进行。
初始化阶段是执行类构造器 方法的过程。
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块( static 块) 中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
public class Init {
static{
i = 0;//可以赋值
//System.out.println(i);//不能访问
}
static int i = 1;
public static void main(String[] args) {
System.out.println(i);
}
}
输出结果:
1
虚拟机保证子类的 方法执行之前,父类的 方法已经执行完毕,因此在虚拟机中第一个被执行的方法的类肯定Object。
对于接口,不能使用static块,但是可以有静态变量的赋值操作。子类接口的 方 法调用并不保证父接口的 方法被先调用,只有用到父接口的静态变量的时候, 父接口 方法才会被调用。接口的实现类在初始化时也一样不会执行接口 的 方法。
虚拟机会保证一个类的 方法在多线程环境中被正确地加锁、同步。如果一个线 程的 方法调用时间过长,就可能造成多个线程阻塞。
()方法对已类或借口不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成方法。
通过一个类的全限定名来获取描述此类的二进制字节流,实现这一功能的代码模块成为类加载器。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。即比较两个类是否”相等“,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is == null){
return super.loadClass(name);
}
byte[] b = null;
try {
b = new byte[is.available()];
is.read(b);
} catch (Exception e) {
e.printStackTrace();
}
return defineClass(name,b,0,b.length);
}
};
Object o = myLoader.loadClass("com.bw.oom.classloader.ClassLoaderTest").newInstance();
System.out.println(o.getClass());
System.out.println(o instanceof com.bw.oom.classloader.ClassLoaderTest);
}
}
运行结果:
class com.bw.oom.classloader.ClassLoaderTest
false
可以看出对象o确实是类com.bw.oom.classloader.ClassLoaderTest的对象,但是其和com.bw.oom.classloader.ClassLoaderTest做类型所属关系的时候却反会了false,这是因为虚拟机中存在了两个ClassLoaderTest类,一个由系统应用程序类加载器加载,另一个由自定义的加载器加载的,虽然来自同一个Class文件,但依然是两个类。
可以加-XX:+TraceClassLoading参数查看系统加载了哪些类
//可以看到系统加载了两个ClassLoaderTest类
[Loaded com.bw.oom.classloader.ClassLoaderTest from __JVM_DefineClass__]
[Loaded com.bw.oom.classloader.ClassLoaderTest$1 from __JVM_DefineClass__]
上图所示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model),双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance) 的关系来实现,而是都使 用组合(Composition) 关系来复用父加载器的代码。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该先传送到顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求(在它的搜索范围内没有找到这个类)时,子加载器才会去尝试加载。
java类随着它的加载器一起具备了一种带有优先级的层次关系。类 java.lang.Object,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型.由各个类加载器自行去加载的话.如果用户编写了一个称为“java.lang.Object”的类.并存放在程序的ClassPath中.那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也就无法保证,应用程序也将会一片混乱。
//java.lang.ClassLoader#loadClass()
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查请求的类是否已经加载
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,说明父类加载器不能加载请求
}
if (c == null) {
long t1 = System.nanoTime();
//在父类加载器不能加载的时候再调用本身的findClass方法进行加载
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;
}
}