概要
初识JVM
JVM全称Java Virtual Machine 中文译名Java虚拟机。
本质上是一个运行在计算机上的程序,职责是运行Java字节码文件。
JVM的功能
1》解释和运行
1.对字节码文件中的指令,实时的解释生成机械码,让计算机执行
2》内存管理
1.自动为对象、方法等分配内存
2.自动的垃圾回收机制,回收不再使用的对象
3》即时编译
1.对热点代码进行优化,提高执行效率
JVM的实时解释、即时编译,为Java提供了跨平台特性,但也在不做优化的情况下性能相对C、C++更低。
JVM在读取到热点代码时会对热点代码编译成机器码并保存在内存当中,Java提供的即时编译(Just-In-Time)进行了性能的优化,最终能达到接近C、C++的运行性能,甚至在特定场景下能够超越C、C++的运行性能。
常见的JVM
1》Java虚拟机规范
1.字节码文件中的内容和格式必须要满足规范
2.类和接口在加载到内存以及初始化的过程中这些步骤是符合规定的
3.字节码指令里面的指令集在规范中也有设计
4.《Java虚拟机规范》是对虚拟机设计的要求,而不是对Java设计的要求,也就是说虚拟机可以运行在Groovy、Scala生成的class字节码文件上。
5.Java语言以及虚拟机规范:https://docs.oracle.com/javase/specs/index.html
2》五种常见虚拟机
1.HotSpot(Oracle JDK版本) 作者:Oracle 版本:所有版本
特性:使用最广泛,稳定可靠,社区活跃、JIT支持、Oracle JDK默认虚拟机,闭源
2.HotSpot(Open JDK版本) 作者:Oracle 版本:所有版本
特性:使用最广泛,稳定可靠,社区活跃度中、JIT支持、Open JDK默认虚拟机,开源
场景:适用于对JDK有二次开发需求的场景
3.GraalVM 作者:Oracle 版本:11,17,19,企业版支持8
特性:社区活跃度高,多语言支持Ruby、Python、C++等,高性能、JIT、AOT支持
场景:微服务、云原生框架、需要多语言混合编程的环境
4.Dragonwell JDK 作者:Alibaba 版本:标准版8,11,17、扩展版11,17
特性:基于Open JDK 的增强,高性能、bug修复、安全性提升,JWarmup、ElasticHeap、Wisp特性支持
场景:电商、物流、金融领域等对性能要求比较高的场景
5.Eclipse OpenJ9 作者:IBM 版本:8,11,17,19,20
特性:高性能,可扩展,支持JIT、AOP
背景:微服务、云原生架构
HotSpot发展历程
1999年 -HotSpot发布,JDK1.2中作为附加功能存在,1.3之后作为默认的虚拟机,此时HotSpot的性能和稳定性都存在一定问题,并不能使Java性能超过C和C++。
2006年 -JDK 6 发布,添加了自旋锁提升并发性能等优化,提升了JDK的性能和稳定性
2009年-2013年 -JDK 7 中首次提出了G1垃圾收集器,JDK 8 中引入了JMC等工具,去除了永久代。
2018年-2019年 JDK 11 优化了G1垃圾回收器的性能,同时推出了ZGC新一代的垃圾回收器;JDK 12 推出Shenan-doah垃圾回收器
至今-以HotSpot为基础的GraalVM虚拟机诞生,解决单体应用中多语言整合难题的同时也提升了这些语言运行时的效率。极高的性能和极高的启动速度也更适用于当下的云原生架构。
整体架构流程
JVM的组成
字节码文件详解
1》解决问题
1.版本冲突问题
2.系统升级问题
2》使用Jclasslib打开字节码文件
GitHub地址:https://github.com/ingokegel/jclasslib
(1) 基本信息>
魔数、字节码文件对应的Java版本号访问标识(public、final等等)、父类和接口
一般信息>
次版本号、主版本号、常量池计数、访问标志
本类索引、父类索引
接口计数、字段计数、方法计数、属性计数
魔数>
ca fe ba be
软件使用文件头校验文件类型,如果软件不支持该种类型就会出错
文件类型 | 字节数 | 文件头 |
---|---|---|
JPEG(.jpg) | 3 | FFD8FF |
PNG(.png) | 4 | 89504E47 |
bmp | 2 | 424D |
XML(.xml) | 5 | 3C3F786D6C |
Java(.class) | 4 | CAFEBABE |
主副版本号>
主版本号: JDK 1.0 - 1.1 使用了 45.0 - 45.3,JDK 1.2 是 4.6 之后每升级一个大版本就加1;
1.2 之后 (主版本号 - 44)即为 大版本号
版本号的作用主要是判断当前字节码的版本和运行时的JDK是否兼容。
版本不兼容报错引起VersonError>
解决方案:1. 升级JDK版本
2.将第三方依赖的版本号降低或者更换依赖,以满足JDK版本的要求【推荐】
(2) 常量池 >
保存了字符串常量、类或接口名,字段名主要在字节码指令中使用
常量池的作用:避免相同的内容重复定义,节省空间。
常量池中的每个数据都有一个编号,编号从1开始。在字段或者字节码指令中通过编号可以快速找到对应的数据。
字节码指令中通过编号引用到常量池的过程称为符号引用。
(3) 字段>
当前类或接口声明的字段信息
字段中存放的是属性名的索引和常量池的索引,通过索引去常量池找相应内容
(4) 方法>
当前类或接口声明的方法信息、字节码指令
字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的Code属性中。
操作数栈:临时存放数据的地方
局部变量表:存放方法中的局部变量的位置,依照声明顺序依次存入局部变量表
分析实例1:
源代码》
int i = 0;
int j = i + 1;
方法区的机器码》
iconst_0 //将0放入到操作数栈
istore_1 //将操作栈中的数据弹出来,放到局部变量表1的位置
iload_1 //将局部变量表1的位置copy出数据放到操作数栈
iconst_1 //将1放入到操作数栈中
iadd //将操作数栈中的数据执行加操作
istore_2 //将操作栈中的数据取出放入局部变量表2的位置
return //方法结束返回
分析实例2:
源代码》
int i = 0;
i = i++;
方法区的机器码》
iconst_0 //将0放入到操作数栈
istore_1 //将操作栈中的数据弹出来,放到局部变量表1的位置
iload_1 //将局部变量表1的位置copy出数据放到操作数栈
iinc 1 by 1 //在局部变量表1的位置增加1
istore_1 //将操作栈中的数据取出放入局部变量表1的位置,如有则覆盖掉
return //方法结束返回
(5) 属性>
类的属性,比如源码的文件名、内部类的列表等
3》字节码文件的组成
1.javap -v命令
javap是JDK自带的反编译工具,可以通过控制台查看字节码文件的内容。适合在服务器上查看字节码文件内容。
直接输入javap查看所有参数
输入 javap -v 字节码文件名称 查看具体的字节码信息。(如果jar包需要先试用 jar -xvf jar包名字 命令解压)
2.Jclasslib插件(IDEA)
IDEA插件版本可以在代码编译之后实时看到字节码文件内容。
View ——》 Show Bytecode With Jclasslib
Jclasslib中展示的字节码文件是最后一次编译时的字节码文件
3.阿里arthas
线上监控诊断产品,通过全局视角实时查看应用load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率。
官网:https://arthas.aliyun.com/doc/
功能:监控面板、查看字节码信息、方法监控、类的热部署、内存监控、垃圾回收监控、应用热点定位
常用命令:gump、jad详细参考官方开发文档
类加载器详解
类的生命周期
类的生命周期描述了一个类加载、使用、卸载的整个过程
加载-》连接-》初始化-》使用-》卸载
类的加载阶段
1.加载(loading)阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。程序员可以使用Java代码拓展的不同渠道(本地文件、动态代理生成、通过网络传输的类)。
2.类的加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到内存的方法区中。
生成一个InstanceKlass对象,保存类的所有信息,里面还包含实现特定功能比如多态的信息
3.同时。Java虚拟机还会在对重生成一份与方法去中的数据类似的java.lang.Class对象。
作用是在Java代码中去获取类的信息以及存储静态字段的数量(JDK8之后)
静态字段存放在堆中(JDK8之后)
JDK自带的hsdb工具查看Java虚拟机内存信息。工具位于JDK安装目录下lib文件夹中的sa-jdi.jar中。
启动命令:java -cp sa-jdi.jar sun.jvm.htspot.HSDB
类的连接阶段
验证-》准备-》解析
验证:验证内容是否满足《java虚拟机规范》
准备:给静态变量赋初值(为堆中的静态变量赋默认初值,如果是final修饰的static静态变量,准备阶段将会直接对代码中的变量进行赋值)
解析:将常量池中的符号引用替换成执行内存的指向内存的直接引用
符号引用:在字节码文件中使用编号来访问常量池中的内容
直接引用:不再使用编号,而是使用内存中的地址进行访问具体的数据
类的初始化阶段
初始化阶段会执行静态代码中的代码,并未静态变量赋值。
初始化阶段会执行字节码文件中clinit部分的字节码指令
clinit方法中执行顺序于Java中编写的顺序是一致的
以下几种方式会导致类的初始化:
1.访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化。
2.调用Class.forName(String className),forName的两参数构造默认initialize = true 也就是默认会初始化类,而forName的三参构造器提供了initialize参数,可以赋值为false也就是不对其进行初始化 。
3.new 一个该类的对象时。
4.执行Main方法的当前类。
例题:
1.下列代码执行结果
public class Test1 {
public static void main(String[] args) {
System.out.println("A");
new Test1();
new Test1();
}
public Test1(){
System.out.println("B");
}
{
System.out.println("C");
}
static {
System.out.println("D");
}
}
执行结果:
D
A
C
B
C
B
clinit指令在某些特定情况下不会出现,即没有进行类的初始化阶段》
1.无静态代码块且无静态变量赋值语句
2.有静态变量的生命,但是没有赋值语句
3.静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化
涉及继承时类的初始化》
1.直接访问父类的静态变量,不会触发子类的初始化
2.子类的初始化clinit调用之前,会先调用父类的clinit初始化方法
初始化有关的规律》
1.数组的创建不会导致数组中元素的类进行初始化
2.final修饰的变量如果赋值的内容需要执行指令才能得到结果,会执行clinit方法进行初始化
public class Test1 {
public static void main(String[] args) {
System.out.println(TestA.a);
}
}
class TestA{
public static final int a = Integer.valueOf(1);
static {
System.out.println("TestA的静态代码块运行");
}
}
类加载器
类加载器是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。
类加载器只参与加载过程中的字节码获取并加载到内存这一部分。
类加载器的应用场景》
SPI机制、类的热部署、Tomcat类的隔离
面试题》
什么是类的双亲委派机制、怎么打破类的双亲委派机制、自定义类加载器实现
线上问题》
使用Arthas不停机解决线上故障
类加载的分类
类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。
JDK8及8之前的类加载器的分类:
虚拟机底层实现C++ | Java | ||
启动类加载器Bootstrap | 加载Java中最核心的类 | 扩展类加载器Extension | 允许扩展Java中比较通用的类 |
应用程序类加载器Application | 加载应用使用的类 |
Arthas中类加载器的相关的功能:
类加载器的详细信息可以通过classloader命令查看:
classloader - 查看 classloader 的继承树,urls,类加载信息,使用 classloader 去 getResource
启动类加载器
- 启动类加载器是由HotSpot虚拟机提供的,使用C++编写的类加载器。
- 默认加载Java安装目录/jre/lib下的类文件,比如:rt,jar 、tools.jar 、resources.jar 等。
- 程序员不能在代码中获取到启动类加载器
Arthas中搜索类的命令sc,“Search-Class”的缩写,这个命令能搜索出所有已经加载到JVM中的Class信息,这个命令支持的参数有[d]、[E]、[f]、[x:]。
//示例 所有String类的类加载器信息
sc -d java.lang.String
通过启动类加载器去加载用户jar包:
- 放入jre/lib下进行扩展(不推荐,有可能会因为名称不满足规范导致无法被加载)
- 使用参数进行扩展(使用 -Xbootclasspath/a:jar包目录/jar包名 进行扩展)
JAVA中的默认类加载器
- 扩展类加载器号应用程序类加载器都是JDK提供的、使用Java编写的类加载器。
- 他们的源码都位于sun.misc.Launcher中,是一个静态内部类。继承自URLClassLoader。具备通过目录或者指定jar包将字节码文件加载到内存中。
- ClassLoader:抽象类定义了类加载器的具体行为模式,通过JNI调用底层的Java虚拟机方法
- SecureClassLoader:使用证书机制提升类加载器的安全性
- URLClassLoader:利用URL获取目录下或者指定的jar包进行加载,获取其字节码数据。
扩展类加载器
- 扩展类加载器(Extension Class Loader)是由JDK中提供的、使用Java编写的类加载器。
- 默认加载Java安装目录/jre/lib/ext下的类文件
通过扩展类加载器去加载用户jar包:
- 放入/jre/lib/ext下进行扩展(不推荐)
- 使用参数进行扩展(使用 -Djava.ext.dirs=jar 包目录 进行扩展,这种方式会覆盖掉原始目录,可以用;(windows):(macos/linux)分隔,追加上原始目录)
如果路径中有特殊字符,可以通过使用双引号将其全路径引起来,避免报错
应用程序类加载器
- 默认加载 classpath 下的类文件
- Maven提供的类和手动创建的类使用此类加载器加载
Arthas 通过 ClassLoader 指令查看 某一类加载器加载的所有jar包的路径
ClassLoader -l //获取当前类加载器 及 hash
ClassLoader -c [哈希值] //在c后追加hash值,获取该类加载的所有jar包路径
双亲委派机制
双亲委派机制的作用
- 保证类加载的安全性,确保核心类库的完整性和安全性
- 避免同一个类被多次加载
双亲委派机制
- 当一个类加载器接收到加载类的任务的时候,会自底向上查找是否被加载过,再由顶向下进行加载。
- 每个类加载器都有一个父类加载器,在类加载的过程中,每个类加载器都会先检查是否已经加载了该类,如果已经加载则直接返回,否则会将加载请求委派给父类加载器。(避免重复加载)
- 如果所有的父类加载器都无法加载该类,则由当前类加载器自己尝试加载。所以看上去是自顶向下尝试加载。
- 顺序自顶向下为:启动类加载器==》扩展类加载器==》应用程序类加载器
在Java中使用代码的方式去主动加载一个类的方式
- 使用Class.forName方法,使用当前类的加载器去加载指定的类。
- 获取到类加载器,通过类加载器的loadClass方法指定某个类加载器加载。
public class ClassLoaderTest1 {
public static void main(String[] args) throws ClassNotFoundException {
//获取main方法所在类的类加载器,应用程序类加载器
ClassLoader classLoader = ClassLoaderTest1.class.getClassLoader();
System.out.println(classLoader);
//使用应用程序类加载器加载 test.FloydDemo
Class<?> clazz = classLoader.loadClass("test.FloydDemo");
System.out.println(clazz.getClassLoader());
}
}
父类加载器本质上是类加载器的一个参数 parent
扩展类加载器的parent=null,但是在代码逻辑上,扩展类加载器依然会把启动类加载器当成父类加载器处理。
Arthas 指令
ClassLoader -t //执行此指令可以看到类的父子关系
打破双亲委派机制
打破双亲委派机制共有三种方式
1.自定义类加载器
例如Tomcat
类加载器的四个核心方法
例:
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class BreakClassLoaderTest extends ClassLoader{
private String basePath;
private final static String FILE_EXT = ".class";
private final static String JAVA_EXT = "java.";
public void setBasePath(String basePath) {
this.basePath = basePath;
}
private byte[] loadClassDate(String name){
String fileName = name.replace('.', File.separatorChar) + FILE_EXT;
File classFile = new File(basePath, fileName);
byte[] classData = null;
try (FileInputStream fis = new FileInputStream(classFile)) {
classData = new byte[(int) classFile.length()];
fis.read(classData);
} catch (IOException e) {
e.printStackTrace();
}
return classData;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if(name.startsWith(JAVA_EXT)){
return super.loadClass(name);
}
byte[] date = loadClassDate(name);
return defineClass(name,date,0, date.length);
}
public static void main(String[] args) throws ClassNotFoundException {
BreakClassLoaderTest classLoader = new BreakClassLoaderTest();
classLoader.setBasePath("D:\\project\\test\\demo3\\target\\classes\\");
Class<?> clazz = classLoader.loadClass("TestA");
System.out.println(clazz.getClassLoader());
}
}
2.线程上下文类加载器
JDBC为例:
JDBC使用了DirverManager来管理项目中引入的不同数据库驱动,比如MySQL驱动、Oracle驱动。
- DriverManager类位于让it.jar包中,由启动类加载器加载。
- 而用户jar包中的驱动需要由应用类加载器加载,这违反了双亲委派机制。
- SPI机制:JDK内置的一种服务提供发现机制
- SPI中使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器。
//方法来源于ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
//在调用load方法时获取了线程上下文中的保存的应用程序类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
例:通过线程上下文的方式获取应用程序类加载器
public class NewThreadDemo {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getContextClassLoader());
}
}).start();
}
}
执行结果:
sun.misc.Launcher$AppClassLoader@58644d46
Process finished with exit code 0
例:获取其他类加载器/自定义类加载器
public static void main(String[] args) throws ClassNotFoundException {
BreakClassLoaderTest classLoader = new BreakClassLoaderTest();
Thread.currentThread().setContextClassLoader(classLoader);
System.out.println(Thread.currentThread().getContextClassLoader());
}
执行结果:
BreakClassLoaderTest@4554617c
Process finished with exit code 0
JDBC的流程
- 启动类加载器加载DriverManager
- 在初始化DriverManager时,通过SPI机制加载jar包中的MySQL驱动
- SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象
- 这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,打破了双亲委派机制
SPI工作原理》
1.ClassPath路径下的META-INF、services文件夹中,以接口的全限定名来命名文件名,对应的文件里面写该接口的实现。
2.使用ServiceLoader加载实现类
是否打破双亲委派机制?
没有打破双亲委派机制。因为JDBC只是在DriverManager加载完之后,通过初始化阶段出发了驱动类的加载,类的加载依然遵循双亲委派机制。
3.Osgi框架的类加载器
JDK9之后的类加载器
在JDK8及之前的版本中,扩展类加载器和应用程序类加载器的源码位于rt.jar包中的sun.misc.Launcher.java
JDK9引入了module的概念,类加载器在设计上发生了很多变化。
1.启动类加载器使用Java编写,位于jdk.internal.loader.ClassLoaders类中。Java中的BootClassLoader继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件。
启动类加载器依然无法通过java代码获取到,返回的仍然是null,保持了统一。
2.扩展类加载器被替换为平台类加载器。
平台类加载器遵循模块化方式加载字节码文件,所以继承关系丛URLClassLoader变成了BuiltinClassLoader,BuiltinClassLoader实现了从模块中加载字节码文件。平台类加载器的存在更多的是为了与老版本的设计方案兼容,本身没有特殊的逻辑。
技术名词解释
提示:这里可以添加技术名词解释
例如:
- Bert
- GPT 初代
- GPT-2
- GPT-3
- ChatGPT
技术细节
提示:这里可以添加技术细节
例如:
- API
- 支持模型类型
小结
提示:这里可以添加总结
例如:
提供先进的推理,复杂的指令,更多的创造力。