总体上说,虚拟机把描述类的数据,从字节码文件加载到内存,然后进行数据校验、转换解析、初始化,最终形成可以被寻积极使用的Java类型,这就是虚拟机的类加载机制。
与C/C++在编译时,需要连接不同,Java语言里,类型的加载、连接和初始化都是在程序运行期间完成。虽然增加了性能开销,但有更高灵活性。
Java的动态扩展特性,依赖运行时的动态加载和动态连接。
例如,面向接口编程,可以在运行时再指定实际的实现类;用户可以通过自定义类加载器,在运行时,从网络或其他地方加载一个二进制流,作为程序代码的一部分。
类从被加载到虚拟机内存中开始,到卸载出内存为止,其生命周期,如下图
加载、验证、准备、初始化、卸载,这5阶段的顺序是确定的。
解析阶段不一定,某些情况下,可以在初始化之后再开始。这是为了支持Java语言的运行时绑定
虚拟机规范严格规定,5种情况,必须立即类初始化:
遇到new
,getstatic
,putstatic
,invokestatic
字节码指令,若类未曾初始化,则先初始化
使用java.lang.reflect包的方法,反射调用类,若类未曾初始化,先初始化
REF_getStatic
,REF_putStatic
,REF_invokeStatic
的方法句柄,且句柄对应的类未曾初始化过,则先初始化加载是类加载(Class Loading)过程的一个阶段。
加载完成3件事情
HotSpot虚拟机的java.lang.Class对象,放在方法区Method Area,不是堆Heap
加载阶段与连接阶段的部分内容,交叉进行,加载阶段未完成,连接阶段可能已经开始
连接阶段的第一步,目的是为了确保class文件的字节流中包含的信息,符合当前虚拟机的要求,而且,不会危害虚拟机自身安全
大致完成4个阶段的检验
正式为类变量分配内存,并设置类变量初始值的阶段,这些变量所使用的内存,都将在方法区中分配
即static修饰的变量,初始值为各个类型的0值
如果是final修饰的类变量,会直接生成ConstantValue属性。在准备阶段,虚拟机会根据ConstantValue为变量赋值
虚拟机将常量池内的符号引用,转换为直接引用的过程
符号引用(Symbolic References)
:class文件结构规范定义了的引用
参看:JVM之类文件结构
分为:
类加载过程的最后一步,这一阶段,才真正开始执行类中定义的Java代码(或者说字节码)
准备阶段,变量已经被清零,有了初始值;初始化阶段,则根据我们定义的Java代码逻辑去初始化类变量和其他资源
从另外的角度表达,初始化阶段,是执行类构造器的
方法的过程
从虚拟机的角度来讲,只存在2种不同的类加载器
从程序员角度讲
双亲委派机制(Parents Delegation Model)
双亲委派工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。
每个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中
只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去加载。
代码实现集中在java.lang.ClassLoader的loadClass()方法中
//JDK 1.8 中的loadClass()方法
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;
}
}
为什么要使用双亲委派模型,组织类加载器之间的关系?
Java类随着它的类加载器一起具备了一种带优先级的层次关系。比如java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各个类加载器环境中,都是同一个类。
如果没有使用双亲委派模型,让各个类加载器自己去加载,那么Java类型体系中最基础的行为也得不到保障,应用程序会变得一片混乱。