④JVM 类加载机制
④JVM 类加载机制
学习核心
- 类的生命周期
- 掌握类的生命周期核心(每个节点的作用)
- 结合案例理解类的生命周期构建流程
- 自定义类加载器(自定义类加载器实现、场景应用、常见自定义加载器(一些破坏双亲委派的场景))
- 类加载器和类加载机制
- 类加载器(分类)
- 双亲委派机制
- JDK9之后类的加载委派关系
- JVM优化-降低类加载开销
- 类加载机制之开发常见实用技巧
学习资料
类的生命周期
java指令的使用
public class Hello {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
首先确保JDK环境的安装配置
javac
:编译文件,生成.class
文件
javap
:反编译文件,解析.class
文件
此处需要注意区分idea中项目引用JDK版本和系统默认配置JDK版本,这也是程序开发过程中经常遇到的问题(例如使用低版本JDK加载高版本JDK导致的一些兼容问题,或者是开发环境切换是JDK版本没有统一等)
- idea中配置JDK版本是针对当前工程引用所设定的(idea运行项目所引用的是idea针对当前工程生效的JDK版本)
- 系统环境配置JDK(例如windows)是针对系统运行环境所设定的,它可以对所有工程生效(通过idea的terminal工具栏进入的实际上是CMD窗口,所引用的JDK即系统环境)
# 编译
javac Hello.java
# 反编译
javap -v -p Hello.class
# 执行
java Hello
常见错误:运行提示【错误: 找不到或无法加载主类】
# 编译之后执行java Hello提示【错误: 找不到或无法加载主类】
// 方案1:java Hello 无效
// 方案2:java com.noob.jvm.Hello(加上类的全限定名称,无效)
// 方案3:java环境配置(确认)
# 解决方案:删除Hello.java文件中的package定义,重新编译执行
javac Hello.java
java Hello
原因分析:环境变量classpath配置问题,可参考解决方案,或者参考上述,去掉package。
确认当前目录是否与类文件所在的目录相同,如果不同,可以使用 -classpath 或 -cp 参数来指定当前类路径
jar
:jar打开之后实际上是.class
文件+资源文件的组合,可以人通过jar
指令将Hello.class和相关依赖文件转化为jar
- 将 class 文件和 java 源文件归档到一个名为 hello.jar 的档案中:
jar cvf hello.jar Hello.class Hello.java
- 通过
e
选项指定 jar 的启动类Hello
:jar cvfe hello.jar Hello Hello.class Hello.java
# 打包jar
jar cvf hello.jar .\Hello.class .\Hello.java
// output
已添加清单
正在添加: Hello.class(输入 = 415) (输出 = 285)(压缩了 31%)
正在添加: Hello.java(输入 = 122) (输出 = 98)(压缩了 19%)
# 运行jar
java -jar hello.jar
// output
hello.jar中没有主清单属性(检查META-INF/MANIFEST.MF)
# 提示没有主清单属性,则在打包JAR文件时,创建META-INF/MANIFEST.MF,使用命令设置主类
jar cfm hello.jar META-INF/MANIFEST.MF Hello.class
调整:使用idea提供的方式进行打包:(如果没有META-INF文件夹需要手动构建,因为其需要自动构建MANIFEST.MF配置信息)
执行指令:java -jar JavaBase.jar
就会看到其执行了Hello
类的生命周期
类的生命周期包括:加载 =》链接 =》初始化 =》使用 =》卸载
其中类的加载过程包括:加载(loading) =》连接(Linking)(包括验证(Verification)、准备(Preparation)、解析(Resolution)) =》初始化(initialization),连接被细分为3个步骤
1.加载
加载阶段:根据class的完全限定名获取相应的二进制文件,如果找不到二进制内容则抛出NoClassDefFound
错误
步骤“加载”(Loading)阶段需要完成三件事:
【1】通过一个类的全限定名来获取定义此类的二进制字节流
【2】将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
【3】在内存中生成一个代表这个类的java.lang.Class
对象,作为方法区这个类的各种数据的访问入口
类加载器:加载阶段是开发人员可控性最强的阶段,类加载器可以是系统系统的,也可以是自定义的
加载方式:类的二进制字节流并没有限定说必须从Class文件获取,其他获取的渠道举例:
- 从本地文件系统加载
- 从数据库中获取
- 从zip、jar等文件中获取
- 从网络下载等
2.验证
验证阶段:按照虚拟机要求验证字节流的合法性
验证是连接阶段的第一步,验证的主要目的就是按照虚拟机的要求去检查Class字节流,确保这个字节流是符合要求的,不会有安全性问题等。
验证需要完成如下工作:
- 文件格式验证:例如是否以魔数0xCAFEBABE开头;版本号等能否被虚拟机执行
- 元数据验证:进行语义分析,确保符合Java语言规范。例如这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
- 符号引用验证:符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验 例如:根据符号引用描述的名字能否找到对应的类;或者符号引用中的类、字段、方法的可访问性(
private、protected、public、<package>
)是否可被当前类访问等
3.准备
准备阶段:为类的静态变量(Static变量)分配内存(区分是否用final修饰),并将其初始化为默认值(0,0L,null,false等这种)
注意点:
- 准备阶段只给类变量分配内存,不会给实例变量分配内存
- 准备阶段正常只会赋零值,准备阶段后value=0(例如public static int value = 123;)
- 如果是加了final修饰则会在准备阶段会直接赋初始值(例如public static final int value = 123; 准备阶段后value为123)
4.解析
解析阶段:将常量池内的符号引用替换为直接引用的过程。包括(类或接口解析;字段解析;方法解析;接口方法解析)
符号引用:符号引用以一组符号来描述所引用的目标,与虚拟机实现的内存布局无关,各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中
直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者是一个能 间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。
5.初始化
初始化是为类的静态变量赋予正确的初始值(前面准备阶段是为变量赋零值),JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
- 方式1:声明类变量时指定初始值
public static int value = 123;
- 方式2:使用静态代码块为类变量指定初始值
public static int value;
static{
value = 123;
}
初始化步骤
- 如果这个类还没有被加载和连接,则程序先加载并连接该类
- 如果该类的直接父类还没有被初始化,则先初始化其直接父类
- 如果类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机
类初始化时机: 只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
- 调用类的静态方法
- 访问某个类或接口的静态变量,或者对该静态变量赋值初始化某个类的子类,则其父类也会被初始化
- 创建类的实例:new的方式
- 反射(如
Class.forName("com.noob.jvm.Test")
) - Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
类加载器和类加载机制
1.类加载器
在Java程序中,对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。如果一个类被两个不同的加载器加载,即使来源是同一个Class文件,那这也是两个不同的类,主要会体现在,Class对象的equals()方法,isinstance()方法的返回结果,以及使用instance-of关键字做对象所属关系判定等各种情况。
如何判断两个类是否相等?
任意一个类都必须由类加载器和这个类本身共同确立其在虚拟机中的唯一性。
两个类只有由同一类加载器加载才有比较意义,否则即使两个类来源于同一个 class 文件,被同一个JVM加载只要类加载器不同,这两个类就必定不相等。
自定义类加载器
自定义类加载器场景
- 实现类似进程内隔离,类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果
- 应用需要从不同的数据源获取类定义信息,例如网络数据源,而不是本地文件系统
- 或者是需要自己操纵字节码,动态修改或者生成类型
案例分析(JDK1.8):自定义类加载器
1)自定义一个类加载器
2)利用自定义类加载器加载MyUser类,并生成一个对象实例selfLoadUser
3)判断selfLoadUser 是否为MyUser类的实例此处的MyUser是系统加载器加载的)
package com.noob.classloader;
public class MyUser {
private String name;
}
package com.noob.classloader;
import java.io.IOException;
import java.io.InputStream;
// 类加载器Demo
public class ClassLoaderDemo {
public static void main(String[] args) throws Exception {
// 1.自定义类加载器
ClassLoader classLoader = 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,e);
}
}
};
// 2.利用自定义类加载器加载MyUser类,生成一个对象实例selfLoadUser
// Object selfLoadUser = classLoader.loadClass("MyUser").newInstance(); // MyUser (wrong name: com/noob/classloader/MyUser)
Object selfLoadUser = classLoader.loadClass("com.noob.classloader.MyUser").newInstance();
System.out.println(selfLoadUser.getClass());
// 3.判断selfLoadUser是否为MyUser的实例(此处的MyUser是系统加载器加载的)
System.out.println(selfLoadUser instanceof MyUser);
}
}
//output
class com.noob.classloader.MyUser
false
此处需要关注的是,虽然是同一个MyUser文件,但是通过不同的类加载器加载之后会被看成不同的类
- 示例结果:虽然selfLoadUser的类名是com.noob.classloader.MyUser,但是selfLoadUser却被认为不是User类的实例
- 结果分析:虽然是同一个MyUser,但是是通过不同的类加载器加载的,步骤2中通过自定义类加载创建实例selfLoadUser,而步骤3中判定的MyUser类则是使用默认的类加载器加载出来的,因此在执行判定的时候selfLoadUser并不是该类的实例
结合上述案例,自定义类加载器的构建核心说明如下:自定义类加载器用例参考
- 获取字节码
- 通过指定名称,找到其二进制实现(即自定义类加载器的定制部分):例如在特定数据源根据名称获取、修改或者生成字节码
- 字节码 =》Class对象
- 根据字节码创建Class对象并完成类加载过程(二进制信息 =》Class对象转化依赖于defineClass方法(无需额外实现,直接调用该final方法即可)),获取到Class对象后完成类加载过程
自定义类加载器场景应用:二进制文件加密
使用自定义类加载器来加载其他格式的类,对加载方式、加载数据的格式进行自定义处理,只要能通过 classloader 返回一个 Class 实例即可,大大增强了加载器灵活性。例如实现一个可以用来处理简单加密的字节码的类加载器,用来保护class 字节码文件不被使用者直接拿来破解。
public class Hello {
static {
System.out.println("Hello Class Initialized!");
}
}
例如将上述文件编译生成.class
文件,然后对这个二进制文件进行加密(例如使用jdk自带的Base64算法,将字节码加密成一个文本信息),自定义类加载器HelloClassLoader则通过继承ClassLoader类重写findClass方法,将这段加密后的Base64文本字符串给还原出来,并执行相应逻辑(可以看到此处只需要拿到加密后的的二进制文件,而不关心原二进制文件)
// 自定义类加载器(对二进制字节码文件进行加密)
public class HelloClassLoader extends ClassLoader {
public static void main(String[] args) {
try {
new HelloClassLoader().findClass("jvm.Hello").newInstance(); // 加载并初始化Hello类
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String helloBase64 = "yv66vgAAADQAHwoABgARCQASABMIABQKABUAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2N" +
"hbFZhcmlhYmxlVGFibGUBAAR0aGlzAQALTGp2bS9IZWxsbzsBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAApIZWxsby5qYXZhDAAHAAgHABkMABoAGwEAGEhlb" +
"GxvIENsYXNzIEluaXRpYWxpemVkIQcAHAwAHQAeAQAJanZtL0hlbGxvAQAQamF2YS9sYW5nL09iamVjdAEAEGphdmEvbGFuZy9TeXN0ZW0BAANvdXQBABVMamF2" +
"YS9pby9QcmludFN0cmVhbTsBABNqYXZhL2lvL1ByaW50U3RyZWFtAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL1N0cmluZzspVgAhAAUABgAAAAAAAgABAAcACA" +
"ABAAkAAAAvAAEAAQAAAAUqtwABsQAAAAIACgAAAAYAAQAAAAMACwAAAAwAAQAAAAUADAANAAAACAAOAAgAAQAJAAAAJQACAAAAAAAJsgACEgO2AASxAAAAAQAK" +
"AAAACgACAAAABgAIAAcAAQAPAAAAAgAQ";
byte[] bytes = decode(helloBase64);
return defineClass(name,bytes,0,bytes.length);
}
public byte[] decode(String base64){
return Base64.getDecoder().decode(base64);
}
}
// output
Hello Class Initialized!
两个没有关系的自定义类加载器之间加载的类是不共享的(只共享父类加载器,兄弟之间不共享),这样就可以实现不同的类型沙箱的隔离性。可以用多个类加载器,各自加载同一个类的不同版本,相互之间不影响彼此,从而在这个基础上可以实现类的动态加载卸载、热插拔的插件机制等,具体信息可以参考OSGi等模块化技术
自定义类加载器案例(打破双亲委派机制)
(1)案例1:tomcat
tomcat 通过 war 包进行应用的发布,它其实是违反了双亲委派机制原则的
对于一些需要加载的非基础类,会由一个叫作 WebAppClassLoader 的类加载器优先加载。等它加载不到的时候,再交给上层的 ClassLoader 进行加载。这个加载器用来隔绝不同应用的.class 文件,例如两个应用,可能会依赖同一个第三方的不同版本,它们是相互没有影响的。
如何在同一个 JVM 里,运行着不兼容的两个版本,当然是需要自定义加载器才能完成的事。
tomcat 是怎么打破双亲委派机制的呢?:结合图示分析,WebAppClassLoader加载自己目录下的 .class 文件,并不会传递给父类的加载器。但是,它可以使用 SharedClassLoader 所加载的类,实现了共享和分离的功能(从应用上理解,tomcat中可以设定一个公共的区域存放供每个应用共享的jar包,每个应用也可引用自己目录下的jar)
但是如果是自己写一个 ArrayList,放在应用目录里,tomcat 依然不会加载。它只是自定义的加载器顺序不同,但对于顶层来说,还是一样的。
(2)案例2:SPI机制
SPI机制:动态加载自定义类实现(Service Provider Interface),通过在 META-INF/services 目录下,创建一个以接口全限定名为命名的文件(内容为实现类的全限定名),即可自动加载
DriverManager 类和 ServiceLoader 类都是属于 rt.jar 的,它们的类加载器是 Bootstrap ClassLoader(最上层)。而具体的数据库驱动,却属于业务代码,这个启动类加载器是无法加载的。可以进一步跟踪代码会发现它将当前的类加载器设置为线程的上下文类加载器。那么,对于一个刚刚启动的应用程序来说当前的加载器是谁呢?即启动 main 方法的那个加载器,到底是哪一个?
# SPI动态加载代码
// 使用ServiceLoader动态加载指定接口的实现类
ServiceLoader<DataStorage> loader = ServiceLoader.load(DataStorage.class);
// 使用迭代器迭代实现类信息
Iterator<DataStorage> iterator = loader.iterator();
// 跟踪ServiceLoader源码实现:它将当前的类加载器设置为线程的上下文类加载器
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
Launcher(位于sun.misc
包)为jre中用于启动程序入口main()的类。主要功能:创建ExtClassLoader和AppClassLoader,还根据配置创建SercurityManager,设置进程上下文类加载器
public Launcher() {
ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
Launcher是用于启动程序入口main()的类(归属于rt.jar
),跟踪器构造器实现,可以看到Thread.currentThread().setContextClassLoader(this.loader);
,则说明当前上下文线程加载器是应用程序类加载器(加载classpath),因此使用其加载第三方驱动是可行的。
(3)OSGi
OSGi 曾经非常流行,Eclipse 就使用 OSGi 作为插件系统的基础。OSGi 是服务平台的规范,旨在用于需要长运行时间、动态更新和对运行环境破坏最小的系统。
OSGi 规范定义了很多关于包生命周期,以及基础架构和绑定包的交互方式。这些规则,通过使用特殊 Java 类加载器来强制执行,比较霸道。
比如,在一般 Java 应用程序中,classpath 中的所有类都对所有其他类可见,这是毋庸置疑的。但是,OSGi 类加载器基于 OSGi 规范和每个绑定包的 manifest.mf 文件中指定的选项,来限制这些类的交互,这就让编程风格变得非常的怪异。但不难想象,这种与直觉相违背的加载方式,肯定是由专用的类加载器来实现的。
随着 jigsaw 的发展(旨在为 Java SE 平台设计、实现一个标准的模块系统)。OSGi 是一个庞大的话题,只需要知道,有这么一个复杂的东西,实现了模块化,每个模块可以独立安装、启动、停止、卸载,就可以了。如果有机会接触相关方面的工作,也许会不由的发出感叹:原来 Java 的类加载器,可以玩出这么多花样
2.双亲委派模型
三层类加载器&双亲委派模型:(JDK 8及之前)
启动类加载器:负责加载存放在<JAVA_HOME>\lib
目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的类库加载到虚拟机的内存中。
扩展类加载器:这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载<JAVA HOME>\lib\ext
目录中,或者被java.ext.dirs
系统变量所指定的路径中所有的类库
应用程序类加载器:由于应用程序类加载器是ClassLoader类中的getSystemClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库
JDK 9之前的Java应用都是由这三种类加载器互相配合来完成加载的,用户还可以自定义类加载器,他们之间的协作关系如图所示:
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。
如何理解类加载机制的双亲委派模型:思考一种场景:如果不同的类加载器都自行加载需要的某个类型,就会出现多次重复加载的资源浪费问题
使用委派模型的目的就是为了避免重复加载Java类型,以减少不必要的资源浪费
类跟随它的加载器一起具备了有优先级的层次关系,确保某个类在各个类加载器环境中都是同一个,保证程序的稳定性
类加载机制的三个基本特征
- 双亲委派模型:不是所有类加载都遵循双亲委派模型,还有上下文加载器等
- 但不是所有类加载都遵守这个模型,例如启动类加载器所加载的类型可能要加载用户代码的:
- JDK 内部的 ServiceProvider/ServiceLoader机制,用户可以在标准 API 框架上,提供自己的实现,JDK 也需要提供些默认的参考实现
- Java 中 JNDI、JDBC、文件系统、Cipher 等很多方面,都是利用的这种机制,这种情况就不会用双亲委派模型去加载,而是利用所谓的上下文加载器
- 但不是所有类加载都遵守这个模型,例如启动类加载器所加载的类型可能要加载用户代码的:
- 可见性:子类加载器可以访问父类加载器加载的类型,反之则不允许
- 子类加载器可以访问父加载器加载的类型,但是反过来是不允许的,不然,因为缺少必要的隔离,就没有办法利用类加载器去实现容器的逻辑
- 单一性:父加载器加载过的类型对子类加载器可见,就不会在子类加载器中重复加载,但是邻居间的加载器互相并不可见,同一类型可被加载多次
- 由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载
- 但类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见
扩展:底层扩展
- Bootstrap加载器
对于做底层开发的工程师,有的时候可能不得不去试图修改 JDK 的基础代码(即通常意义上的核心类库),例如可以使用命令行参数去覆盖核心类库的一些机制
// Bootstrap
# 指定新的bootclasspath,替换java.*包的内部实现
java -Xbootclasspath:<your_boot_classpath> your_App
# a意味着append,将指定目录添加到bootclasspath后面
java -Xbootclasspath/a:<your_dir> your_App
# p意味着prepend,将指定目录添加到bootclasspath前面
java -Xbootclasspath/p:<your_dir> your_App
- 扩展类加载器
引入extension机制,负责加载jre/lib/ext
目录下的jar包,该目录可以通过设置java.ext.dirs
来进行覆盖
java -Djava.ext.dirs=your_ext_dir HelloWorld
- 应用类加载器
负责加载classpath的内容,此处有个System系统类加载器概念(其默认就是JDK内建的应用类加载器),其同样可以修改。如果指定了这个参数,JDK内建的应用类加载器就会成为定制加载器的父类(参考图示理解),这种方式一般用在类似需要改变双亲委派模式的场景
java -Djava.system.class.loader=com.yourcorp.YourClassLoader HelloWorld
3.JDK9后的类加载委派关系(了解)
因为目前使用还是基于JDK8的应用比较多,首要掌握JDK8的原理,了解JDK9的类加载机制
在 JDK 9 中,由于 Jigsaw 项目引入了 Java 平台模块化系统(JPMS),Java SE 的源代码被划分为一系列模块。
类加载器、类文件容器都发生了很大的变化:
- -Xbootclasspath 参数不可用:API 已经被划分到具体的模块,所以上文中,利用“-Xbootclasspath/p”替换某个 Java 核心类型代码,实际上变成了对相应的模块进行的修补,可以采用下面的解决方案:首先,确认要修改的类文件已经编译好,并按照对应模块(假设是 java.base)结构存放, 然后,给模块打补丁:
java --patch-module java.base=your_patch yourApp
- 扩展类加载器被重命名为平台类加载器(Platform Class-Loader),而且 extension 机制则被移除。也就意味着,如果指定 java.ext.dirs 环境变量,或者 lib/ext 目录存在,JVM 将直接返回错误!建议解决办法就是将其放入 classpath 里
- 部分不需要 AllPermission 的 Java 基础模块,被降级到平台类加载器中,相应的权限也被更精细粒度地限制起来
- rt.jar 和 tools.jar 同样是被移除了!JDK 的核心类库以及相关资源,被存储在 jimage 文件中,并通过新的 JRT 文件系统访问,而不是原有的 JAR 文件系统。虽然看起来很惊人,但幸好对于大部分软件的兼容性影响,其实是有限的,更直接地影响是 IDE 等软件,通常只要升级到新版本就可以了
- 增加了 Layer 的抽象, JVM 启动默认创建 BootLayer,开发者也可以自己去定义和实例化 Layer,可以更加方便的实现类似容器一般的逻辑抽象
JVM优化-降低类加载开销
由于字节码是平台无关抽象,而不是机器码,所以 Java 需要类加载和解释、编译,这些都导致 Java 启动变慢。掌握了类加载机制,思考下有没有什么通用办法,不需要代码和其他工作量,就可以降低类加载的开销呢?
- 方案1:AOT,相当于直接编译成机器码,以降低解释和编译开销(但目前处于试验性,且支持的平台有限,例如JDK9仅支持Linux X64,具备局限性)
- 方案2:AppCDS(Application Class-Data Sharing),CDS 在 Java 5 中被引进,但仅限于 Bootstrap Class-loader,在 8u40 中实现了 AppCDS,支持其他的类加载器,在目前 2018 年初发布的 JDK 10 中已经开源
AppCDS工作原理
(1)JVM 将类信息加载, 解析成为元数据,并根据是否需要修改,将其分类为 Read-Only 部分和 Read-Write 部分。然后,将这些元数据直接存储在文件系统中,作为所谓的 Shared Archive
Java -Xshare:dump -XX:+UseAppCDS -XX:SharedArchiveFile=<jsa> \
-XX:SharedClassListFile=<classlist> -XX:SharedArchiveConfigFile=<config_file>
(2)在应用程序启动时,指定归档文件,并开启 AppCDS
Java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=<jsa> yourApp
通过上面的命令,JVM 会通过内存映射技术,直接映射到相应的地址空间,免除了类加载、解析等各种开销。
AppCDS 改善启动速度非常明显,传统的 Java EE 应用,一般可以提高 20%~30% 以上;实验中使用 Spark KMeans 负载,20 个 slave,可以提高 11% 的启动速度。
与此同时,降低内存 footprint,因为同一环境的 Java 进程间可以共享部分数据结构。前面谈到的两个实验,平均可以减少 10% 以上的内存消耗。
当然,也不是没有局限性,如果恰好大量使用了运行时动态类加载,它的帮助就有限了
类加载机制之开发常见实用技巧
1.如何排查找不到Jar包的问题?
问题场景:有时候已经明明将某个jar加入到环境中,可是运行的时候还是找不到?
编写代码查看到各个类加载器加载了哪些jar,以及把哪些路径加到了classpath里
package com.noob.jvm.tools;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
public class JvmClassLoaderPrintPath {
public static void main(String[] args) {
// 启动类加载器
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
System.out.println("启动类加载器");
for(URL url : urls) {
System.out.println(" ==> " +url.toExternalForm());
}
// 扩展类加载器
printClassLoader("扩展类加载器", JvmClassLoaderPrintPath.class.getClassLoader().getParent());
// 应用类加载器
printClassLoader("应用类加载器", JvmClassLoaderPrintPath.class.getClassLoader());
}
public static void printClassLoader(String name, ClassLoader CL){
if(CL != null) {
System.out.println(name + " ClassLoader -> " + CL.toString());
printURLForClassLoader(CL);
}else{
System.out.println(name + " ClassLoader -> null");
}
}
public static void printURLForClassLoader(ClassLoader CL){
Object ucp = insightField(CL,"ucp");
Object path = insightField(ucp,"path");
ArrayList ps = (ArrayList) path;
for (Object p : ps){
System.out.println(" ==> " + p.toString());
}
}
private static Object insightField(Object obj, String fName) {
try {
Field f = null;
if(obj instanceof URLClassLoader){
f = URLClassLoader.class.getDeclaredField(fName);
}else{
f = obj.getClass().getDeclaredField(fName);
}
f.setAccessible(true);
return f.get(obj);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
// output
启动类加载器
==> file:/C:/custom/develop/CONFIG/jdk/jdk1.8/jdk1.8.0_151/jre/lib/resources.jar
==> file:/C:/custom/develop/CONFIG/jdk/jdk1.8/jdk1.8.0_151/jre/lib/rt.jar
扩展类加载器 ClassLoader -> sun.misc.Launcher$ExtClassLoader@14899482
==> file:/C:/custom/develop/CONFIG/jdk/jdk1.8/jdk1.8.0_151/jre/lib/ext/access-bridge-64.jar
==> file:/C:/custom/develop/CONFIG/jdk/jdk1.8/jdk1.8.0_151/jre/lib/ext/cldrdata.jar
应用类加载器 ClassLoader -> sun.misc.Launcher$AppClassLoader@18b4aac2
==> file:/C:/custom/develop/CONFIG/jdk/jdk1.8/jdk1.8.0_151/jre/lib/charsets.jar
==> file:/C:/custom/develop/CONFIG/jdk/jdk1.8/jdk1.8.0_151/jre/lib/deploy.jar
2.如何排查类的方法不一致的问题?
问题场景:假如确定一个 jar 或者 class 已经在 classpath 里了,但是却总是提示java.lang.NoSuchMethodError
很可能是加载了错误的或者重复加载了不同版本的 jar 包。此时可以用前面的方法就可以先排查一下,加载了具体什么 jar,然后是不是不同路径下有重复的 class 文件,但是版本不一样
3.怎么看到加载了哪些类,以及加载顺序?
问题场景:针对上一个问题,假如有两个地方有 Hello.class,一个是新版本,一个是旧的,怎么才能直观地看到他们的加载顺序呢?
可以直接打印加载的类清单和加载顺序,只需要在类的启动命令行参数加上-XX:+TraceClassLoading
或者 -verbose
即可,注意需要加载 Java 命令之后,要执行的类名之前,不然不起作用。(如果执行java指令提示【错误: 找不到或无法加载主类】,则去除掉package部分)
# 编译
javac -encoding UTF-8 HelloClassLoader.java
# 执行
java HelloClassLoader
java -verbose HelloClassLoader
// output(省略部分内容)
.....
[Loaded java.security.AllPermission from C:\custom\develop\CONFIG\jdk\jdk1.8\jdk1.8.0_151\jre\lib\rt.jar]
[Loaded java.security.UnresolvedPermission from C:\custom\develop\CONFIG\jdk\jdk1.8\jdk1.8.0_151\jre\lib\rt.jar]
[Loaded java.security.BasicPermissionCollection from C:\custom\develop\CONFIG\jdk\jdk1.8\jdk1.8.0_151\jre\lib\rt.jar]
[Loaded HelloClassLoader from file:/E:/workspace/Git/github/PROJECT/366GP/noob-demo/rebirth/base/JavaBase/src/main/java/com/noob/jvm/]
[Loaded sun.launcher.LauncherHelper$FXHelper from C:\custom\develop\CONFIG\jdk\jdk1.8\jdk1.8.0_151\jre\lib\rt.jar]
.....
[Loaded sun.nio.cs.ISO_8859_1$Encoder from C:\custom\develop\CONFIG\jdk\jdk1.8\jdk1.8.0_151\jre\lib\rt.jar]
[Loaded sun.nio.cs.Surrogate$Parser from C:\custom\develop\CONFIG\jdk\jdk1.8\jdk1.8.0_151\jre\lib\rt.jar]
[Loaded sun.nio.cs.Surrogate from C:\custom\develop\CONFIG\jdk\jdk1.8\jdk1.8.0_151\jre\lib\rt.jar]
[Loaded jvm.Hello from __JVM_DefineClass__]
Hello Class Initialized!
[Loaded java.lang.Shutdown from C:\custom\develop\CONFIG\jdk\jdk1.8\jdk1.8.0_151\jre\lib\rt.jar]
[Loaded java.lang.Shutdown$Lock from C:\custom\develop\CONFIG\jdk\jdk1.8\jdk1.8.0_151\jre\lib\rt.jar]
基于上述信息,可以很清楚的看到类的加载先后顺序,以及是从哪个 jar 里加载的,进而排查类加载的问题
4.怎么调整或修改 ext 和本地加载路径?
问题场景:从前面的例子可以看到,假如什么都不设置,直接执行 java 命令,默认也会加载非常多的 jar 包,怎么可以自定义加载哪些 jar 包呢?
例如某个代码很简单,只需要加载某个jar(例如rt.jar ),也可通过指令指定
# CMD窗口测试(idea中的terminal可能对指令解析有点问题),此处以JvmClassLoaderPrintPath为例(去除package部分)
// 编译
javac JvmClassLoaderPrintPath
// 指定rt.jar
java -Dsun.boot.class.path="C:\custom\develop\CONFIG\jdk\jdk1.8\jdk1.8.0_151\jre\lib\rt.jar" -Djava.ext.dirs= JvmClassLoaderPrintPath
// output
启动类加载器
==> file:/C:/custom/develop/CONFIG/jdk/jdk1.8/jdk1.8.0_151/jre/lib/rt.jar
扩展类加载器 ClassLoader -> sun.misc.Launcher$ExtClassLoader@4e25154f
应用类加载器 ClassLoader -> sun.misc.Launcher$AppClassLoader@15db9742
==> file:/E:/workspace/Git/github/PROJECT/366GP/noob-demo/rebirth/base/JavaBase/src/main/java/com/noob/jvm/tools/
==> file:/C:/custom/develop/CONFIG/jdk/jdk1.8/jdk1.8.0_151/lib/dt.jar
==> file:/C:/custom/develop/CONFIG/jdk/jdk1.8/jdk1.8.0_151/lib/tools.jar
其中命令行参数-Dsun.boot.class.path
表示要指定启动类加载器加载什么,最基础的东西都在 rt.jar 这个包了里,所以一般配置它就够了。需要注意的是因为在 windows 系统默认 JDK 安装路径有个空格,所以需要把整个路径用双引号括起来,如果路径没有空格,或是 Linux/Mac 系统,就不需要双引号
5.怎么运行期加载额外的 jar 包或者 class 呢?
问题场景:有时候在程序已经运行了以后,还是想要再额外的去加载一些 jar 或类?
简单说就是不使用命令行参数的情况下,怎么用代码来运行时改变加载类的路径和方式。假如说某个路径下,有生成的 Hello.class 文件,怎么在代码里能加载这个 Hello 类呢?
方式1:自定义 ClassLoader 的方式
方式2:直接在当前的应用类加载器里,使用 URLClassLoader 类的方法 addURL(不过这个方法是 protected 的,需要反射处理一下)。因为程序在启动时并没有显示加载 Hello 类,所以在添加完了 classpath 以后,没法直接显式初始化,需要使用 Class.forName 的方式来拿到已经加载的Hello类(Class.forName(“jvm.Hello”)默认会初始化并执行静态代码块)
public class JvmAppClassLoaderAddURL {
public static void main(String[] args) throws Exception {
String appPath = "file:/e:/test/";
URLClassLoader urlClassLoader = (URLClassLoader) JvmAppClassLoaderAddURL.class.getClassLoader();
try {
Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
addURL.setAccessible(true);
URL url = new URL(appPath);
addURL.invoke(urlClassLoader, url);
Class.forName("Hello"); // 效果等同于Class.forName("Hello").newInstance()
} catch (Exception e) {
e.printStackTrace();
}
}
}
6.如何替换JDK中的类
如何替换 JDK 中的类?以HashMap为例。
当 Java 的原生 API 不能满足需求时,比如要修改 HashMap 类,就必须要使用到 Java 的 endorsed 技术。需要将自己的 HashMap 类,打包成一个 jar 包,然后放到 -Djava.endorsed.dirs
指定的目录中。注意类名和包名,应该和 JDK 自带的是一样的。但是,java.lang 包下面的类除外,因为这些都是特殊保护的。
基于上述的双亲委派机制,是无法直接在应用中替换 JDK 的原生类的。但是,有时候又不得不进行一下增强、替换,比如想要调试一段代码,或者比 Java 团队早发现了一个 Bug。所以,Java 提供了 endorsed 技术,用于替换这些类。这个目录下的 jar 包,会比 rt.jar 中的文件,优先级更高,可以被最先加载到