JVM类的加载器和加载过程

之前JVM的那篇笔记实在太长了 不方便 查阅 故在复习时把每个章节拆出来独立成文

类加载器子系统作用

image-20200616112003033

  • 类加载器子系统 负责从文件系统或者网络中加载class文件 class文件在文件开头有特定的文件标识
  • ClassLoader只负责class文件的加载 ,至于他是否可以运行,则由Execution Engine 执行引擎决定
  • 加载的类信息 存放于一块称为方法区的内存空间. 方法区还会存放运行时常量池信息 可能还包括字符串 字面量和数字常量 (这部分常量信息是class文件中的常量池)

类加载器

image-20200616114826425

  1. class file存在于本地硬盘 可以理解为设计师画在纸上的模板 而最终这个模板在执行的时候是要加载到JVM中来 根据这个文件 实例化出n个一模一样的实例
  2. class file加载到JVM中 称为DNA元数据模板 放在方法区
  3. 在class文件 – >jvm –>元数据模板 过程需要一个运输工具 (类装载器)
  4. 左边为堆空间

类的加载过程

image-20200616125946258

image-20210306012032957

加载

  1. 通过一个类的全限定名获取定义此类的二进制字节流

  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

    (方法区的落地实现 jdk7以前 永久代 jdk8之后 元空间)

  3. 在内存中生成一个 代表这个类的java.lang.class对象 作为方法区这个类的各种数据的访问入口

链接

  1. 验证

目的在于确保class文件的字节流中包含的信息符合当前虚拟机要求 保证被加载类的正确性

不会危害虚拟机自身安全.

主要包括四种验证:文件格式验证.元数据验证,字节码验证,符号引用验证

  1. 准备(Prepare)

类变量分配内存并且设置该类变量的默认初始值 即零值

这里不包含final修饰的static 因为final 在编译的时候就会分配了 准备阶段会显式初始化

这里不会为 实例变量分配初始化 类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中

static变量 称为类变量 静态变量

1
private static int a= 1;//准备阶段: a=0 ----> 初始化 a=1
  1. 解析

将常量池内的符号引用转换为直接引用的过程 (符号引用就是一组符号描述所引用的目标 直接引用就是直接指向目标的指针)

解析操作往往会伴随jvm在执行初始化后才执行

解析动作主要针对类或接口 字段 类方法 接口方法 方法类型 等

初始化

  • 初始化阶段就是执行类构造器方法<clinit>()的过程

    (如果没有类变量 )

  • 此方法不需定义 是javac编译器 自动收集所有类变量的赋值动作和静态代码块中的语句合并而来

  • 构造器方法中指令按照语句在源文件中出现的顺序执行

  • <clinit>()不同于类的构造器

  • 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ClassInitTest {
private static int num = 1;

static{
num = 2;
number = 20;
System.out.println(num);
//System.out.println(number);//报错:非法的前向引用。在声明前 可以赋值 但是你不能去调用它
}

private static int number = 10; //linking之prepare: number = 0 --> initial: 20 --> 10

//之所以可以在后面声明 是因为在linking的时候已经初始化了

public static void main(String[] args) {
System.out.println(ClassInitTest.num);//2
System.out.println(ClassInitTest.number);//10
}
}

任何一个类 声明以后 内部至少存在一个类的构造器

成员变量a如果满足如下的4点,就必须在使用前必须对该成员变量进行声明

  1. 设定C为直接包含该成员变量的类或者接口
  2. 如果a出现在在C的或静态成员/非静态成员初始化 或者 C的静态或非静态代码块中
  3. 如果a不是 一个赋值不等式的左值
  4. 通过简单名称来访问

类加载器的分类

  • JVM支持两种类型的类加载器 分别为引导类加载器(Bootstrap ClassLoader)自定义类加载器(User-Defined ClassLoader)

  • 从概念上来讲 自定义类加载器 一般指的是 程序开发中 由开发人员自定义的一类类加载器 但是java虚拟机规范却没有这么定义 而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器

image-20200616145924640

Bootstrap(c和c++ 编写) 是 一类

其他是 一类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {

//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

//获取其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d

//获取其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null

//对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}

启动类加载器

  • 使用c/c++实现 嵌套在JVM内部
  • 它用来加载java的核心类库(JAVA_HOME/jre/lib/rt.jar resources/jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
  • 并不继承自java.lang.classloader 没有父加载器
  • 加载拓展类和应用程序类加载器, 并指定为他们的父类加载器
  • 出于安全考虑 Boostrap启动类加载器 只加载包名为java javax sun开头的类

扩展类加载器(Extension Classloader)

  • java编写 由sun.misc.launcher$ExtClassLoader实现

  • 派生于ClassLoader类

  • 父类加载器为启动类加载器

  • 从java.ext.dirs系统属性所指定的目录中加载类库 或从jdk的安装目录jre/lib/ext子目录下加载类库

    如果用户创建的jar放在此目录下 也会自动由扩展类加载器加载

应用程序类加载器(系统类加载器 AppClassLoader)

  • java编写 由sun.misc.launcher$AppClassLoader实现

  • 派生于ClassLoader类

  • 父类加载器为扩展类加载器

  • 它负责加载环境变量classpath或系统属性 java.class.path 指定路径下的类库

  • 该类加载是程序中默认的类加载器 一般来说 java应用的类都是由他完成加载

  • 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
//获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);//null 引导类加载器

System.out.println("***********扩展类加载器*************");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}

//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d

}

/*
**********启动类加载器**************
file:/C:/Program%20Files/Java/jdk1.8.0_241/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_241/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_241/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_241/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_241/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_241/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_241/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_241/jre/classes
null
***********扩展类加载器*************
C:\Program Files\Java\jdk1.8.0_241\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
sun.misc.Launcher$ExtClassLoader@12a3a380

Process finished with exit code 0
*/

用户自定义类加载器

为什么要自定义类加载器?

  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄露

自定义类加载器实现步骤

  1. 继承抽象类java.lang.ClassLoader类的方式 实现自己的类加载器
  2. JDK1.2之前 在自定义类加载器是 总会去继承classloader并重写loadclass()从而实现 自定义 但是1.2之后不建议用户去覆盖loadclass()方法 而是建议把自定义的类 加载逻辑写在findClass()方法中
  3. 在编写自定义类加载器时候 如果没有太复杂的需求 可以直接继承URLClassLoader类 这样可以避免自己去编写findClass()方法及其获取字节码流 的方式 使自定义类加载器编写更加简洁.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {

try {
byte[] result = getClassFromCustomPath(name);
if(result == null){
throw new FileNotFoundException();
}else{
return defineClass(name,result,0,result.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}

throw new ClassNotFoundException(name);
}

private byte[] getClassFromCustomPath(String name){
//从自定义路径中加载指定类:细节略
//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
return null;
}

public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
Class<?> clazz = Class.forName("One",true,customClassLoader);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}

关于ClassLoader

它是一个抽象类 其后所有的类加载器 都是继承自ClassLoader

image-20200616150051791

image-20200616154531479

获取ClassLoader的途径

image-20200616154658641

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
try {
//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);

//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);

} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/*
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
*/

双亲委派机制

Java虚拟机对class文件采用的是 按需加载的方式 也就是说 当需要使用该类的时候才会将他的class文件加载到内存生成class对象 而且加载某个类的class文件时,java虚拟机采用的是双亲委派模式,即把请求交给父类处理 它是一种任务委派模式

1
2
3
4
5
6
7
8
9
10
public class StringTest {

public static void main(String[] args) {
java.lang.String str = new java.lang.String();
System.out.println("hello,atguigu.com");

StringTest test = new StringTest();
System.out.println(test.getClass().getClassLoader());
}
}

image-20200616155651927

1
2
3
4
5
6
7
8
9
10
11
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
//错误: 在类 java.lang.String 中找不到 main 方法
public static void main(String[] args) {
System.out.println("hello,String");
}
}

image-20200616160315395

优势:

  • 避免类的重复加载

    通过委托可以知道上级是否已经加载过该类,如果加载过了就不再加载

  • 保护程序安全 防止核心API被随意篡改

    • 自定义类:java.lang.String
1
2
3
4
5
6
7
8
package java.lang;

public class XytStart {

public static void main(String[] args) {
System.out.println("hello!");
}
}

沙箱安全机制

自定义String类 但是在加载自定义String类的时候会率先使用引导类加载器加载.而引导类加载器在加载的过程中会先加载jdk自带的文件,报错信息说没有 main方法

就是因为加载的是rt.jar包下的String 这样可以保证对java核心源代码的保护 这就是沙箱安全机制.

Java安全模型的核心就是Java沙箱

java沙箱就是一个限制程序运行的环境 沙箱机制就是将java代码限制在虚拟机特定的运行范围内 严格限制代码对本地资源方位.沙箱主要限制系统资源访问

系统资源包括 CPU 内存 文件系统 网络

所有的java程序都可以指定沙箱,可以定制安全策略

在java中 代码分为本地代码和远程代码 本地默认可信任 远程不可信

本地代码可以访问本地一切资源

下图JDK1.0 沙箱 机制

image-20200616164837224

image-20200616165117013

image-20200616165214282

image-20200616165341499

image-20200616165916956

image-20200616170006748

其它

​ JVM必须 知道一个类型是由启动加载器加载的还是由用户类加载器加载的.如果一个类型是由用户类加载器 加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中.当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的

类的主动使用和被动使用

在JVM中 两个class对象是否为同一个类的两个必要条件:

  • 类的完整类名必须一致 包括包名
  • 加载这个类的ClassLoader(指ClassLoader实例对象) 必须相同

主动使用

  • 创建类的实例
  • 访问某个类或接口的静态变量 或者对该静态变量赋值
  • 调用类的静态方法
  • 反射
  • 初始化一个类的子类
  • java虚拟机启动时被标明为启动类的类
  • JDK7 开始提供的 动态语言支持

除了以上其中 其他类的使用方式都是 被动使用 都不会导致类的初始化