JVM类加载总结
1、概述
类加载的过程,就是将类的字节码装载到内存方法区的过程(方法区的相关知识参看Java内存模型)。
与C语言这样需要在运行前就进行链接(Link)的语言不通,Java语言中类型的加载、链接、初始化都是在程序运行期间完成的。
这种策略为Java应用程序提高了极大的动态灵活性。
Java虚拟机(JVM)中用来完成类加载的具体实现,是类加载器。
类加载器读取.class字节码文件将其转换成java.lang.Class类的一个实例。每个实例用来表示一个java类。通过该实例的newInstance()方法可以创建出一个该类的对象。
(我们通常会说方法区中存的是类,实际上存的也是实例,只不过是特殊实例,是Class这个类的实例)
加载类的全流程如下图所示(简书这货居然不支持流程图和甘特图,蛋疼……)。
类加载流程图2、类加载流程
类加载的流程主要分为加载、链接、初始化三个阶段。其中链接又细分为验证、准备、解析三个阶段。
2-1、加载(Loading)
加载阶段jvm主要做三件事
-
通过类的“全限定名”获取量二进制字节流
这里会说成是“二进制字节流”,是因为class文件的来源非常广。除了最常见的jar、war文件,还可以从网络获取,可以运行时冻土工程,由其他文件生成(比如jsp),甚至直接从数据库读取。
-
将字节流变成方法区的运行时数据结构
-
生成代表这个类的java.lang.Class对象(这个对象也放在方法区中),作为方法区中其对应的类型数据的访问入口
2-2、验证(Verification)
保证读入的class文件流是合法的——符合当前jvm版本的要求,更重要的是不会危及jvm安全。
毕竟java编译并不是class文件的唯一来源,而且class文件也是很容易篡改的。
2-3、准备(Preparation)
在方法区中,为类变量分配内存并分配初始值。
注意这里的初始值指的是“零值”,比如数值为各种0(0、0L、0.0f),String为null。
比如
public static int classint = 123;
那么在这一阶段 classint 的值为0。因为现在还没有执行任何java方法,赋值123这个动作是在类构造器的<clinit>()方法中的。
唯一的特例:
public static final int classint = 123;
使用final修饰的变量实际上就是常量(ConstantValue属性),其赋值与java方法无关,在准备阶段会直接赋值。
2-4、解析(Resolution)
将常量池中的“符号引用”替换为“直接引用”的过程。
- 符号引用:以一组符号来描述所引用的目标。与虚拟机当前内存状况无关,需要引用的目标未必已经加载。
- 直接引用:直接指向目标的指针、相对偏移量或者是间接定位用的句柄。
A:“班费交给谁?”
B:“交给班长”
A:“谁是班长,坐在哪儿?”
B:“我也不知道,反正我知道要交给班长” —— 符号引用
C:“我知道,他坐在第二排靠窗的座位” —— 直接引用
2-5、初始化(Initialization)
初始化,就是执行类构造器<clinit>()方法的过程。
所谓的<clinit>()方法,是编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并而成的。顺序为该语句在源文件中的顺序。
同时,虚拟机会保证首先执行父类的<clinit>()方法,然后才是子类的<clinit>()。
所以最先执行的是Object的<clinit>();
并且父类的静态代码块一定先于子类被执行。
一个很重要的特性(其实是面试时常问):一个类只会被初始化一次。
3、类的主动引用(何时触发类的初始化)
以外几种场景(动作)被称为类的主动引用
- 最常见的场景:使用new关键字实例化对象
- 读取或者为一个类的静态变量赋值(但是final修饰的除外)
- 调用一个类的静态方法
- 使用反射对类进行引用
- 初始化一个类时,如果其父类还没有被初始化(复习一下上一节的知识哈),则先对父类进行类加载
- 虚拟机启动时,会先初始化包含main()方法的那个类(主类)
接口比较特殊:子接口初始化时,并不要求父接口完成初始化。
4、类的被动引用(何时不触发类的初始化)
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化(只有真正声明这个静态变量的类才会被初始化)
- 通过数组定义类,不会触发此类的初始化 A[] a = new A[10];
- final修饰的常量,编译期间存入常量池,引用它不会触发定义常量所在的类(本质上并没有直接引用定义常量的类)。
- 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化(这个参数是告诉虚拟机,是否要对类进行初始化)
- ClassLoader默认的loadClass方法,也不会触发初始化动作。
5、类加载器
最开始就提过,jvm中用来完成类加载的具体实现,就是类加载器。类加载器读取.class字节码文件将其转换成java.lang.Class类的一个实例。
在jvm中,任意一个类,都是通过 “加载它的类加载器” + “类本身” 来确定其唯一性的。
也就是说,即使对于同一个类,被不同的类加载器加载过两次,对于jvm来说也是不相等的。
java中的类加载器有以下几种:
5-1、启动类加载器 Bootstrap ClassLoader
使用 C++编写的。(其他类加载器都是使用Java编写,继承自 java.lang.ClassLoader)
负责加载:
存放在<JAVA_HOME>\lib目录下的,或者被 -Xbootclasspath 参数指定的路径中的,
并且是虚拟机识别的类库(仅按照文件名识别,比如rt.java,名字不符合的类库即使放在lib目录中也不被加载)。
5-2、扩展类加载器 Extension ClassLoader
负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量指定路径中的类库
5-3、应用程序类加载器 Application ClassLoader
这个类加载器是ClassLoader中getSystemClassLoader()方法的返回值,一般也称为系统类加载器。
负责加载用户类路径(ClassPath)上指定的所有类库。
应用程序中如果没有自定义过自己的类加载器,默认使用这个类加载器。
6、双亲委派模式
6-1、什么是双亲委派模式
只看UML图的话,ExtClassLoader和AppClassLoader是同级的,不存在继承、依赖关系(两者都存放在Launcher类中)
image(Bootstrap ClassLoader是C++编写的,不在上图中)
但实际上,在类加载的时候,各个类加载器之间是有先后关系的。
jvm加载类时,调用类加载器的顺序是这样的(自顶向下尝试加载类/自底向上委派):
双亲委派模式每一个类加载器在获得类加载请求时,自己不动手,都优先向自己的“上级”发起类加载请求,一直到最基本的启动类加载器;
然后从最上层开始,逐级判断这个类应不应该是自己负责加载的,如果是就加载,不是就将请求打回,由自己的下一级进行判断。
也就是说,不管什么类,都是一定由最上级(parents)的类加载器首先进行判断是否加载,下级只负责将请求上传,工作不被甩到自己头上是绝不会主动去干的。这种模式被称为“双亲委派模式”。
下面是该模式的时序图
双亲委派时序图6-2、双亲委派的好处
Java类随着他的类加载器一起具备了一种带有优先级的层次关系。
比如Object存放在rt.jar里面,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。
否则系统中会出现多个不同的Object类。
可以自己尝试一下自定义一个 java.lang.Object,然后用自定义类加载器去加载(破坏掉双亲委派机制,不让委派给其他类加载器)。这样一个Object可以被加载,但是由于其类加载器不同,jvm仍然不会将它当做所有类的基类来对待。
6-3、解读源码
我们来看一下 Launcher 类的源码,理解一下双亲委派是如何工作的。
(注意一下,下文的“父、类加载器”,不要理解成“父类、加载器”,仅仅指双亲委派时的顺序,无关继承关系)
public class Launcher {
private static Launcher launcher = new Launcher();
// 启动类加载器要读取被 -Xbootclasspath 参数指定路径中的类库,所以这里读取系统参数中的路径名
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
public Launcher() {
// 创建扩展类加载器对象。其中会读取系统参数 System.getProperty("java.ext.dirs")
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError("Could not create extension class loader", e);
}
// 创建应用程序类加载器。参数中使用了扩展类加载器的对象,是为了设定亲子关系,将其设定为自己的父类加载器(上一级)
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError("Could not create application class loader", e);
}
Thread.currentThread().setContextClassLoader(this.loader);
………………
}
上面一段代码中,两级类加载器调用的 .getxxxClassLoader()方法,其实都去调用了Launcher的父类(URLClassLoader)的构造方法,它们需要传的参数,是自己的父类加载器。
这里,扩展类加载器传的是null,应用程序类加载器传的是扩展类加载器。
之所以会传null,可以直接看基类 ClassLoader 的 loadClass() 方法
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 这里会调用一个native方法findLoadedClass0,检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 这个parent就是上面那段代码中类加载器实例化时指定的父类加载器
if (parent != null) {
// 父类加载器非空时,直接调用其loadClass方法。那么appClassLoader就会调用extClassLoader的loadClass方法,向上委派
c = parent.loadClass(name, false);
} else {
// 这里就是给扩展类加载器传准备的了,父类加载器为空时,就是用BootstrapClassLoader去加载类
// (BootstrapClassLoader不是java写的,这里没法创建java对象,只能是null)
// 这个方法是一个native方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
………………
}
if (c == null) {
// 谁都无法加载的话,开始调用findClass方法
long t1 = System.nanoTime();
c = findClass(name);
…………………
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
上面代码里有一个有意思的地方,“谁都无法加载的话,开始调用findClass方法”。而这个findClass方法里面是啥呢?
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
直接抛异常哈哈
实际上,这是给用户自定义类加载器留的接口,需要自定义的时候,重写findClass()方法即可。
(如果想人为破坏双亲委派模式的话,还需要自己重写loadClass()方法——这也是个protected方法)
7、补充:关于非静态方法
类加载时,不光静态方法,实际上非静态方法也会被加载(同样加载到方法区),
只不过要调用到非静态方法需要先实例化一个对象,然后用对象调用非静态方法。
因为如果让类中所有的非静态方法都随着对象的实例化而建立一次,会大量消耗资源,所以才会让所有对象共享同一个非静态方法,然后用this关键字指向调用非静态方法的对象。
网友评论