文章目录
7. 虚拟机类加载机制
7.2 类加载的时机
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期会经历如上7个阶段
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,它们会按照这个顺序开始,但不一定是这个顺序结束,因为这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段
初始化的时机
加载什么时候开始,没有严格规定
关于初始化,规定了以下六种情况必须立即对类进行“初始化”(加载、验证、准备自然需要在此之前开始)
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,相应的Java代码场景
(1)使用new关键字实例化对象(非数组)的时候
(2)读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
(3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
(4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
(5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
(6)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
主动引用和被动引用
有且只有上述六种情况会触发初始化,称为对一个类型进行主动引用;其他的都是被动引用
被动引用
-
示例1:通过子类引用父类的静态字段,不会导致子类初始化
class SuperClass{ static { System.out.println("SuperClass init!"); } public static int value = 123; } class SubClass extends SuperClass{ static { System.out.println("SubClass init!"); } } public class Test { public static void main(String[] args){ System.out.println(SubClass.value); } } //输出:SuperClass init! //输出:123
虽然不会触发子类的初始化,但HotSpot触发了子类的加载过程(《Java虚拟机规范》并未对此做要求)
-XX:+TraceClassLoading //查看类加载情况
扫描二维码关注公众号,回复: 14561050 查看本文章 -
示例2:通过数组定义来引用类,不会触发此类的初始化
public class Test { public static void main(String[] args){ SuperClass[] sca = new SuperClass[10]; } } //无输出
且此时只有SuperClass被加载了,SubClass没有被加载
-
示例3:常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
class ConstClass{ static { System.out.println("ConstClass init!"); } public static final String HW = "hello world"; } public class Test { public static void main(String[] args){ System.out.println(ConstClass.HW); } } //输出:hello world
编译阶段,常量值“hello world”被直接存储在Test类的常量池中了,此后对ConstClass.HW的引用都变成了对自己Test.HW的引用
Test类的Class文件中并没有ConstClass类的符号引用入口,这两个类在编译成Class文件之后就没有关系了
ConstClass类甚至没有被加载
接口的初始化
接口中不能使用“static{}”语句块,但编译器仍然会为接口生成“clinit()”类构造器,用于初始化接口中所定义的成员变量。
接口与类真正有所区别的是前面的第三种初始化时机:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化
7.3 类加载的过程
加载
Java虚拟机的加载工作:
-
通过一个类的全限定名来获取定义此类的二进制字节流
注意这里是全限定名,包括包名,这也是为什么直接运行一个class文件有时会提示找不到文件,因为需要在对应的包下运行
没有要求这个二进制字节流从哪来的,这也是反射产生的基础:反射可以直接动态生成一个Class文件
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
-
在
内存
中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
类的加载:
加载阶段既可以使用Java虚拟机里内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成
开发人员通过定义自己的类加载器去控制字节流的获取方式
(重写一个类加载器的findClass()或loadClass()方法),实现根据自己的想法来赋予应用程序获取运行代码的动态性
数组的加载:
数组类本身不通过类加载器创建
,它是由Java虚拟机直接在内存中动态构造出来的
数组的元素类型:ElementType,指的是数组去掉所有维度的类型
数组的组件类型:Component Type,指的是数组去掉一个维度的类型
- 数组的组件类型是引用类型:递归采用类加载去加载这个组件类型,数组C将被标识在加载该组件类型的
类加载器的类名称空间
上
(一个类型必须与类加载器一起确定唯一性) - 数组的组件类型不是引用类型:Java虚拟机将会把数组C标记为与引导类加载器关联
- 数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的可访问性将默认为public,可被所有的类和接口访问到
类的存储位置和访问:
加载阶段结束后,Java虚拟机外部的二进制字节流
就按照虚拟机所设定的格式存储在方法区
之中了
类型数据妥善安置在方法区之后,会在Java堆内存
中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的类型数据的外部接口
在加载结束之前,连接可能就已经开始了,边加载边连接
类加载之后,方法区就有了该类的信息
连接之验证
验证的原因:
Java编译器虽然能检查错误,但有的class文件并非由编译器生成
这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,运行后不会危害安全
验证内容:
验证阶段的工作量在虚拟机的类加载过程中占了相当大的比重
,大体上分为文件格式验证、元数据验证、字节码验证和符号引用验证
-
文件格式验证
验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,例如魔数是否为0xCAFEBABE只有这一阶段是基于二进制字节流进行的,只有通过了这个阶段的验证之后,这段字节流才被允许进入Java虚拟机内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了
-
元数据验证
是对类的元数据信息(个人理解为就是Class信息)进行语义校验
,保证不存在与《Java语言规范》
定义相悖的元数据信息
例如:这个类是否有父类、这个类的父类是否继承了不允许被继承的类等等 -
字节码验证:最复杂的阶段,耗时长
通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证之后,就要对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为
例如:数据类型匹配、指令跳转等- 字节码验证的优化:
JDK 6之后将一部分字节码验证工作交给编译器去做。在方法体Code属性的属性表中新增加了一项名为“StackMapTable”的新属性,描述了方法体所有的基本块开始时本地变量表和操作栈应有的状态。在字节码验证期间,Java虚拟机就不需要根据程序推导这些状态的合法性,只需要检查StackMapTable属性中的记录是否合法即可,即将类型推导转变为类型检查
但StackMapTable属性也可能存在错误或被篡改,带来一定的安全风险
- 字节码验证的优化:
-
符号引用验证
发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生,目的就是确保解析行为能正常执行
验证内存:是否能找符号对应的类、方法、字段,是否有访问权限等等
验证失败将会抛出一个java.lang.IncompatibleClassChangeError的子类异常,典型的如:java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等
连接之准备
准备阶段的工作:
为类变量(即静态变量,不包括实例变量)分配内存并设置类变量初始值的阶段
这里的初始值是默认初始值,而不是用户规定初始值
public static int value = 123;
//准备阶段之后value是0,而不是123
关于分配内存:概念上而言,这些变量在方法区上分配内存。在JDK 7及之前,HotSpot使用永久代来实现方法区,此时方法区和堆是分开的区域;但在JDK 8及之后,方法区被放在了堆中,所以实际上是在堆中分配内存,但从逻辑上仍称为在方法区中分配
初始值:
特殊情况下的初始值:
如果类字段的字段属性表中存在ConstantValue属性
,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值
public static final int value = 123;
//编译时将会为value生成ConstantValue属性,准备阶段之后value是123
连接之解析
解析阶段的工作:
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用:符号可以是任何形式的字面量,引用的目标并不一定是已经加载到虚拟机内存当中的内容
- 直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄
解析的时机:
并未明确规定,但要求在操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析
虚拟机自行选择实现:(1)类被加载器加载时就解析;(2)一个符号引用将要被使用前才去解析
多次解析:
对同一个符号引用进行多次解析请求很常见,但除了invokedynamic指令以外,虚拟机实现可以对第一次解析的结果进行缓存
原则:如果一个符号引用之前已经被成功解析过,那么后续的引用解析请求就应当一直能够成功;反之,如果第一次解析失败了,其他指令对这个符号的解析请求也应该收到相同的异常,哪怕这个请求的符号在后来已成功加载进Java虚拟机内存之中
invokedynamic指令:
用于动态语言支持,它对应的引用称为“动态调用点限定符”,含义是必须等到程序实际运行到这条指令时,解析动作才能进行。其余的更多是提前解析
对于invokedynamic指令,之前的解析结果不影响其他invokedynamic指令
不同符号引用的解析:
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这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种常量类型
1. 类或接口的解析
假定:正在执行的类为D,它有一个从未解析过的符号引用N,这个N需要被解析成为类或接口C的直接引用
- C非数组类型:D的类加载器,通过N的全限定名,加载类C。其中,类C又可能触发其他类的加载
- C是数组类型,并且数组的元素类型为对象:按照第一点规则加载数组元素类型,然后虚拟机再生成一个代表该数组维度和元素的数组对象
- 确定D是否具有对C的访问权限,即满足下列条件之一:(1)被访问类C是public的,并且与访问类D处于同一个模块;(2)被访问类C是public的,不与访问类D处于同一个模块,但C允许被D访问;(3)被访问类C不是public的,但是它与访问类D处于同一个包中
2. 字段解析
(符号引用被放到了常量池中)
- 首先对CONSTANT_Class_info符号引用进行解析,得到字段所属的类或接口的符号引用,然后解析这个类或接口(用C表示)
- 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
- 否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口
- 否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类
- 否则,查找失败,抛出java.lang.NoSuchFieldError异常
成功查找之后还要对字段进行权限检查,如果不具备访问权限,将抛出java.lang.IllegalAccessError
- 同名变量将报错
public class Test {
interface Interface0 {
int A = 0;
}
interface Interface1 extends Interface0 {
int A = 1;
}
interface Interface2 {
int A = 2;
}
static class Parent implements Interface1 {
public static int A = 3;
}
static class Sub extends Parent implements Interface2 {
//public static int A = 4;
//注释掉后报错:pack.Test.Parent 中的变量 A 和 pack.Test.Interface2 中的变量 A 都匹配
}
public static void main(String[] args) {
System.out.println(Sub.A);
}
}
static class Sub extends Parent implements Interface2 //编译报错
static class Sub extends Parent implements Interface1 //编译通过,运行报错
static class Sub extends Parent implements Interface0 //编译通过,运行报错
3. 方法解析
- 对CONSTANT_Class_info符号引用进行解析,得到方法所属的类或接口C
- Class文件格式中类的方法和接口的方法符号引用的常量类型定义是分开的,如果在类方法表中发现C是接口,则抛出java.lang.IncompatibleClassChangeError异常
- 在类C中查找,找不到去类C的父类中查找
- 还找不到去类C的接口找,找到就说明是个抽象方法,抛出java.lang.AbstractMethodError异常
- 最终都没找到,抛出java.lang.NoSuchMethodError
查找成功返回直接引用,查找失败抛出java.lang.IllegalAccessError
4. 接口方法解析
- 对CONSTANT_Class_info符号引用进行解析,得到方法所属的类或接口C
- 如果在接口方法表中发现C是个类而不是接口,抛出java.lang.IncompatibleClassChangeError异常
- 在接口C中查找
- 在接口C的父接口中查找。如果有多个父接口,并存在多个匹配方法,将从中选择一个返回直接引用(没有规定返回哪一个,但一般这种情况会在编译阶段就被阻止,不会出现多个匹配)
失败抛出java.lang.NoSuchMethodError,JDK 9之前接口都是public的,没有java.lang.IllegalAccessError,但这之后开始出现私有
初始化
clinit()方法
·<clinit> //初始化阶段就是执行类构造器<clinit>()方法的过程
编译器自动生成,由编译器自动收集类中的所有类变量的赋值动作
和静态语句块(static{}块)中的语句
合并产生的
public class Test {
static {
i = 0; // 给变量复制可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
init和clinit
clinit不需要显式地调用父类构造器,Java虚拟机会保证在子类的()方法执行前,父类的()方法已经执行完毕。因此在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object
init是示例构造器,在new对象时才会被触发,而clinit是类构造器,在类初始化时触发
clinit()并不是必需的,没有静态语句块,也没有赋值操作时,就不会生成它
clinit的加锁同步
如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的()方法,其他线程都需要阻塞等待
问题:如果一个类的()方法耗时很长,就可能造成多个进程阻塞
-
示例:线程阻塞
public class Test { static class DeadLoopClass { static { // 如果不加上这个if语句,编译器将提示“Initializer does not complete normally”并拒绝编译 if (true) { System.out.println(Thread.currentThread() + "init DeadLoopClass"); while (true) { } } } } public static void main(String[] args) { Runnable script = new Runnable() { public void run() { System.out.println(Thread.currentThread() + "start"); DeadLoopClass dlc = new DeadLoopClass(); System.out.println(Thread.currentThread() + " run over"); } }; Thread thread1 = new Thread(script); Thread thread2 = new Thread(script); thread1.start(); thread2.start(); } }
7.4 类加载器
类与类加载器
对于任意一个类,都必须由加载它的类加载器
和这个类本身
一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间
使用不同的类加载器将会对类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof关键字的结果有影响
/**
* 类加载器与instanceof关键字演示
* 同一个类由不同类加载器加载,instanceof结果为false
*/
package pack;
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
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(name);
}
}
};
Object obj = myLoader.loadClass("pack.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof pack.ClassLoaderTest);
}
}
双亲委派模型
两种类加载器:站在Java虚拟机的角度来看,只存在两种不同的类加载器
- 启动类加载器(BootstrapClassLoader):使用C++语言实现,是虚拟机自身的一部分
- 其他所有的类加载器:Java语言实现,虚拟机外部,全都继承自抽象类java.lang.ClassLoader
类加载架构:三层类加载器、双亲委派
三层类加载器:
-
启动类加载器(Bootstrap Class Loader):
(1)加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的类库
(2)无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,使用null代替即可//ClassLoader.getClassLoader()方法的代码片段 public ClassLoader getClassLoader() { ClassLoader cl = getClassLoader0(); if (cl == null) return null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { ClassLoader ccl = ClassLoader.getCallerClassLoader(); if (ccl != null && ccl != cl && !cl.isAncestor(ccl)) { sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION); } } return cl; }
-
扩展类加载器(Extension Class Loader)
在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现的。
负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库
加载具有通用性的类库,可以为开发者直接使用 -
应用程序类加载器(Application Class Loader):
由sun.misc.Launcher$AppClassLoader来实现,是ClassLoader类中的getSystemClassLoader()方法的返回值,又称系统类加载器负责加载用户类路径上所有的类库,可以为开发者直接使用
如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
双亲委派模型:
各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器
不过类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码
-
如果一个类加载器收到了类加载的请求,首先把这个请求委派给父类加载器去完成,
因此所有的加载请求最终都应该传送到最顶层的启动类加载器中
。只有当父加载器无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载 -
优势:类随着它的类加载器一起具备了一种带有优先级的层次关系。例如加载Object类,无论哪一个类加载器去加载它,最终都是启动类加载器去加载,从而保证了它始终是同一个类(类+类加载器决定唯一性)
-
实现双亲委派的代码很短,且全部集中在java.lang.ClassLoader的loadClass()方法之中
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先,检查请求的类是否已经被加载过了 Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果父类加载器抛出ClassNotFoundException // 说明父类加载器无法完成加载请求 } if (c == null) { // 在父类加载器无法加载时 // 再调用本身的findClass方法来进行类加载 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } //先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的loadClass()方法 //若父加载器为空则默认使用启动类加载器作为父加载器 //假如父类加载器加载失败,抛出ClassNotFoundException异常,才调用自己的findClass()方法尝试进行加载。
破坏双亲委派模型(略)
完全弄懂了OSGi的实现,就算是掌握了类加载器的精粹
7.5 Java模块化系统
在JDK 9中引入的Java模块化系统(Java Platform Module System,JPMS)
目的:实现模块化的关键目标——可配置的封装隔离机制
对类加载架构也做出了相应的变动调整,才使模块化系统得以顺利地运作
Java模块:
模块是一个或多个 Package 组成的集合,一个模块就是一个 jar 文件,相比于普通的 jar 文件,模块的根目录会存在一个 module-info.class 文件,用来描述模块的基本信息
(图来自:https://blog.csdn.net/m0_59924193/article/details/122906843)
Java模块的定义:除了代码外,还包括
- 依赖其他模块的列表
- 导出的包列表,即其他模块可以使用的列表
- 开放的包列表,即其他模块可反射访问模块的列表
- 使用的服务列表
- 提供服务的实现列表
// 模块名称:ModuleA
module ModuleA {
// 依赖哪些模块
requires org.demo.requireA;
requires org.demo.requireB;
// 导出哪些包(其它模块可以import使用)
exports com.demo.exportA;
exports com.demo.exportB;
// 开放哪些包(其它模块可以通过反射使用)
opens com.demo.openA;
opens com.demo.openB;
// 使用哪些服务
// 使用 org.demo.exportA.export.InterfaceA 服务接口
uses InterfaceA;
// 提供哪些服务(一次可以声明多个服务,用英文逗号分隔)
// provides A with B 可以理解为:为接口 A 提供实现类 B。
// 为接口 org.demo.exportA.export.IntefaceA(模块B)提供实现类 com.demo.exportA.export.ExportA(模块A)
provides InterfaceA with ExportA;
}
可配置的封装隔离机制:
- (1)解决了基于类路径来查找依赖的可靠性问题
在JDK 9之前,如果类路径中缺失了运行时依赖的类型,只能等到运行时抛出异常
引入模块化封装后,虚拟机就可以根据模块定义中设置好的依赖关系,提前检查是否有缺失 - (2)解决了原来类路径上跨JAR文件的public类型的可访问性问题
JDK 9之前,public类型可以被任意程序在任意地方访问
模块提供了更精细的可访问性控制,必须明确声明其中哪一些public的类型可以被其他哪一些模块访问,这种访问控制也主要是在类加载过程中完成的
模块的兼容性
为了兼容传统的类路径查找机制,JDK 9提出了“模块路径”的概念:
(1)放在类路径上的JAR文件,无论是否模块化都当作是传统的JAR包来对待;
(2)放在模块路径上的JAR文件,即使没有使用JMOD后缀,甚至不包含module-info.class文件,它也会被当作一个模块来对待
- JAR文件在类路径的访问规则
所有类路径下的JAR文件及其他资源文件,都被视为自动打包在一个匿名模块(Unnamed Module)里
这个匿名模块几乎是没有任何隔离的,它可以看到和使用类路径上所有的包、JDK系统模块中所有的导出包,以及模块路径上所有模块中导出的包 - 模块在模块路径的访问规则
模块路径下的具名模块(Named Module)只能访问到它依赖定义中列明依赖的模块和包,匿名模块里所有的内容对具名模块来说都是不可见的 - JAR文件在模块路径的访问规则
如果把一个传统的、不包含模块定义的JAR文件放置到模块路径中,它就会变成一个自动模块(Automatic Module)
尽管不包含module-info.class,但自动模块将默认依赖于整个模块路径中的所有模块,因此可以访问到所有模块导出的包,自动模块也默认导出自己所有的包
模块化下的类加载器
-
扩展类加载器被平台类加载器取代
(1)整个JDK都基于模块化进行构建(原来的rt.jar和tools.jar被拆分成数十个JMOD文件),已天然地满足了可扩展的需求,<JAVA_HOME>\lib\ext目录无须再保留
(2)类似地,在新版的JDK中也取消了<JAVA_HOME>\jre目录,因为随时可以组合构建出程序运行所需的JRE来,譬如假设我们只使用java.base模块中的类型,那么随时可以通过以下命令打包出一个“JRE”:jlink -p $JAVA_HOME/jmods --add-modules java.base --output jre
如果你需要编写java程序,需要安装JDK。如果你需要运行java程序,只需要安装JRE就可以了
-
平台类加载器和应用程序类加载器都不再派生自java.net.URLClassLoader
注意:尽管JDK 9之后有“BootClassLoader”存在,但为了保持兼容,所有在获取启动类加载器的场景(譬如Object.class.getClassLoader())中仍然会返回null来代替,而不会得到BootClassLoader的实例
-
委派关系发生变化
当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载
三个类加载器各自加载哪些模板是有规定的