文章目录
- [第9章 类加载及执行子系统的案例与实战](#第9章 类加载及执行子系统的案例与实战)
-
- [9.0 个人感悟](#9.0 个人感悟)
- [9.1 概述](#9.1 概述)
- [9.2 案例分析](#9.2 案例分析)
-
- [9.2.1 Tomcat:正统的类加载器架构](#9.2.1 Tomcat:正统的类加载器架构)
- [9.2.2 OSGi:灵活的类加载器架构](#9.2.2 OSGi:灵活的类加载器架构)
- [9.2.3 字节码生成技术与动态代理的实现](#9.2.3 字节码生成技术与动态代理的实现)
- [9.2.4 Backport工具:Java的时光机器](#9.2.4 Backport工具:Java的时光机器)
- [9.3 实战:自己动手实现远程执行功能(略过)](#9.3 实战:自己动手实现远程执行功能(略过))
第9章 类加载及执行子系统的案例与实战
9.0 个人感悟
1. 夸一夸这本书,理论与实践结合。 这本书前面讲了很多"硬核"知识,包括.class文件结构、类加载器的双亲委派模型、方法调用的分派机制、栈帧结构等等,那时候的感受是这八股有啥用。这章用了Tomcat、OSGi、动态代理、远程执行四个案例,把前面技术全部串了起来,一下子让那些抽象的概念有了具体的应用场景。这种从理论到实践的闭环,才是真正把知识内化的过程。
2. 再会Tomcat。 第一次接触Tomcat只知道它是一个Web服务器、怎么部署项目以及那只猫的故事。再相见时发现原来它有很多优秀的设计。"多个Web应用需要共享库又不能相互干扰"背后牵扯到类加载器的隔离、共享、安全性、热部署四个维度的考量。它通过多层次的类加载器结构和精心设计的委派关系,优雅地解决了这些问题。这种设计思路对多租隔离有很大启发。
3. 又见动态代理。 以前用Spring AOP、用JDK动态代理以及设计模式中的代理模式,只觉得是"黑魔法",知道它能增强方法,但不知道它为什么能做到。学完这章才明白,Proxy在运行时动态生成了一个实现了目标接口的代理类的字节码,然后通过类加载器加载这个新生成的类。学习是个不断深入的过程。代理模式可以参考设计模式学习(14) 23-12 代理模式
9.1 概述
在Class文件格式与执行引擎这部分内容中,用户的程序能直接参与的内容并不太多------Class文件以何种格式存储、类型何时加载、如何连接,以及虚拟机如何执行字节码指令等,都是由虚拟机直接控制的行为,用户程序无法对其进行改变。能通过程序进行操作的,主要是字节码生成 与类加载器这两部分的功能。
下面是一些典型案例,展示类加载器和字节码生成技术在实际工程中的应用。
9.2 案例分析
9.2.1 Tomcat:正统的类加载器架构
主流的Java Web服务器,如Tomcat、Jetty、WebLogic、WebSphere等,都实现了自己定义的类加载器,而且一般都不止一个。这是因为一个功能健全的Web服务器,需要解决以下四个核心问题:
1. Web应用程序的类库隔离
部署在同一个服务器上的两个Web应用程序所使用的Java类库必须能相互隔离。两个不同的应用程序可能依赖同一个第三方类库的不同版本(例如App1用Spring 4.x,App2用Spring 5.x),服务器必须保证它们互不干扰。
2. Web应用程序的类库共享
某些类库可以被多个应用程序共享。例如,10个使用Spring框架的应用部署在同一台服务器上,如果每个应用都各自存放一份Spring JAR包,加载到内存后会严重浪费方法区空间,甚至引发内存过度膨胀。
3. 服务器自身的安全隔离
服务器本身也是用Java实现的,也有自己的类库依赖。基于安全考虑,服务器使用的类库应与应用程序的类库互相独立,防止应用中的恶意代码影响服务器运行。
4. 支持JSP的热替换(HotSwap)
JSP文件最终会被编译成Java Class才能执行,由于其纯文本存储的特性,运行时修改的概率远高于普通Java类。Web服务器需要支持JSP生成类的热替换,实现修改后无须重启的体验。
为了解决这些问题,Tomcat设计了多层次的目录结构和对应的类加载器。在经典的Tomcat架构中,有以下四组目录:
| 目录 | 加载器 | 可见范围 |
|---|---|---|
/common/* |
CommonClassLoader | Tomcat + 所有Web应用 |
/server/* |
CatalinaClassLoader | 仅Tomcat自身 |
/shared/* |
SharedClassLoader | 所有Web应用(Tomcat不可见) |
/WEB-INF/* |
WebappClassLoader | 仅当前Web应用 |
这些自定义类加载器按照双亲委派模型组织,形成如图所示的层次结构:CommonClassLoader作为CatalinaClassLoader和SharedClassLoader的父加载器,而每个Web应用都有独立的WebappClassLoader实例,彼此隔离。
![[9.2.1 tomcat 类加载器结构.png]]

JSP热替换的实现原理:每个JSP文件编译出来的Class都由独立的JasperLoader加载。当服务器检测到JSP文件被修改时,直接丢弃旧的JasperLoader实例并新建一个,从而加载新版本的JSP类,实现热替换。
补充说明 :Tomcat 6.x之后做了简化,默认将/common、/server、/shared三个目录合并为一个/lib目录。只有显式配置server.loader和shared.loader才会恢复经典架构。理解这一点有助于避免照搬书中配置时遇到困惑。
9.2.2 OSGi:灵活的类加载器架构
Java程序社区流传着这样一句话:"学习JEE规范,去看JBoss源码;学习类加载器,就去看OSGi源码。
"OSGi(Open Service Gateway Initiative)是OSGi联盟制定的一个基于Java语言的动态模块化规范,在Java程序员中最著名的应用案例就是Eclipse IDE。
OSGi与双亲委派模型的本质区别
传统的双亲委派模型是树形结构 ------加载请求自下而上逐级委派,层级关系固定。而OSGi的Bundle类加载器之间是一种网状结构 ------每个Bundle都可以声明它所依赖的Java Package(通过Import-Package),也可以声明它允许导出的Java Package(通过Export-Package)。当一个Bundle需要某个Package时,加载请求会被委派给声明导出了该Package的Bundle来完成。

这种设计带来的核心优势:
- 精确的可见性控制:一个模块里只有被Export过的Package才能被外界访问,其他Package和Class完全隐藏。
- 模块级热插拔:可以只停用、重新安装或启动应用程序的一部分,而不需要整体重启。
- 平级依赖:依赖关系从"上层模块依赖下层模块"转变为"平级模块之间的依赖",更符合微服务化的设计思想。
OSGi的代价 :
灵活性的背后是复杂度。OSGi的网状依赖带来了线程死锁和内存泄漏的风险,也让问题的排查变得困难。这也解释了为什么JDK 9的模块化系统(JPMS)没有完全照搬OSGi的设计,而是在兼容性和简洁性之间做了不同的权衡。
9.2.3 字节码生成技术与动态代理的实现
字节码生成技术的应用场景非常广泛:编译时植入的AOP框架、动态代理、反射、CGLib等底层都离不开它。其中,动态代理是理解字节码生成技术最直观的入口。
动态代理的核心原理
"动态"二字的意思是:当原始类和接口还未知时,就已经确定了代理类的代理行为。JDK提供了java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler来实现这一机制:
Proxy.newProxyInstance()接收目标接口、类加载器和InvocationHandler。Proxy在运行时动态生成一个实现了目标接口的代理类的字节码(通过generateProxyClass()方法)。- 通过指定的类加载器将这个新生成的类加载到JVM中。
- 代理对象的方法调用都会被转发到
InvocationHandler.invoke(),在此处实现增强逻辑。
代码示例:JDK动态代理示例
java
public class DynamicProxyTest {
// 接口
interface Subject {
void request();
}
// 实现
static class RealSubject implements Subject {
public void request() {
System.out.println("real request");
}
}
static void main() {
// 代理处理器
InvocationHandler handler = new InvocationHandler() {
private Object target = new RealSubject();
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
// 增强逻辑
System.out.println("before");
Object result = method.invoke(target, args);
System.out.println("after");
return result;
}
};
// 代理 核心方法Proxy.newProxyInstance
Subject proxy = (Subject) Proxy.newProxyInstance(
Subject.class.getClassLoader(),
new Class[]{Subject.class},
handler
);
proxy.request(); // 输出: before → real request → after
}
}
输出:
before
real request
after
9.2.4 Backport工具:Java的时光机器
Backport工具(如Retrotranslator)的作用是将高版本JDK编译出来的Class文件转换为可在低版本JDK上运行的格式。例如,它可以将JDK 5编译的Class文件转变为JDK 1.4或1.3可部署的版本,支持自动装箱、泛型、动态注解、枚举、变长参数、增强for循环、静态导入等语法特性,甚至还能支持JDK 5中新增的集合改进和并发包操作。
这个案例展示了字节码转换技术的另一个重要应用方向:版本兼容。通过操作字节码,可以在不改变源代码的情况下,让高版本语法特性"降级"运行在旧版JVM上。
9.3 实战:自己动手实现远程执行功能(略过)
书中9.3节详细实现了一个"在远程服务器上临时执行Java代码"的功能,通过自定义类加载器加载上传的Class文件,并修改字节码中的常量池来劫持System.out输出,从而获取执行结果。这个案例完整展示了类加载器和字节码修改技术的综合运用。
考虑在今天已经有了更加成熟和优雅的解决方案,这里就不记录了。最典型的就是阿里巴巴开源的Arthas(阿尔萨斯),它是一套强大的Java诊断工具,可以attach到目标JVM进程,通过Instrumentation API动态修改字节码,实现方法监控、热替换、在线调试等功能。感兴趣可以了解了解。