Class 文件格式、执行引擎主要以 Class 文件描述了存储格式、类何时加载、如何连接、VM 如何执行字节码指令,这些动作基本都是 JVM 直接控制,用户代码无法干预和改变;
用户可以干预的只有字节码生成、类加载器两部分,而这两部分的应用是许多常用功能和程序实现的基础;
一个功能完备的 Web 服务器(Tomcat、Jetty、WebLogic、WebSphere),都要解决如下问题:
单独的一个 ClassPath 无法满足 Web 应用的部署需求,因此 Web Server 各自提供了不同含义的第三方类库 ClassPath(lib、classes,各自对应不同的访问范围和服务对象),每个路径需要相应的自定义类加载器去加载里面的 Java 类库;
Tomcat 目录
/common/*
,Common 类加载器负责,类库可以被 Tomcat 和所有 Web 应用程序共同使用;/server/*
,Catalina 类加载器(也称 Server 类加载器)负责,类库可被 Tomcat 使用,对所有 Web 应用程序不可见;/shared/*
,Shared 类加载器负责,类库可以被所有 Web 应用程序共同使用,对 Tomcat 不可见;/lib/*
,Tomcat 6 之后,默认不开启 server.loader 和 share.loader 两个类加载器,而是用 Common 类加载器替代,因此 /common/*
、/server/*
、/shared/*
三个路径默认合并到了一个 /lib/*
路径,相当于 /common/*
的作用,/WEB-INF/*
,Webapp 类加载器负责,每个 Web 应用程序对应一个 Webapp 类加载器,类库仅可被当前 Web 应用程序使用,对 Tomcat 和其他 Web 应用程序不可见;JSP 类加载器负责 JSP 文件的加载,每个 JSP 文件对应一个 JasperLoader 类加载器;而 JSP 加载器存在的目的就是被丢弃,一旦 JSP 文件被修改,就需要新建一个 JSP 类加载器替换旧的,以实现 JSP 文件的 HotSwap 功能;
Common 类加载器能够加载的类可以被 Catalina 类加载器和 Shared 类加载器使用;Catalina 类加载器和 Shared 类加载器加载的类相互隔离;Webapp 类加载器可以使用 Shared 类加载器加载的类,但各 Webapp 类加载器加载实例之间相互隔离;
OSGi
的研究有助于学习类加载器的知识;
OSGi
,Open Service Gateway Initiative
,OSGi 联盟制定的一个基于 Java 语言的动态的模块系统规范(JDK 9 引入的 JPMS 是静态的模块系统);因与 JPMS 模块化特性重叠,OSGi 现在着重向动态模块化系统的方向发展,用于实现模块级热插拔等;许多大型软件平台和中间件服务器都是基于或声明将要基于 OSGi 规范来实现的(如 IBM Jazz、GlassFish、JBoss OSGi 等);
Bundle
,OSGi 的模块,类似于普通 Java 类库,且都是以 JAR 格式封装,内部都是 Java 的 Package 和 Class;但 Bundle 可以额外声明它所依赖的 Package(Import Package)和它允许导出发布的 Package(Export Package,未 export 的 package 和 Class 对外将是不可见的),用于精准控制可见性和依赖关系;
OSGi
的 Bundle 类加载器之间只有规则,没有固定的委派关系;所有 Package 的类加载动作都会委派给发布它的 Bundle 类加载器来完成;不涉及某个具体 Package 时,所有 Bundle 加载器都是平级关系,只有具体到某个 Package 或 Class 时,才根据 Package 导入导出定义来构造 Bundle 间的委派和依赖;
假设存在 Bundle A、Bundle B、Bundle C,且 3 个 Bundle 的依赖关系如下:
Bundle A
: 声明发布了 packageA,依赖了 java.* 的包;Bundle B
: 声明依赖了 packageA 和 packageC,同时也依赖了 java.* 的包;Bundle C
: 声明发布了 packageC,依赖了 packageA;3 个 Bundle 之间的类加载器及父类加载器之间的关系如下:
若 Bundle A 依赖 Bundle B 的 Package B,Bundle B 依赖 Bundle A 的 Package A;在类加载时,首先加载 Bundle A 时会锁定 Bundle A 的类加载器实例,然后委派 Bundle B 的类加载器去加载 Bundle B;而此时加载 Bundle B 的线程也会锁定 Bundle B 的类加载器实例,再去请求 Bundle A 的 Package A,如此相互等待,就会形成死锁
;(这个问题在 JDK 7 之前可以通过 osgi.classloader.singleTheadLoads 参数强制单线程串行进行类加载解决,但会损耗性能;在 JDK 7 之后才从 JDK 层面得到解决,将锁的级别从 ClassLoader 对象本身降到了要加载的类名级别)
JDK 中的 javac 是字节码技术的老祖宗
,学习字节码的最好方式是阅读 javac 的源码(jdk.compiler/xhare/classes/com/sun/tools/javac
);
使用字节码的场景有 Web 服务器中的 JSP 编译器、编译时织入的 AOP、动态代理、反射也可能通过运行时生成字节码提高执行速度;
动态代理
,针对使用 Java 代码实现代理类的静态
代理写法,动态代理在原始类和接口未知的情况下,实现代理类的行为,让代理类和原始类脱离了直接关系,从而让代理可以灵活的重用在不同场景;Spring 通过动态代理的方式对 Bean 进行增强;
动态代理示例
public class DynamicProxyTest {
interface IHello {
void sayHello();
}
static class Hello implements IHello {
@Override
public void sayHello() {
System.out.println("hello world");
}
}
static class DynamicProxy implements InvocationHandler {
Object originalObj;
Object bind(Object originalObj) {
this.originalObj = originalObj;
return Proxy.newProxyInstance(originalObj.getClass().getClassLoader(), originalObj.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("welcome");
return method.invoke(originalObj, args);
}
}
public static void main(String[] args) {
IHello hello = (IHello) new DynamicProxy().bind(new Hello());
hello.sayHello();
hello.toString();
// 输出:
// welcome
// hello world
// welcome
}
}
Proxy::newProxyInstance() 方法返回一个实现了 IHello 的接口,并代理了 new Hello() 实例化的对象
;其中程序进行了验证、优化、缓存、同步、生成字节码、显示类加载等;
设置保存运行时生成的代理类字节码文件
// 在 main() 中添加如下代码
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
可以得到 #Proxy0.class 的 Class 文件,通过反编译,可以得到动态代理类;
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package edu.aurelius.jvm.clazz;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
final class $Proxy0 extends Proxy implements DynamicProxyTest.IHello {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
public $Proxy0(InvocationHandler var1) throws {
super(var1);
}
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final void sayHello() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("edu.aurelius.jvm.clazz.DynamicProxyTest$IHello").getMethod("sayHello");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
这里 super.h
是父类 Proxy 中保存的 InvocationHandler 对象(DynamicProxy 对象
),统一调用 super.h
对象的 invoke() 方法来实现代理方法,让 DynamicProxy 对象
的 invoke() 中插入的逻辑可以作用在所有被代理对象(new Hello() 实例化的对象
)的方法上;
实际开发中以字节为单位拼接字节码的应用场景几乎是很少见的,在需要大量操作字节码时,建议使用封装好的字节码类库(Javassist、GCLib、ASM);
Java Backporting Tools
,Java 逆向移植工具,如 Retrotranslator 和 Retrolambda;
Retrotranslator
,将 JDK 5 编译出来的 Class 文件转变为可以在 JDK 1.4 或 1.3 上部署的版本,从而支持自动装箱、泛型、动态注解、枚举、变长参数、遍历循环、静态导入、集合改进、并发包、泛型与注解等的反射等语法特性;Retrolambda
,将 JDK 8 的 Lambda 表达式、try-resources、接口默认方法等语法转换为在 JDK 5、6、7 部署环境上支持的形式;JDK 升级新增功能分类
逆向移植可以较好的模拟前 2 类升级,后 3 类则无能为力或影响运行效率;
对第 1 类升级的逆向移植可以通过独立类库代替 JDK 中部分功能的方式来实现;
对第 2 类升级需要使用 ASM 等字节码操作工具对字节码中元数据信息和一些语法支持的内容做相应修改;
对第 3 类升级则需要绕开字节码指令,牺牲了性能;如 lambda 表达式的逆向支持,其实是生成了一组匿名内部类代替 Lambda;IntelliJ IDEA 中将匿名内部类显示成 Lambda 表达式其实就是反向使用这个过程的效果;
上一篇:「JVM 执行引擎」栈架构的字节码的解释执行引擎
下一篇:「JVM 原理使用」实现远程服务端动态执行临时代码
PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!
参考资料: