当前位置:网站首页>理解加载class到JVM的时机
理解加载class到JVM的时机
2022-07-24 02:33:00 【言成言成啊】
最近有看《深入理解Java虚拟机》,作者很聪明,这边直接一笔带过,跟没提一样。
甚至百度都搜不到,领域大佬直接给大众树了死标杆,由此,我自己来记录踩坑了。
纸上得来终觉浅,绝知此事要躬行,不知道说了多少次。
一、idea远程断点
1.1 远程断点基本步骤
为啥要远程断点?
我特意干掉了某个依赖,本地可以启动,打包后有bug,我还特意确定了本地也是没有引入的。我怀疑是idea偷偷给我引入了。所以要排查问题,只能远程断点排查了。
远程调试的要点是,本地的代码与远程的jar包是一致的,这样才能保证断点的行数能够对的上。
不管远程的debug还是本地的debug,都是通过远程连接到jvm执行的。至于为啥,我也不知道。
可以搜索相关的JPDA技术,JPDA 的全称是 Java Platform Debugger Architecture,即java平台的调试技术。
准备环境
- 本机机器192.168.110.199
- 本地调试工具idea2021
- 远程机器10.0.0.10
两种debugger模式
- Attach to remote JVM:远程项目先启动,本地主动连接远程已启动好的项目。可能会存在,你连接上时,你的断点已经过了。
- Listen to remote JVM:本地监听先启动,监听到远程项目启动后自动连接。推荐使用这种方式,这也是我们idea直接debug所使用的方式。


将图中生成的args复制下来,进行相应的修改。
-agentlib:jdwp=transport=dt_socket,server=n,address=192.168.110.199:54188
本地debugger模式启动,远程执行命令启动。
java -agentlib:jdwp=transport=dt_socket,server=n,address=192.168.110.199:54188 -jar class-load-1.0.0.jar
结果如图

1.2 条件断点
idea在断点处进行右键,输入条件即可,如图。

二、通过字节码文件创建对象
创建一个自定义的类加载器,用于加载One.class字节码。
One.class具体获取在第三节
/** * 自定义类加载器 * * @author chenchuancheng github.com/meethigher * @since 2022/7/22 21:58 */
public class CustomClassLoader extends ClassLoader {
private final byte[] bytes;
public CustomClassLoader(String classPath) {
try {
FileInputStream fis = new FileInputStream(classPath);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int b;
while ((b = fis.read()) != -1) {
bos.write(b);
}
this.bytes = bos.toByteArray();
bos.close();
fis.close();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 首先,检查class是否已经被加载
Class<?> c = findLoadedClass(name);
// 类名不是指定的类的,默认使用委托的父类加载器
if (!name.equals("One")) {
ClassLoader parent = getParent();
c = parent.loadClass(name);
}
//如果为空,就将字节数组转换为类
if (c == null) {
c = defineClass(name, bytes, 0, bytes.length);
}
return c;
}
}
加载效果如图

三、理解加载字节码到JVM的时机
其实到这里才是正文。
3.1 应用场景
起因,我写了一套监控数据库的基础组件,基础组件嘛,只提供最基础的功能逻辑,而不提供依赖jar包。等到有人使用我的组件时,按需引入依赖就行了。轻量可复用,多好!
打包时不将依赖打入jar包,只需maven依赖scope改为provided即可。scope默认的是compiled。
我的监控对象如下。

依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<version>2.5.2</version>
<exclusions>
<exclusion>
<groupId>org.mongodb</groupId>
<artifactId>bson</artifactId>
</exclusion>
</exclusions>
</dependency>
具体源码如下
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoIterable;
import org.bson.Document;
public class One {
public One() {
}
public void OnePrint() {
System.out.println("哈哈哈哈");
}
public void mongoInfo() {
MongoClient client = MongoClients.create();
MongoDatabase db = client.getDatabase("test");
MongoIterable<String> collectionNames = db.listCollectionNames();
String str = String.format("{collStats : '%s'}", "test");//B为单位
Document document = Document.parse(str);
document = db.runCommand(document);
System.out.println(document);
}
}
我想实现的效果是,即使我没有使用的那些依赖,仍然能new One()成功。等到实际调用mongoInfo()时,如果没有依赖再报错嘛。
但是,我new One()时,就已经触发了Bson的class加载到JVM。
为了更能直观的查看结果,我直接加载One的字节码。
启动命令带上参数-verbose:class表示查看类加载明细。
为啥要加载字节码?因为用IDEA启动,在没有依赖时,会有编译检查,没法直接运行。
我想模拟没有依赖的环境,两种办法都可行,一种是远程连接jar包,一种是加载字节码。
当然也可以配置idea跳过编译检查,麻烦。

到这里,我就好奇了,为啥Bson还没调用的时候,就load到jvm了?而mongo就不会这样?
为了验证上面的问题,我又加入Bson依赖,再次查看类加载结果,确实没有加载mongo。

这边查询《深入理解Java虚拟机》,作者很聪明,只讲了类初始化时机,类加载的触发条件,闭口不讲。

3.2 bug明晰
先明确几个概念。
类加载:字节码load到jvm
类初始化:碰到new、反射invoke、直接调用类的静态变量or方法时等等,会触发类的初始化。一定程度上这也可以理解成类加载的触发条件,因为类加载后才会初始化嘛。
对象实例化:new Object(),这就表示了实例化了一个类型为Object的对象。
顺序:类加载 -> 类初始化 -> 对象实例化
实例化上述代码中的One对象时,由于One中引用了Document以及MongoClient等。
其中Document实现自Bson接口,MongoClient实现自Closeable接口。
通过启动时,查看类加载过程,可知,虽然只是引用、尚未调用,顶级父类仍然会加载到jvm,即进行类加载。
通过注释掉其中一行涉及到多态的写法,就不会触发Bson的类加载了。

总结来说,**类A被调用时,类A引用到的类B直接涉及的父类/父接口(比如多态写法)会立即执行类加载。**而引用的类B本身,通常(父级为抽象类时为特殊情况)在被调用时,才会触发其类加载。所以这个问题,只要使用非多态写法即可解决。
非多态写法的类,是需要调用,才会触发加载。
多态写法的,引用到的抽象类和接口的加载情况略有不同。
经测试,父级只要引用到,即使没有调用也会立即加载。对于父级是抽象类的,还会带着子类一同加载。对于父级是接口的,子类只会在被调用时加载。
3.3 思想验证
已经明确的是,非多态写法只有使用到时才会触发类加载。
至于父级抽象类和接口的加载时机,需要测试。
One继承自抽象类AbstractClass,Two实现自InterfaceClass。
public class Three {
public void test() {
InterfaceClass aClass = new Two();
}
public void test1() {
AbstractClass aClass = new One();
}
public static void main(String[] args) {
new Three();
}
}
如上案例项目启动,没有引用到父级方法时,jvm只会加载Three。

但是如果引用到父级的方法。
public class Three {
public void test() {
InterfaceClass aClass = new Two();
aClass.test();
}
public void test1() {
AbstractClass aClass = new One();
aClass.test();
}
public static void main(String[] args) {
new Three();
}
}

可以看到引用到父级方法时,
如果父级是接口,则jvm只会加载接口。
如果父级是抽象类,则jvm会加载抽象类和实现类。
3.4 解决方案
我所说的解决方案是不加依赖的情况下。
第一,非多态写法。这个不可行,毕竟这是别人包里的api。
第二,**将类的引用,放到另外一个类,利用类本身就是懒加载的特性,实现延迟加载。**该思路借鉴单例模式的延迟初始化占位类思想。
代码如下
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoIterable;
import org.bson.Document;
public class One {
public One() {
}
public void OnePrint() {
System.out.println("哈哈哈哈");
}
public void mongoInfo() {
MongoClient client = MongoClients.create();
MongoDatabase db = client.getDatabase("test");
MongoIterable<String> collectionNames = db.listCollectionNames();
String str = String.format("{collStats : '%s'}", "test");//B为单位
System.out.println(OneHolder.hold(db, str));
}
private static final class OneHolder {
/** * 这样就相当于将引用放到了OneHolder这个类这里 * 只有OneHolder被调用时,才会触发引用到的类的父级的加载 * * @param db * @param str * @return */
private static Document hold(MongoDatabase db, String str) {
Document document = Document.parse(str);
return db.runCommand(document);
}
}
public static void main(String[] args) {
new One();
}
}
类的懒加载:类被调用时触发类加载,用到才会加载。
单例的懒加载:类加载完成后,等到初次获取实例时,进行对象的实例化。
单例的急加载:类加载并执行初始化时就立即进行对象的实例化。
3.5 有趣案例
看代码
public class Base {
private String baseName = "base";
static {
System.out.println("Base静态代码块执行");
}
public Base() {
System.out.println("Base构造函数执行");
callName();
}
public void callName() {
System.out.println(baseName);
}
static class Sub extends Base {
private String baseName = "sub";
static {
System.out.println("Sub静态代码块执行");
}
public Sub() {
System.out.println("Sub构造函数执行");
}
public void callName() {
System.out.println(baseName);
}
}
public static void main(String[] args) {
Base b = new Sub();//null
}
}
运行结果是
Base静态代码块执行
Sub静态代码块执行
Base非静态代码块执行
Base构造函数执行
null
Sub非静态代码块执行
Sub构造函数执行
通过多态形式实例化Sub,加载顺序如下。
- 父类的静态代码块和静态变量
- 子类的静态代码块和静态变量
- 父类的非静态代码块和非静态变量
- 父类的构造函数
- 子类的非静态代码块和非静态变量
- 子类的构造函数
子类Sub没有构造函数,所以就是默认的public new Sub(){}
由于子类重写了父类的callName方法,所以执行父类构造函数里面callName实际是获取子类的baseName,而此时子类的变量还没有进行加载,所以为null
四、参考致谢
idea,使用Remote 连接tomcat,远程DEBUG模式调试_可乐cc呀的博客-CSDN博客_idea tomcat 远程debug
IDEA远程断点调试jar包项目_单手入天象的博客-CSDN博客_idea 断点进入jar包
基于Java动态编译实现springboot项目动态加载class文件的一些经历和思考_追风小勺年的博客-CSDN博客_springboot 动态加载类
Java多态时类的加载顺序_奋起直追CDS的博客-CSDN博客
spring-boot-starter-undertow和tomcat的区别_芭比萌妹的博客-CSDN博客_undertow和tomcat的区别
边栏推荐
- Resumption: a deck of cards (54), three people fighting the landlord, what is the probability that the big and small kings are in the same family
- 【补题日记】[2022牛客暑期多校1]J-Serval and Essay
- Essential skills for programmers -- breakpoint debugging (idea version)
- Today's code farmer girl learned about the express framework under node
- Data conversion problem in Qt development serial communication software: when reading_ Qbytearray to string; When sending_ Data format; Int to hexadecimal format string; Intercept characters in string
- [diary of supplementary questions] [2022 Niuke summer multi school 2] k-link with bracket sequence I
- MySQL---four JDBC
- Installation, configuration and use of sentry
- Canvas drawing (mouse click to draw and lift to end)
- 输入cnpm -v出现cnpm : 无法加载文件 C:\Users\19457\AppData\Roaming\npm\cnpm.ps1,因为在此系统上禁止运行脚本。
猜你喜欢

22 -- 二叉搜索树的范围和

BPG notes (III)

Use the pagoda panel to plan tasks and automatically push the website to Baidu for inclusion

Responsive layout a web page displays different effects on different devices) meta:vp
![[untitled]](/img/57/a3104833cb5fcc05916075f3bfdaf3.png)
[untitled]

LeetCode 70爬楼梯、199二叉树的右视图、232用栈实现队列、143重排链表

MySQL---four JDBC

Doodle Icons - 一组免费商用的涂鸦风格图标库,可爱轻快又独特
![[diary of supplementary questions] [2022 Niuke summer school 1] i-chiitoitsu](/img/be/47b8a86399f760e7cd6181528884c6.png)
[diary of supplementary questions] [2022 Niuke summer school 1] i-chiitoitsu

Chinese scientists have made new progress in high security quantum key distribution networks
随机推荐
[diary of supplementary questions] [2022 Hangdian summer school 1] b-dragon Slayer
About offline use of SAP Fiori application
[untitled]
Réalisation d'un diagramme de zone de ligne brisée - Diagramme Rose - Diagramme à barres
Mysql数据库,排序与单行处理函数篇
Wechat applet realizes broken line area chart rose chart three-dimensional histogram
Sharing a case of controller restart caused by a CIFS bug in NetApp Fas series
Codeworks 5 questions per day (average 1500) - day 23
分享一个基于Abp 和Yarp 开发的API网关项目
The combination sum of C language power deduction question 39. Backtracking method and traversal method
Force open web page
Ggplot2 displays png
Vscade connects to the server. The password is correct, but it has been unable to connect
[knowledge atlas] practice -- Practice of question and answer system based on medical knowledge atlas (Part2): Atlas data preparation and import
Wallys/DR4019S/IPQ4019/11ABGN/802.11AC/high power
Mysql database, sorting and single line processing functions
認識傳輸層協議—TCP/UDP
Responsive layout a web page displays different effects on different devices) meta:vp
Combined with actual combat, analyze gb/t 28181 (II) -- equipment directory synchronization
Responsive pbootcms template decoration design website