文章目录
2_类加载器子系统
2.1_类的生命周期
类的生命周期会经历如图 7 个阶段。
2.2_类的加载过程
类的加载过程有 3 个阶段:加载、连接、初始化。
-
加载:
-
通过一个类的全限定名获取定义此类的二进制字节流;
-
将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构;
-
在内存中生成一个代表这个类的
java.lang.Class
对象(反射),作为方法区这个类的各种数据的访问入口。
-
-
链接:
-
验证:
-
确保 Class 文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全;
-
主要包括四种验证,文件格式验证,源数据验证,字节码验证,符号引用验证。
-
-
准备:
-
为类变量分配内存并且设置该类变量(静态变量)的默认初始值,即零值;
-
这里不包含用
final
修饰的static
(常量),因为final
在编译时就会分配了,准备阶段会显式初始化; -
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中。
-
-
解析:
-
将常量池内的符号引用转换为直接引用的过程;
-
解析操作往往会伴随着 JVM 在执行完初始化之后再执行;
-
符号引用就是一组符号来描述所引用的目标。符号应用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄;
-
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的
CONSTANT_Class_info
、CONSTANT_Fieldref_info
、CONSTANT_Methodref_info
等。
-
-
-
初始化:
-
初始化阶段就是执行类构造器方法
<clinit>()
的过程; -
不需要定义该方法,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来;
-
构造器方法中指令按语句在源文件中出现的顺序执行;
-
<clinit>()
不同于类的构造器<init>()
; -
若该类有父类,JVM 会保证子类的
<clinit>()
执行前,父类的<clinit>()
已经执行完毕; -
虚拟机必须保证一个类的
<clinit>()
方法在多线程下被同步加锁。
-
2.3_类加载器的分类
- JVM 支持两种类型的加载器,分别为引导类加载器(
BootStrap ClassLoader
)和自定义类加载器。 - 将所有**派生于抽象类
ClassLoader
**的类加载器都划分为自定义类加载器。(如Extension Class Loader
和System Class Loader
) - 自定义类默认使用的是系统类加载器进行加载;而Java的核心类库都是使用引导类加载器来加载的。
2.3.1_虚拟机自带的加载器
-
启动类加载器(引导类加载器,
BootStrap Class Loader
):- 使用 C/C++ 语言实现,嵌套在 JVM 内部;
- 用于加载 Java 的核心类库(
JAVA_HOME/jre/lib/rt.jar/resources.jar或sun.boot.class.path
路径下的内容); - 并不继承自
java.lang.ClassLoader
,无父加载器,是包含关系; - 加载拓展类和应用程序类加载器,并指定为他们的父加载器;
- 出于安全考虑,BootStrap启动类加载器只加载包名为
java、javax、sun
等开头的类。
-
扩展类加载器(
Extension Class Loader
):- Java语言编写,由
sun.misc.Launcher$ExtClassLoader
实现; - 派生于
CLassLoader
类; - 父类加载器为启动类加载器;
- 从
java.ext.dirs
系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext
子目录(扩展目录)下加载类库。如果用户创建的 JAR 放在此目录下,也会由拓展类加载器加载。
- Java语言编写,由
-
应用程序类加载器(系统类加载器,
AppClassLoader
):- 同 ExtClassLoader 的 1、2、3;
2. 负责加载环境变量 classpath 或系统属性java.class.path
指定路径下的类库;
3. 该类加载器是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载;
4. 通过ClassLoader#getSystemClassLoader()
方法可以获取到该类加载器。
- 同 ExtClassLoader 的 1、2、3;
注:
从 JDK9 开始,平台类加载器PlatformClassLoader
取代了扩展类加载器。
public class ClassLoaderTest {
public static void main(String[] args) {
try {
System.out.println(Class.forName("java.lang.String").getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(ClassLoader.getPlatformClassLoader());
System.out.println(Thread.currentThread().getContextClassLoader());
}
}
输出:
null
jdk.internal.loader.ClassLoaders$PlatformClassLoader@48140564
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
2.3.2_用户自定义类加载器
作用:
-
隔离加载类
-
修改类加载的方式
-
拓展加载源
-
防止源码泄漏
实现步骤:
2.4_双亲委派机制
当某个类加载器需要加载某个.class
文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
举例:
自己创建一个和JDK的 String 类相同全限定类名的类(即篡改 String 类):
package java.lang;
public class String {
static {
System.out.println("自定义java.lang.String类的静态代码块");
}
public static void main(String[] args) {
System.out.println("能否打印自定义java.lang.String类的输出语句");
}
}
运行后会报错 java: 程序包已存在于另一个模块中: java.base
自定义的java.lang.String
类与java.base.java.lang.String
相冲突,因为程序优先委托给上级类加载器,委托到启动类加载器时,发现核心类库中有String类,优先执行该类,而我们自定义的String类就不会被加载。
因此它拥有以下的优点:
-
防止重复加载同一个
.class
。通过委托询问上层,加载过了,就不用再加载一遍。 -
保护程序安全,防止核心 API 被随意修改。Java的核心类库都是被启动类加载器加载,即便篡改了也不会被加载。
2.5_沙箱安全机制
2.6_补充
类的主动使用和被动使用
主动使用在类加载系统中的第三阶段initialization即初始化阶段调用了clinit()方法,而被动使用不会去调用。
主动使用,分为七种情况:
-
创建类的实例
-
访问某各类或接口的静态变量,或者对该静态变量赋值
-
调用类的静态方法
-
反射 比如Class.forName(com.dsh.jvm.xxx)
-
初始化一个类的子类
-
java虚拟机启动时被标明为启动类的类
-
JDK 7 开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
除了以上七种情况,其他使用java类的方式都被看作是对类的被动使用,都不会导致类的初始化。