0.前言
最近又开始看《深入理解Java虚拟机》这本书了,发现这东西很久不看忘得很快,还是写下来加深点影响吧,在这一篇文章里,我们要讨论的问题主要有以下几个方面:
(1)什么是虚拟机的类加载机制。
(2)类加载的过程
(3)类加载器
1. 什么是虚拟机的类加载机制
虚拟机把描述类的文件从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程,我们称为虚拟机的类加载机制。
2. 类加载的过程是怎么样的
整个过程如下图所示:
下面我们来分步解释下整个过程:
2.1 加载:
在这个阶段,虚拟机主要完成一下三件事:
(1)通过一个全额限定名来获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态储存结构转化为方法区的运行时数据结构。
(3)在Java堆中声称一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
Java虚拟机内存模型有不懂的可以看这篇文章:
https://www.jianshu.com/p/43d403b3eff7
2.2 连接
类的连接主要分为三个步骤:
(1)验证:验证被加载后的类是否有正确的结构,类数据是否符合虚拟机的要求,确保不会危害虚拟机安全。
包含四个阶段的校验动作:
- a.文件格式验证;
- b.原数据信息进行语义校验;
- c.字节码验证;
- d.符号引用验证。
(2)准备:为类的静态变量(static filed)在方法区分配内存,并设置默认初始值(0值或null值),这些内存都将在方法区分配。对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。
这里有2点要特别注意:
- 这里虚拟机只初始化类的静态变量,不初始化实例变量。
- 这里说的初始值“通常情况下”,指的是数据类型的零值。
举个例子:public static it value = 123
准备阶段后初始值是0,不是123。把value赋值为123的操作,在初始化阶段才做。
(3)解析:将类的二进制数据内的符号引用替换为直接引用。
什么意思呢?要解答这个问题,首先我们需要知道什么是符号引用和直接引用:
- 符号引用:用自足福海来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。
- 直接引用:可以是指向目标的指针,相对偏移量或者能间接找到目标的句柄。如果有了直接引用,那目标必定已经在内存中了。
2.3 初始化
初始化是类加载过程的最后一步,之前的4步都是完全有虚拟机主导的,直到这一步才由Java程序代码来控制。在准备阶段,变量已经赋过一次系统要求的初始值了,在初始化阶段,则是根据程序员的要求来初始化变量和其他资源。或者说是执行类的构造器<clinit>()
的过程。
在Java类中,对类变量指定初始值有两种方式:
(1)在声明类变量时指定初始值。
public static int A = 1
(2)在使用静态初始化块时,为类变量指定初始值。
public int A;
static {
A = 2;
}
JVM会按照这些语句在程序中的排列顺序依次执行他们。
- JVM初始化一个类的步骤:
(1)假如这个类还没有被加载和连接,则程序先加载并连接该类。
(2)假如该类的直接父类还没有被初始化,则先初始化其直接父类。若该直接父类又有直接父类,依次类推。所以JVM最先初始化的总是 java.lang.Object 类。
(3)当程序主动使用任何一个类时,系统会保证该类以及所有父类(包括直接父类和间接父类)都会被初始化。
(4)假如类中有初始化语句,则系统依次执行这些初始化语句
3. 类加载器
3.1 什么是类加载器
虚拟机设计团队把类加载阶段中的"通过一个类的全限定名来获取描述此文件的二进制字节流"这个动作放到Java虚拟机外部趋势线,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
3.2类与类加载器
对于任何一个类,都需要由加载它的类加载器和这个类本身一同确定其唯一性,怎么理解这句话呢?简单来说就是如果两个类即使来自于同一个文件,如果加载他们的类加载器不一样,那这两个类必定不相同。
可以看个书上的例子:
package demo.jvm;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoadTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
};
Object obj = myLoader.loadClass("demo.jvm.ClassLoadTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof demo.jvm.ClassLoadTest);
}
}
输出:
class demo.jvm.ClassLoadTest
false
代码做了两件事,第一:自己构造了一个类加载器myLoader
, 第二:使用这个类加载器去加载了一个名为demo.jvm.ClassLoadTest
的类,并实例化了这个类对象。
从输出来看,这个对象却是是类demo.jvm.ClassLoadTest
实例化出来的,但是从第二句来看,这个对象与系统实例化出来的对象类型并不相同,这也验证了之前的说法:如果两个类即使来自于同一个文件,如果加载他们的类加载器不一样,那这两个类必定不相同。
3.3 双亲委派模型
Java虚拟机只有两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader):使用C++语言(HotSpot)实现,是虚拟机的一部分,该类加载器实例无法被用户获取;
- 所有其它的类加载器:均由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader;
从Java程序员的角度,类加载器还可以继续细化,绝大部分Java程序都会使用到以下3种类加载器。
- 启动类加载器 (Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的目录中的,并且是虚拟机识别的(仅按照文件名识别,例如rt.jar)类库加载到虚拟机内存中。 启动类加载器无法被Java程序直接引用,用户在编写自定义加载器时,如果需要把加载请求委托给引导类加载器,直接使用null代替即可。
- 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,他负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。该类是ClassLoader中的getSystemClassLoader()方法的返回值,因此也称作“系统类加载器”。它负责用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有定义过自己的类加载器,一般情况下这个就是程序的默认类加载器。
如下图所示的这种类加载器之间的关系,我们称为双亲委派模型。
它的工作过程是这样的
(1)如果一个类加载器收到了类加载的请求,他首先不会自己尝试去加载这个类,而是把这个请求发给父类加载器去完成。
(2)所有的加载请求都是会传送到顶层的启动类加载器。
(3)当父类加载器反馈自己无法完成这个类的加载,子加载器会尝试自己加载。
那么双亲委派的好处是什么呢?
双亲委派模型能保证基础类仅加载一次,不会让jvm中存在重名的类。比如String.class,每次加载都委托给父加载器,最终都是BootstrapClassLoader,都保证java核心类都是BootstrapClassLoader加载的,保证了java的安全与稳定性。