【实战JVM】-01-JVM通识-字节码详解-类的声明周期-加载器
- 1 初识JVM
- 1.1 什么是JVM
- 1.2 JVM的功能
- 1.2.1 即时编译
- 1.3 常见JVM
- 2 字节码文件详解
- 2.1 Java虚拟机的组成
- 2.2 字节码文件的组成
- 2.2.1 正确打开字节码文件
- 2.2.2 字节码组成
- 2.2.3 基础信息
- 2.2.3.1 魔数
- 2.2.3.1 主副版本号
- 2.2.4 常量池
- 2.2.5 方法
- 2.3 linux中打开字节码文件
- 2.4 字节码常用工具 Arthas
- 2.4.1 安装Arthas
- 2.4.2 Arthas功能
- 2.4.2.1 获取系统实时面板-dashboard
- 2.4.2.2 加载特定类的字节码-dump
- 2.4.2.3 反编译已加载类的源码-jad
- 2.4.2.4 查看JVM已加载的类信息-sc
- 3 类的生命周期
- 3.1 生命周期的概述
- 3.2 加载阶段
- 3.2.1 查看内存中的对象
- 3.3 连接阶段
- 3.3.1 验证阶段
- 3.3.1.1 验证是否符合jvm规范
- 3.3.1.2 元信息验证
- 3.3.1.3 验证语义
- 3.3.1.4 符号引用验证
- 3.3.2 准备阶段
- 3.3.3 解析阶段
- 3.4 初始化阶段
- 3.4.1 笔试题
- 3.4.2 特殊情况
- 3.5 总结
- 4 类的加载器
- 4.1 类加载器的分类
- 4.1.1 JDK8之前的分类
- 4.1.2 使用Arthas查看类加载器-classloader
- 4.1.3 C++启动类加载器BootstrapClassLoader
- 4.1.4 Java中默认类加载器
- 4.1.4.1 扩展类加载器ExtClassLoader
- 4.1.4.2 应用程序类加载器 AppClassLoader
- 4.1.4.3 Arthas-classloader高级用法
- 4.2 类加载器的双亲委派机制
- 4.2.1 Arthas查看类加载器父子关系
- 4.2.2 面试
- 4.3 打破双亲委派机制
- 4.3.1 自定义类加载器
- 4.3.1.1 Arthas展示类的详细信息
- 4.3.1.2 正确的自定义类加载器
- 4.3.2 线程上下文类加载器
- 4.3.2.1 SPI机制
- 4.3.2.2 总结
- 4.3.3 热部署
- 4.3.3.1 热更新注意事项
- 4.4 JDK8之后的类加载器
1 初识JVM
1.1 什么是JVM
1.2 JVM的功能
1.2.1 即时编译
即时编译Just-In-Time 简称JIT进行性能的优化,最终到达接近C、C++的性能
将热点代码转换为机器码后保存至RAM,下次执行时直接从RAM中调用。
1.3 常见JVM
java -version
2 字节码文件详解
2.1 Java虚拟机的组成
2.2 字节码文件的组成
2.2.1 正确打开字节码文件
安装jclasslib
打开任意一个class文件
2.2.2 字节码组成
-
基础信息(一般信息+接口):
- 魔数、字节码对应的java版本号,访问标识(public、final等),以及这个类父类是哪个,以及实现了哪些接口
-
常量池:
- 保存了字符串常量、类、接口名、字段名。主要在字节码指令中使用。
-
字段:
-
当前类或接口的字段信息,包括名字,描述符,访问标识。
-
private final static int a1=0
-
-
方法:
- 当前类或接口的声明的方法信息字节码指令
-
属性:
- 类的属性,比如源码的名字、内部类的列表等
2.2.3 基础信息
2.2.3.1 魔数
打开二进制的png文件,就是以89504E47开始的
jpg则以FFD8FF开始
java字节码则是以CAFEBABE开始
2.2.3.1 主副版本号
52对应jdk1.8 61则对应jdk17
2.2.4 常量池
public class HelloWorld{
public static final String a1= "a1a1a1";
public static final String a2= "a1a1a1";
public static void main(String[] args){
System.out.println("Hello world!");
}
}
查看编译后的class文件
两个都是常量,且指向同一块常量值索引,CONSTANT_String_info存放着cp_info #33,依旧是个索引
查看cp_info #33,这时候字面量才是a1a1a1
为什么要两级索引呢?这是因为在jvm中的运行时数据区域中有这方法区,方法区主要用来存储已被虚拟机加载的类的信息、常量、静态变量和即时编译器编译后的代码等数据。方法区里有一个运行时常量池,用于存放静态编译产生的字面量和符号引用。该常量池具有动态性,也就是说常量并不一定是编译时确定,运行时生成的常量也会存在这个常量池中。
public class HelloWorld{
public static final String a1= "abc";
public static final String a2= "abc";
public static final String abc= "abc";
public static void main(String[] args){
System.out.println("Hello world!");
}
}
比如字段名和常量名都叫abc,但常量名是abc是String类型,而字段名是无类型的,但是都指向utf8格式的abc
2.2.5 方法
public static void main(String[] args){
int i=0;
i=i++;
System.out.println(i);
}
对应字节码:
0 iconst_0 //操作数栈: [] -> [0],将常量值0压入操作数栈。
1 istore_1 //操作数栈: [0] -> [],将操作数栈顶的整数值(0)存入本地变量1。
2 iload_1 //操作数栈: [] -> [0],将本地变量1中的整数值(0)加载到操作数栈。
3 iinc 1 by 1 //本地变量1的值增加1。原值是0,现在变为1。
6 istore_1 //操作数栈: [0] -> [],将操作数栈顶的整数值存入本地变量1。本地变量: [1]-> [0],
7 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;> //获取System.out的值并压入操作数栈。
10 iload_1 //操作数栈: [Ljava/io/PrintStream;] -> [Ljava/io/PrintStream;, 0],将本地变量1中的整数值加载到操作数栈。
11 invokevirtual #3 <java/io/PrintStream.println : (I)V>
14 return
i处于局部变量表的1号位
如果换成++i
public static void main(String[] args){
int i=0;
i=++i;
System.out.println(i);
}
0 iconst_0
1 istore_1 //0放到本地变量表
2 iinc 1 by 1 //本地变量表先自增,0->1
5 iload_1 //将本地变量1中的整数值(1)加载到操作数栈。
6 istore_1 //操作数栈: [1] -> [],将操作数栈顶的整数值存入本地变量1。本地变量: [1]-> [1],
7 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
10 iload_1
11 invokevirtual #3 <java/io/PrintStream.println : (I)V>
14 return
这样就不会发生i=i++这种覆盖赋值的情况了。
作业:
int i=0,j=0,k=0;
i++;
j=j+1;
k+=1;
i和k一样快,都是把0从操作数栈中放入本地变量中直接操作本地变量自增。
j最慢,从本地变量表中加载到操作数栈中,再加载1,再相加,再放入本地变量表。
2.3 linux中打开字节码文件
javap -v 字节码文件名称
2.4 字节码常用工具 Arthas
2.4.1 安装Arthas
启动arthas
先启动项目再分析
public class ArthasDemo {
public static void main(String[] args) {
while (true) {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在arthas工作目录中启动
java -Dfile.encoding=UTF-8 -jar arthas-boot.jar
-Dfile.encoding=UTF-8
是让arthas以utf8格式启动,这样不会乱码
成功捕获到ArthasDemo,选择3回车进入arthas内部,他还自动下载了arthas3.7.2版本
3
2.4.2 Arthas功能
2.4.2.1 获取系统实时面板-dashboard
我们设置每隔两秒刷新一次,刷新3次
dashboard -i 2000 -n 3
只显示1次
2.4.2.2 加载特定类的字节码-dump
dump -d D:/File/StudyJavaFile/JavaStudy/JVM/low/day01/resource/ com.sjb.arthas.ArthasDemo
这样就获取了运行时的java文件的字节码信息
2.4.2.3 反编译已加载类的源码-jad
jad com.sjb.arthas.ArthasDemo
和我们的源码几乎一致
案例
启动springboot-classfile后
public UserVO user(@PathVariable("type") Integer type,@PathVariable("id") Integer id){
//前边有一大堆逻辑,巴拉巴拉
if(type==UserType.REGULAR.getType()){
return new UserVO(id,"普通用户无权限查看");
}
return new UserVO(id,"这是尊贵的收费用户才能看的秘密!");
}
不能用==来判断类型,需要equals
即使是普通用户,但是因为用的==判断的类型,也能进入vip用户
使用jad查看
jad com.itheima.springbootclassfile.controller.UserController
定位到问题信息,以供以后热更新
2.4.2.4 查看JVM已加载的类信息-sc
sc -d 类名(java.lang.String)
查看当前类的类加载器,如果为空,则为启动类加载器。
3 类的生命周期
3.1 生命周期的概述
3.2 加载阶段
3.2.1 查看内存中的对象
推荐使用JDK自带的hsdb工具查看Java虚拟机内部的内存信息。工具位于JDK安装目录下的lib文件夹的sa-jdi.jar中。
启动jvm项目的HsdbDemo
使用jps展示当前所有的java进程及id
jps
启动命令
D:\Software\software_with_code\idea\jdk\jdk1.8.0_381\lib>java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
输入我们要找的类的编号HsdbDemo:18648
直接搜索HsdbDemo,因为我们只new了一次,自然只有一个对象。
一句话概括:类加载器将类的信息加载到内存中,java虚拟机在方法区和堆区中各分配一个对象去保存这个信息,而我们需要操作的则是堆区中的对象,jdk8之后静态字段也是存在堆中的。
3.3 连接阶段
3.3.1 验证阶段
3.3.1.1 验证是否符合jvm规范
3.3.1.2 元信息验证
3.3.1.3 验证语义
3.3.1.4 符号引用验证
判断是否访问了其他类的private方法。
3.3.2 准备阶段
value在准备阶段分配的值是默认值0,而赋值为1是初始化阶段做的事
但是也有例外,如果是final修饰的基本数据类型,会在准备阶段直接将代码中的值进行赋值。
public static final int value=1;
3.3.3 解析阶段
直接引用不在使用编号,而是直接使用内存中的地址进行访问具体的数据。
3.4 初始化阶段
- 初始化阶段会执行静态代码中的代码,并为静态变量赋值
- 初始化阶段会执行字节码文件中的clinit部分的字节码指令。
如果颠倒一下顺序,那么输出则是1
因为静态变量是在连接阶段的准备阶段完成默认初始化。然后再赋为2,再赋为1。
3.4.1 笔试题
(1)
构造代码块先于构造方法前执行
3.4.2 特殊情况
-
直接访问父类的静态变量,不会触发子类的初始化。
-
子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。
-
数组的创建不会导致数组中元素的类进行初始化。
- 是因为创建数组时是创建的数组的对象,而不是数组中元素的对象。所以数组中元素的类不会进行初始化。
-
如果一个变量用final修饰,并且其中的内容要执行指令才能得出结果,那么会在clinit方法中进行初始化。
3.5 总结
4 类的加载器
类加载器是jvm提供给应用程序去实现获取类和接口字节码数据的技术。
负责在类加载过程中的字节码获取并且加载到内存这一部分。通过加载字节码数据放入内存转换成byte[],接下来调用虚拟机底层的方法将byte[]转换成方法区和堆中的数据。
4.1 类加载器的分类
俩下Shift是搜索
Ctrl+Alt+类是找当前类的所有实现
4.1.1 JDK8之前的分类
- 引导类加载器 Bootstrap,加载属于JVM的一部分,由C++代码实现,负责加载
<JAVA_HOME\>\jre\lib
路径下的核心类库 - 扩展类加载器 ExtClassLoader,扩展类加载器负责加载
<JAVA_HOME>\jre\lib\ext
目录下的类库。 - 应用程序类加载器 AppClassLoader,应用程序类加载器负责加载
classpath环境变量
所指定的类库,是用户自定义类的默认类加载器。
4.1.2 使用Arthas查看类加载器-classloader
启动Hsdbdemo后打开arthas,在arthas工作目录中启动
java -jar arthas-boot.jar
进入Hsdbdemo
classloader
4.1.3 C++启动类加载器BootstrapClassLoader
负责加载<JAVA_HOME\>\jre\lib
路径下的核心类库
通过类名.class.getClassLoader来获取当前类的类加载器。
添加java虚拟机参数D:/jvm/jar/classloader-test.jar
是jar包地址
-Xbootclasspath/a:D:/jvm/jar/classloader-test.jar
4.1.4 Java中默认类加载器
4.1.4.1 扩展类加载器ExtClassLoader
扩展类加载器 ExtClassLoader,扩展类加载器负责加载<JAVA_HOME>\jre\lib\ext
目录下的类库。
添加java虚拟机参数D:/jvm/jar/classloader-test.jar
是jar包地址。
不仅需要jar包的地址,还需要原来ext的地址D:\Software\software_with_code\idea\jdk\jdk1.8.0_381\jre\lib\ext
-Djava.ext.dirs="D:\Software\software_with_code\idea\jdk\jdk1.8.0_381\jre\lib\ext;D:/jvm/jar/classloader-test.jar"
在windows中;是追加,linux和mac中:是追加,尽量用双引号引起俩,以免因为特殊字符报错。
4.1.4.2 应用程序类加载器 AppClassLoader
应用程序类加载器 AppClassLoader,应用程序类加载器负责加载 classpath环境变量
所指定的类库,是用户自定义类的默认类加载器。既可以加载当前项目中创建的类,也可以加载maven依赖中包含的类。
4.1.4.3 Arthas-classloader高级用法
classloader -l
查看当前所有的类加载器以及其哈希值
classloader -c hash值
查看当前查询的类加载器加载的所有jar包
4.2 类加载器的双亲委派机制
jvm中有多个类加载器,双亲委派机制的核心就是解决一个类到底由谁加载的问题。
双亲委派机制的作用:
-
保证类加载的安全性
通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如Java.lang.String,确保核心类库的完整性和安全性。
-
避免重复加载
双亲委派机制可以避免同一个类被多次加载
4.2.1 Arthas查看类加载器父子关系
classloader -t
4.2.2 面试
4.3 打破双亲委派机制
4.3.1 自定义类加载器
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if(name.startsWith("java.")){
return super.loadClass(name);
}
byte[] data = loadClassData(name);
return defineClass(name, data, 0, data.length);
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
BreakClassLoader1 classLoader1 = new BreakClassLoader1();
classLoader1.setBasePath("D:\\lib\\");
Class<?> clazz1 = classLoader1.loadClass("com.itheima.my.A");
BreakClassLoader1 classLoader2 = new BreakClassLoader1();
classLoader2.setBasePath("D:\\lib\\");
Class<?> clazz2 = classLoader2.loadClass("com.itheima.my.A");
System.out.println(clazz1 == clazz2);
Thread.currentThread().setContextClassLoader(classLoader1);
System.out.println(Thread.currentThread().getContextClassLoader());
System.in.read();
}
重写loadClass方法,删除双亲委派机制,如果是java开头的jar包,就交给原先父类的loadClass,如果是自定义的,就自己直接加载。
自定义类加载器没指定双亲的话,默认双亲为应用程序类加载器
4.3.1.1 Arthas展示类的详细信息
sc -d com.itheima.my.A
因为刚刚用两个不同的类加载器加载com.itheima.my.A,自然得到两个不同的对象
4.3.1.2 正确的自定义类加载器
4.3.2 线程上下文类加载器
DriverManager类位于rt.jar包中,由启动类加载器加载。
4.3.2.1 SPI机制
SPI全程Service Provider Interface,是JDK内置的一种服务提供发现的机制
需要在resources目录下新建META-INF/services目录,并且在这个目录下新建一个与上述接口的全限定名一致的文件java.sql.Driver的接口,在这个文件中写入接口的实现类的全限定名com.mysql.cj.jdbc.Driver。
ServiceLoader这个类的源码如下:
public final class ServiceLoader<S> implements Iterable<S> {
//扫描目录前缀
private static final String PREFIX = "META-INF/services/";
// 被加载的类或接口
private final Class<S> service;
// 用于定位、加载和实例化实现方实现的类的类加载器
private final ClassLoader loader;
// 上下文对象
private final AccessControlContext acc;
// 按照实例化的顺序缓存已经实例化的类
private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private java.util.ServiceLoader.LazyIterator lookupIterator;
// 私有内部类,提供对所有的service的类的加载与实例化
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
String nextName = null;
//...
private boolean hasNextService() {
if (configs == null) {
try {
//获取目录下所有的类
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
//...
}
//....
}
}
private S nextService() {
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//反射加载类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
}
try {
//实例化
S p = service.cast(c.newInstance());
//放进缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
//..
}
//..
}
}
}
在线程中使用
Thread.currentThread().getContextClassLoader()
默认获得的是应用程序类加载器
4.3.2.2 总结
所以说,打破双亲委派机制的唯一方法就是重写loadClass或者findClass方法。
4.3.3 热部署
-
启动SpringbootClassfileApplication后,使用arthas进入,反编译出UserController
java -Dfile.encoding=UTF-8 -jar arthas-boot.jar
jad --source-only com.itheima.springbootclassfile.controller.UserController > "D:\Code\JavaCode\JVM\hot-replace\UserController.java"
修改为
if (type.equals(UserType.REGULAR.getType())) {
-
编译成字节码文件,如果直接编译,则会因为找不到类加载器而报错,所以需要先找到UserController.java的类加载器,获取其哈希值。
sc -d com.itheima.springbootclassfile.controller.UserController
-
mc -c
指定类加载器的哈希码-d
指定输出目录mc -c 18b4aac2 "D:\Code\JavaCode\JVM\hot-replace\UserController.java" -d "D:\Code\JavaCode\JVM\hot-replace"
-
通过retransform
retransform "D:\Code\JavaCode\JVM\hot-replace\com\itheima\springbootclassfile\controller\UserController.class"
-
用jad查看热部署是否完成
-
发送http请求
说明热部署已经完成
4.3.3.1 热更新注意事项
4.4 JDK8之后的类加载器
应用类加载器和扩展类加载器都是继承关系从URLClassLoader变为BuiltinClassLoader,没有太大的区别。