JVM sandbox 解析

JVM-SANDBOX(沙箱)实现了一种在不重启、不侵入目标JVM应用的AOP解决方案。

JVM-SANDBOX 涉及的技术原理

一、AOP 技术

AOP(面向切面编程,Aspect Oriented Programming)技术是指对程序中的某个切面进行管理和装饰处理,实现一些功能,将业务主体与关注点进行解耦。应用广泛的 Spring 就是AOP的一种实现。

代理

代理模式是创建一个代理对象来代理原对象的行为,代理对象拥有原对象行为执行的控制权,然后基于代理对象在原对象行为执行的前后插入代码来实现 AOP。
代理分为动态代理和静态代理。静态代理就是在编译时生成一个新的代理类,该代理类增强了业务类,是在编译时增强。动态代理是运行时增强。
动态代理有:JDK、CGLib、ASM、Javasssist 等方案。

1. JDK 动态代理

JDK的动态代理是基于反射实现的,核心 API 是 Proxy 类和 InvocationHandler 接口。JDK通过反射,生成一个代理类,这个代理类是Proxy类的子类,它实现了原来那个类的全部接口,对所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,回调我们实现的InvocationHandler接口的invoke方法。

优点

1)JDK动态代理是JDK原生的,不需要任何依赖即可使用;
2)通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快;

缺点

1)如果要使用JDK动态代理,被代理的类必须实现了接口,否则无法代理;
2)JDK动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring仍然会使用JDK的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。
3)JDK动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低;

2. CGLib动态代理

CGLib实现动态代理的原理是,底层采用了ASM字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(简单理解为Spring中的切面)织入到方法中,对方法进行了增强。

优点

1)使用CGLib代理的类,不需要实现接口;
2)CGLib生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中,所有能够被子类重写的方法进行代理;
3)CGLib执行代理方法的效率要高于JDK的动态代理;

缺点

1)由于CGLib的代理类使用的是继承,这也就意味着如果需要被代理的类是一个final类,则无法使用CGLib代理;
2)由于CGLib实现代理方法的方式是重写父类的方法,所以无法对final方法,或者private方法进行代理,因为子类无法重写这些方法;
3)CGLib生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比JDK通过反射生成代理类的速度更慢;

3. ASM

底层字节码框架,操纵的级别是底层JVM的汇编指令级别,这要求ASM使用者要对class组织结构和JVM汇编指令有一定的了解。

4. Javaassit

Javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架。

Spring 动态代理的实现

Spring 默认使用JDK动态代理,只有在类没有实现接口时,才会使用 CGLib。但是可以通过配置参数强制使用CGLib 代理。
而 Spring Boot 2.X 以后,CGLib 已成为默认的代理模式。

二、Java Agent

动态字节码增强技术对应的目标都是字节码文件。也就是说在JVM加载字节码文件之前就需要对字节码文件进行修改,而一旦JVM类加载器将目标类加载了,然后此时就无法通过修改字节码文件的方式来实现动态的字节码增强了。
所以如果想要在JVM加载字节码文件之后还进行字节码增强,就需要适应到JDK提供的Instrument。Instrument通常可以配合Javaagent一块使用。

下面这些都用到了 Java Agent 技术:
1)IDEA调试功能。
2)热部署,如 Jrebel。
3)线上诊断工具,如 Btrace、Arthas。
4)各种性能分析工具,如 Visual VM、JConsole。

Java Agent 最终以 jar 包的形式存在。主要包含两个部分:
1)配置文件 MANIFEST.MF 中指明agentmain/premain方法所在类。
2)入口类实现 agentmain 和 premain 方法。

方法 premain 和 agentmain 的运行时机不一样。Premian 在JVM 启动时被加载,agentmain 在 JVM 启动后被加载。
Java Agent 有两种启动方式,一种是以 JVM 启动参数 -javaagent:xxx.jar 的形式随着 JVM 一起启动,这种情况下,会调用 premain方法,并且是在主进程的 main方法之前执行。另外一种是以 loadAgent 方法动态 attach 到目标 JVM 上,这种情况下,会执行 agentmain方法。

Premain方法中有一个参数,Instrumentation,它是 agent 技术的核心,是 Java 开放出来的专门用于字节码修改和程序监控的实现。

三、JVMTI

JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。
JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口,这些接口可以供开发者扩展自行的逻辑。
比如我想监听JVM加载某个类的事件,那么我们就可以实现一个回调函数赋给 jvmtiEnv 的回调方法集合里的 ClassFileLoadHook(Class类加载事件),那么当JVM进行类加载时就会触发回调函数,我们就可以在JVM加载类的时候做一些扩展操作。
主要方法:
1)Agent_OnLoad方法:如果agent是在启动时加载的,那么在JVM启动过程中会执行这个agent里的Agent_OnLoad函数(通过-agentlib加载vm参数中)。
2)Agent_OnAttach方法:如果agent不是在启动时加载的,而是attach到目标程序上,然后给对应的目标程序发送load命令来加载,则在加载过程中会调用Agent_OnAttach方法。
3)Agent_OnUnload方法:在agent卸载时调用。

Instrument 就是一种 JVMTIAgent,它实现了Agent_OnLoad和Agent_OnAttach两个方法,也就是在使用时,Instrument既可以在启动时加载,也可以再运行时加动态加载。
启动时加载就是在启动时添加JVM参数:-javaagent:XXXAgent.jar的方式。
运行时加载是通过JVM的attach机制来实现,通过发送load命令来加载。

四、Attach 机制

JDK1.6之后提供了attach机制,工具类都处于com.sun.tools.attach包下,可以通过VirtualMachine.attach方法attach到指定JVM进程上,然后获取到指定JVM的虚拟机对象VirtualMachine。
JVM进程有两个用于attach机制的线程,一个Signal Dispatcher线程,用于处理信号;一个Attach Listener线程,用于JVM进程之间的通信。
另外如果一个JVM被其他进程attach,那么该JVM的Signal Dispatcher线程会处理信号并启动Attach Listener线程。
Attach Listener 线程的主要工作是串流程,流程步骤包括:接收客户端命令、解析命令、查找命令执行器、执行命令等等。

JVM-SANDBOX 实现

启动设计

1)客户端通过 Attach 将沙箱挂载到目标 JVM 进程上。
2)沙箱的启动实际上是依赖 Java Agent。
3)启动之后沙箱会一直维护着 Instrument 对象引用,在沙箱中 Instrument 对象是一个非常重要的角色,它是沙箱访问和操作 JVM 的唯一通道,后续修改字节码和重定义类都要经过 Instrument。
4)另外,沙箱启动之后同时会启动一个内部的 Jetty 服务器,这个服务器用于外部进程和沙箱进行通信。启动时设置的参数实际就是以 http 请求方式交互的。

隔离设计

JVM SandBox 定义了自己的类加载器,严格控制类的加载,沙箱的核心类加载器有两个:SandBoxClassLoader 和 ModuleJarClassLoader。SandBoxClassLoader 用于加载沙箱自身的工作类,ModuleJarClassLoader 用于加载三方自己开发的模块功能类。
通过类加载器,沙箱将沙箱代码和业务代码以及不同沙箱模块之间的代码隔离开来。
这样,不仅实现了“可插拔”所需的隔离,也实现了多租户之间的隔离。

posted @ 2023-02-14 16:24  风小雅  阅读(784)  评论(0编辑  收藏  举报