JVM之类加载子系统
大家好,我是程序员学长。
从今天开始,我们开启一个新的系列文章–JVM(java虚拟机)系列。
(本系列文章是基于JDK8(HotSpot Vm)进行讨论)
首先,先给大家安利一个我觉得不错的 jvm 相关的视频教程-尚硅谷宋红康老师java虚拟机
首先,我们先思考一个问题,一个class文件是如何被java虚拟机加载执行的呢?
带着这个问题,我们来进入今天要分享的JVM系列之-类加载子系统。
类加载子系统
类加载子系统在 java 虚拟机的位置如下图所示。
一个 .Class 文件需要被类加载系统加载后,才能成为被虚拟机直接使用的Java类型。
类加载过程
类加载的过程主要分为加载、验证、准备、解析、初始化,其中验证、准备、解析三个部分统称为链接。
下面我们来看一下类的加载过程。
1.加载
1)通过一个类的全限定名来获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2.验证
目的在于确保Class文件的字节流中包含的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机自身的安全。主要包括以下4点。
1)文件格式的验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
2)元数据的验证
3)字节码的验证
4)符号引用的验证
3.准备
为类变量分配内存并设置该类变量的默认初始值,即零值。
注意这里不包括用 final 修饰的 static,因为 final 在编译的时候就会分配了,准备阶段会显示初始化。
这里不会为实例变量进行内存分配,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
考点来了:
public static int value = 10;
此时变量 value 在准备阶段过后的初始值为 0 而不是 10,因为这时尚未开始执行任何 Java 方法,而把 value 赋值为 10 的 putstatic 指令是程序被编译后,存放于类构造器 <clinit>() 方法之中,所以把value赋值为 10 的动作要到类的初始化阶段才会被执行。
public final static int value = 10;
此时变量value 在准备阶段过后的初始值为 10。
4.解析
将常量池内的符号引用转化为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用进行,分别对应于常量池的CONSTANT_Class_info、CON-STANT_Fieldref_info、 CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、 CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dyna-mic_info 和 CONSTANT_InvokeDynamic_info 8种常量类型。
5.初始化
初始化阶段就是执行类构造器<clinit>()方法的过程。
<clinit>()方法不是由程序员在Java代码中直接编写的方法,它是Javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并而来,编译器收集的顺序是由语句在源文件中出现的顺序决定的。
Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。
Java虚拟机必须保证一个类的<clinit>()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行完毕<clinit>()方法。
public class ClassLoaderDemo {
static class DeadLoop{
static {
if(true){
System.out.println(Thread.currentThread().getName() + "初始化当前类");
while (true){
}
}
}
}
public static void main(String[] args) {
Runnable script = new Runnable() {
public void run() {
System.out.println(Thread.currentThread() + "start");
DeadLoop dlc = new DeadLoop();
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
}
结果:
Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]start
Thread-0初始化当前类
类加载器的分类
站在java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
站在Java开发人员的角度来看,类加载器就应当划分得更细致一些。自JDK 1.2以来,Java一直保持着三层类加载器、双亲委派的类加载架构,尽管这套架构在Java模块化系统出现后有了一些调整变动,但依然未改变其主体结构,
三层类加载器
整体架构如下图所示:
1、启动类加载器
它负责加载存放在 <JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。
2、扩展类加载器
这个类加载器是在类 sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
3、应用程序类加载器
这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
4、用户自定义类加载器
在java的应用程序开发过程中,类的加载几乎都是由以上三种类加载器相互配合来完成加载的,在必要时,我们也可以自定义的类加载器来进行拓展,典型的如增加除了磁盘位置之外的Class文件来源,或者通过类加载器实现类的隔离、重载等功能。
下面我们通过一个例子来看一下如何获取一个类的加载器。
public class ClassLoaderTest {
public static void main(String[] args) {
//获得系统类加载器
ClassLoader systemClassLoader=ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
//获取其上层加载器,扩展类加载器
ClassLoader extClassLoader=systemClassLoader.getParent();
System.out.println(extClassLoader);
//获取其上层加载器,根加载器
ClassLoader bootstrapClassLoader=extClassLoader.getParent();
System.out.println(bootstrapClassLoader);
}
}
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@610455d6
null
通过输出结果可知,我们无法直接通过代码获取到根加载器。
双亲委派机制
这个点也是面试中经常问到的~
上图展示的各种类加载器之间的层次关系被称为类加载器的 “ 双亲委派模型(Parents Delegation Model)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。
java虚拟机对class文件采用按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存。在加载class文件时,java虚拟机采用的是双亲委派模式,即把请求交给父类处理,其工作原理如下所示。
1.如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行。
2.如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终达到顶层的启动类加载器。
3.如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
沙箱安全机制
自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中的java.lang.String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
双亲委派机制的优势
1、避免类的重复加载。
2、保护程序的安全,防止核心API被篡改。
一个小的知识点
如何判断两个class对象是否相同?
在 jvm 中表示两个class对象是否是同一个类存在两个必要条件
1、类的完整类名必须一致,包括包名。
2、加载这个类的 ClassLoader (指 ClassLoader 实例对象) 必须相同。
也就说明,在JVM中,即使这两个类对象(class对象)来源于同一个Class文件,被同一个虚拟机所加载,但只要加载他们的ClassLoader 实例对象不同,那么这两个类对象也是不相同的。
最后
到此为止,我们就把JVM的类加载子系统聊完了,如果觉得不错,转发、在看、点赞安排起来吧。
你知道的越多,你的思维越开阔。我们下期再见。
- 点赞
- 收藏
- 关注作者
评论(0)