感谢尚硅谷宋红康老师的JVM入门到精通课程,该套课程可以在B站查询(持续更新)!
本套教程均为我学习课程之后的学习笔记,防止遗忘巩固自己记忆。
一、类加载器的概念
是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。简单来说就是将Java类从硬盘网络等各种来源加载到jvm中的工具,就是类加载器。
- 类加载子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
- classLorder只负责对class文件的加载。
- 加载的类的信息存放于一块称为方法区的内存空间,除了类的信息外,方法区中还会存放运行时的常量池信息,还可能包括字符串字面量和数字的常量。
二、类加载的过程
类加载的过程有三步:
- 加载(Loding):将字节码文件以二进制流的方式加载到内存,形成Class对象
- 链接(Linking):验证字节码,对静态成员变量赋初值等
- 初始化(Initalition):调用初始化方法对类进行初始化
三、分开剖析类加载的过程
类加载的第一步:加载
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java. lang . Class对象,作为方法区这个类的各种数据的访问入口
可以用下图作为参照理解:
被加载的字节码文件可以以多种方式加载(本篇只列举几个,详细内容请观看视频)
- 从本地系统中直接加载(最为常用的)
- 通过网络获取,典型场景: Web Applet
- 从zip压缩包中读取,成为日后jar、war格式的基础
- …
类加载的第二步:链接
链接阶段又分为三个部分:验证(Verify)准备(Prepare)解析(Reslove)
在验证阶段:
- 确保Class文件的字节流是否符合虚拟机的要求,保证类的正确性,不会危害到虚拟机的自身安全。
- 有四种验证方式:文件格式验证,元数据验证,字节码验证,符号引用验证。
- 在使用二进制查看class文件时,开头都是CA FE BA BE即为标准。
在准备阶段:
- 为变量分配内存并设定初始值,其中static被final修饰,在编译时就会分配,准备阶段显示初始化
- 不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。若static被final修饰,则它就不是变量而是变化为一个常量。
在解析阶段:
- 将常量池中的符号引用转换为直接引用的过程。(利用javap -v 文件名的方法可以看到常量池的符号)
- 通常解析在初始化之后完成(后文会继续详解)
- 符号引用就是一组符号来描述所引用的目标。
- 直接引用就是直接指向目标的指针,相对偏移量或者一个间接定位到目标的句柄。
类加载的第三步:初始化
- 执行类构造器方法
<clinit>()
的过程,他不是定义的,而是自动生成的,是javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的赋值语句合并而来的 - 在jclasslib中可以查看
<clinit>()
的用法。(第一个为<clinit>()
正确方法,第二个为错误方法。)
public class ClassInitTest {
private static int num = 1;
static {
num = 2;
number = 20;
}
private static int number = 10;//linking的准备阶段:number = 0→initial:20→10
public static void main(String[] args) {
System.out.println(ClassInitTest.num);//2
System.out.println(ClassInitTest.number);//10
}
}
public class ClinitTest {
private int a = 1;
public static void main(String[] args) {
int b= 2;
}
}
不存在<clinit>()
- 构造器中的方法中指令按语句在源文件中出现的顺序执行
- 任何一个类声明后,其内部一定有一个类的构造器。这个构造器可以自己定义,也可以由系统默认。
- 该类若具有父类,则JVM会在保证子类
<clinit>()
执行前,父类的<clinit>()
已经执行完毕。 - 虚拟机中必须要保证一个类
<clinit>()
方法在多线程下同步加锁。
四、类的加载器的分类
1. 启动类加载器(BootStrapClassLoader)
- 由C语言进行编写,负责加载系统核心类($JAVA_HOME中jre/lib/rt.jar和resource.jar下的类,或者sun.boot.class.path下的类),是JVM的一部分
- 他并不继承与ClassLoader,也没有上层类加载器
- 负责加载扩展类加载器和应用类加载器(这两个类加载器也是核心类)
- 出于安全考虑,Bootstrap启动类 加载器只加载包名为java、javax、sun等开头的类
2、扩展类加载器(ExtensionsClassLoader)
- 由Java语言编写,负责加载JRE的扩展目录。
- 如果将我们自己编写的类放在指定补录下,也可以被扩展类加载器所加载
- 派生于ClassLoader类
- 父类加载器为启动类加载器
3、系统类加载器/应用类加载器(SystemClassLoader/APPClassLoader)
- 负责加载用户自定义的类(java.Class.Path类路径下的类)
- 派生于Classloader类
- 该类是程序中默认的类加载器,负责加载程序中的基础类
- 用户自定义类加载器:如果我们有自定义类加载器的需求,可以继承ClassLoader抽象类,自定义我们的类加载器。
各种加载器之间的关系:
- 这几种类加载器之前并不是父子继承关系,而是包含关系,可以理解为上下级关系
- 可以使用ClassLoader的getParent()方法获取该加载器的上层加载器
代码段如下:
public static void main(String[] args) {
ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
//获取系统类加载器
System.out.println(sysClassLoader);
ClassLoader extClassLoader = sysClassLoader.getParent();
//获取扩展类加载器
System.out.println(extClassLoader);
ClassLoader bsClassLoader = extClassLoader.getParent();
//获取引导类加载器
System.out.println(bsClassLoader);
}
扫描二维码关注公众号,回复:
9348802 查看本文章
![](/qrcode.jpg)
五、双亲委派机制
- 如果两个类类名包名完全相同,程序依旧不会报错,而是正常运行,并且正常输出系统核心类库中的Date类。这是因为,JVM加载类时,遵循双亲委派机制,所以只会加载系统中的类。
- 如果自定义的类如果包名和类名与系统类完全相同,此类则永远也不会被加载,相当于作废,这个机制可以保护系统核心类库不被私自篡改,所以这种机制也叫沙箱安全机制
- 自定义类不要以java/javax/sun等等特殊包名开头,否则系统会默认为API库中的数据导致无法输出。