【实战JVM】-基础篇-02-类的声明周期-加载器
- 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之后的类加载器
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,没有太大的区别。