之前写了篇关于Java虚拟机内存管理方面的学习总结,点击查看,主要总结了Java虚拟机运行时不同数据区域的作用(堆、方法区、栈等等),以及内存分配和内存回收的原理,本篇文章主要介绍Java虚拟机类加载机制,搞懂虚拟机是怎么加载编译后的类文件的。
一、Class类文件
二、类加载机制
三、类加载器
四、类的初始化
五、总结
一、Class类文件
Sun公司于1995年5月正式推出Java语言,如今Java已经得到广泛的认可和使用,Java得以欢迎的重要原因是它实现了“一次编写,到处运行”的理想。
构成平台无关性的基础是虚拟机和字节码。字节码是一种虚拟的机器指令代码,不针对特定的机器,Java虚拟机是执行Java程序的软件平台,负责将字节码解释成本地机器指令代码。Java源程序经编译后生成存储字节码的Class类文件,它是Java虚拟机执行引擎的数据入口,也是Java技术体系的基础支柱之一。
二、类加载机制
Java源程序在编译后生成Class类文件,在类文件中存储的各种信息最终都需要加载到虚拟机中之后才能被运行和使用。** 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。**
Java类加载过程从上面这张图可以看到,类加载的全过程主要有5个阶段,分别是加载,验证,准备,解析和初始化,其中验证、准备和解析3个阶段属于连接。下面主要对这5个阶段作详细介绍。
加载 加载是类加载过程的第一个阶段,在这个阶段,虚拟机需要完成的功能主要有:(1) 通过一个类的全限定名来获取定义此类的二进制字节流 (2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 (3) 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
验证 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。所谓类变量,就是被static关键字修饰的变量,不包括实例变量。
解析 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化 类初始化阶段是类加载过程的最后一步,到了这一阶段,才真正开始执行类中定义的Java程序代码。
三、类加载器
类加载过程的第一个阶段是加载,它主要是根据类的全限定名来获取类的二进制字节流。这个阶段主要是通过类加载器来完成的。
类加载器的类型
绝大部分Java程序都会使用到三种类加载器:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader) 和 应用程序类加载器(Application ClassLoader),当然我们还可以使用自定义的类加载。
Java类加载器启动类加载器 启动类加载器是使用C++语言实现的,是虚拟机自身的一部分,它负责加载JRE/lib/rt.jar中的类,或者被-Xbootclasspath参数指定的路径中的能被虚拟机识别的类库。
扩展类加载器 由Java语言实现,负责加载Java平台中扩展功能的一些jar包,包括jre/lib/ext/*.jar或java.ext.dirs系统变量指定目录下的jar包。
应用程序类加载器 一般也称为系统类加载器,它负责加载用户类路径(classpath)中指定的jar包及目录中class。
双亲委派模型
在上面的类加载器图中,我们可以看到类加载器间存在一种层次关系,这就是类加载器的双亲委派模型( Parents Delegation Model )。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器间的父子关系不是通过继承来实现,而是使用组合关系来复用父加载器的代码。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
双亲委派模型的意义在于,对于保证Java程序的稳定运作很重要,比如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。如果没有双亲委派模型,由各个类加载器自行去加载的话,如果用户自己写了一个名为java.lang.Object的类,并放在程序的ClassPath中,那么系统将出现多个不同的Object类,很混乱。
双亲委派模型的** 实现过程 **比较简单,如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类无法完成加载请求
}
if (c == null) {
// 父类加载器无法加载时 自身再加载
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
可以看到,主要逻辑是先检查要加载的类是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器,如果父类加载失败,则抛出异常后再调用自己的findClass()方法进行加载。
四、类的初始化
前面已经简单提到,类初始化阶段是类加载过程的最后一步,到了这一阶段,才真正开始执行类中定义的Java程序代码。下面我们先看这样一道题:
// 执行下面代码,输出结果是?
public class Child extends Parent {
static {
System.out.println("Static Child");
}
public Child() {
System.out.println("Child");
}
public static void main(String[] args) {
new Child();
new Child();
}
}
class Parent {
static {
System.out.println("Static Parent");
}
public Parent() {
System.out.println("Parent");
}
}
在准备阶段,变量已经赋过一次系统要求的初始值,在初始化阶段,则是根据程序的要求去初始化类变量和其它资源,初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。所以在关于类的初始化顺序方面,我们可以得到一条结论就是,父类的所有静态变量和静态初始化块都在子类的前面执行。
什么时候进行类的初始化?
主要有以下情况:
1、使用new关键字实例化对象的时候
2、读取或者设置一个类的静态字段时
3、调用一个类的静态方法时
4、对类进行反射调用时,如果类没有进行过初始化,则需要先触发其初始化。
5、初始化一个类时,先初始化其父类。
6、虚拟机启动时,先初始化要执行的主类(包含main()方法的那个类)
五、总结
到这里,关于JVM类加载机制方面的学习总结就结束了,在此感谢周志明老师的《深入理解Java虚拟机》这本书,看完之后有种豁然开朗的感觉。
网友评论