ClassLoader, JavaAgent, Aspectj Weaving一站式扫盲帖

原文:http://calvin1978.blogcn.com/articles/classloader-javaagent.html

最近工作里复习的Class Loader基础知识集锦,写下来希望对别人有帮助,而且不止是为了撂倒面试官。

为了尽量简单明了容易背,有些部分写得比较干。

0. 参考资料:

1. Class装载的三个阶段

1.1 载入 (Load)

从Class文件或别的什么地方载入一段二进制流字节流,把它解释成永久代里的运行时数据结构,生成一个Class对象。
同时也会装载它的父类或父接口,并进行校验,比如编译版本不对会抛出UnsupportedClassError,依赖定义不对会抛 IncompatibleClassChangeError/ClassCircularityError等。

1.2 链接 (Resolve)

将之前载入的数据结构里的符号引用表,解析成直接引用。
中间如果遇到引用的类还没被加载,就会触发该类的加载。
可能JDK会很懒惰的在运行某个函数实际使用到该引用时才发生链接,也可能在类加载时就解析全部引用。

1.3 初始化 (Initniazle)

初始化静态变量,并执行静态初始化语句。

2. Class装载的时机

  1. ClassLoader.loadClass()
  2. 前文所说的链接时触发的装载
  3. Class.forName() 等java.lang.reflect反射包
  4. new 构造对象
  5. 初始化子类时,会同时初始化父类
  6. 访问类的静态变量或静态方法(但static final的常量除外,此君在常量池里)

本质上,也是很懒惰的按需加载的,由于类装载的Lazy和前面解释引用的Lazy,所以Jar包里有时候有些类用到的了没在Class Path里的其他类,也能人品爆发的照跑不误。

除了时机1,其他几种方式默认都到达类装载的初始化阶段。

3. ClassLoader.loadClass() 与 Class.forName()

ClassLoader.loadClass(String name, boolean resolve),其中resolve默认为false,即只执行类装载的第一个阶段。

Class.forName(String name, boolean initialize, ClassLoader loader), 其中initialize默认为true,即执行到类装载的第三个阶段。

4. ClassNotFoundException 和 NoClassDefFoundError

ClassLoader.loadClass() 与 Class.forName() 找不到类定义的二进制流时抛出ClassNotFoundException。

链接阶段解释引用失败,找不到引用的类时抛出NoClassDefFoundError。

其他常见错误包括编译版本不对的

5. ClassLoader及双亲委派机制

ClassLoader.loadClass()的标准流程:

  1. findLoadedClass() 查看类是否已加载
  2. 如果不存在,则调用parent loader的loadClass()
  3. 如果不存在,调用findClass() 在本ClassLoader的ClassPath里加载该类

所谓双亲委派机制,就是先从parent loader开始查找,找不到了才用自己的findClass()函数去查找,兼顾了效率:避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次,和安全:避免子类乱加载。

而OSGI或SPI或热替换方案,则需要破坏这个双亲委托,先调用自己的findClass()。

findClass() 是各个ClassLoader各自实现,各显神通的地方,从各种奇葩地方载入Class二进制字节流。

但最后都会调用defineClass(),传入二进制字节流,返回Class对象。留意此处,呆会AspectJ的时候会回到这里。

在JDK6,loadClass()很过分的定义了方法级的synchronized ,在JDK7改成一个以Class Name作Key的 parallelLockMap,增强了并行加载不同Class的能力。

6. System ClassLoader 与 Thread Context Classloader

有时候,看到错误日志说张三不是张三,包名类名一样但instanceof 死活返回 false,唯一原因是它们由两个不同的ClassLoader加载。

默认的Bootstrap(加载jdk的lib目录),Extension(加载jdk的lib/ext目录),Application(加载启动时定义的classpath)三层ClassLoader机制不再重复。

平时用ClassLoader.getSystemClassLoader()就可以得到sun.misc.Launcher$ApplicationClassLoader 这个Application ClassLoader。

在类A里加载类B,默认使用加载了类A的Loader。但,也有特殊情况,比如JDBC加载driver时的机制,需要在Bootstrap ClassLoader(JDBC属于JDK一部分)里根据配置反射创建jdbc driver的数据实现类,此时Bootstrap Classloader并没有某个jdbc driver实现类的包(在Application Classloaer中),因此Sun设计了一个特殊方案 --Thread Context Class Loader。

JAXB(也是JDK一部分,比如要在Jar包里找xsd schema文件的时候)也使用了它,所以用到它们时就要注意Thread Context ClassLoader的设置,将Application Classloader设进去。可以用代码随时设置current thread的loader,也可以用自定义的ThreadFactory在创建线程时设置,它默认是父线程的loader,如果都没设置就是 System ClassLoader这个Application ClassLoader。

7. Java Agent机制与AspectJ的LoadTime Weaving

在JDK5开始,在启动JVM时可增加-javaagent参数,在装载Class时对类进行动态的修改。

AspectJ的Load Time Weaving机制,需要配置 -javaagent: [path to aspectj-weaver.jar] 。

打开aspectj-weaver.jar,可以看到META-INF/MANIFEST里定义了 Premain-Class: org.aspectj.weaver.loadtime.Agent

再打开这个Agent类,简化后的代码大概这个样子:

ClassFileTransformer s_transformer = new ClassPreProcessorAgentAdapter();

public static void premain(String options, Instrumentation instrumentation) {

instrumentation.addTransformer(s_transformer);

}

可见它的主要作用是将自己的类转换器注册到JDK所传入的Instrumentation。

再看类转换器的定义:ClassLoader会在前面defineClass()的过程中,在把二进制字节流转换为Class对象之前,先把二进制流和当前ClassLoader传给转换器,由转换器加工为另一段二进制字节流返回。

AspectJ就是利用传入的ClassLoader,找出其Class Path里的META-INF/aop.xml,然后根据aop.xml里的配置进行代码植入。

测试显示,加了LoadTime Weaving,类加载的速度明显变慢,如果是100ms就调用超时的服务,需要做类的预加载。

8. Jar包的预加载

比如有个有趣的需求是加载某个Class A所在的Jar里的全部的Class (怎么好像一点都不有趣)

URL jarUrl = ClassA.getProtectionDomain().getCodeSource().getLocation();
JarFile jarfile = new JarFile(jarUrl.getPath());
Enumeration entries = jarfile.entries();

然后遍历JarEntry,过滤出后缀为.class的文件,按类名进行装载就可以了。

9.Class的二进制兼容性

如果Class A 依赖 spring-1.0.jar编译,当spring升级到spring-2.0.jar,Class A不需要修改代码也不需要重新编译,可以直接运行的,spring-2.0.jar就满足二进制兼容性。

Java语言规范的第13章有详细的描述 ,不想直接睡着最好可以找个中文版来看,感谢那些翻译的同学。

虽然规范的这章看着比较长比较吓人,但其实二进制兼容性还是很容易做到的,只要你不做把接口改为抽象类之类或反过来这类奇怪的事情,其他一些看起来很大的改动,比如改throws定义,其实都没有问题。

真的遇到问题,设身处地想想自己是那段Class A的字节码,现在还能不能跑就行。

感谢你看到这里,希望你只在工作里用到这些知识,祝工作愉快。

文章持续修订,转载请保留原链接:
http://calvin1978.blogcn.com/articles/classloader-javaagent.html


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java Agent 是指在程序运行时,通过 Java Instrumentation API 动态修改字节码的一种技术。Java Agent 可以用于实现一些高级功能,例如监控、代码注入等。下面是一个简单的 Java Agent 的设计和编写demo: 1. 创建一个 Maven 项目,并添加以下依赖: ```xml <dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm-all</artifactId> <version>5.2</version> </dependency> ``` 2. 创建一个 Java Agent 类,并实现 premain 方法: ```java public class MyAgent { public static void premain(String agentArgs, Instrumentation inst) { // 在这里添加字节码转换器 } } ``` 3. 在 premain 方法中添加字节码转换器: ```java public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new MyClassFileTransformer()); } ``` 4. 创建一个 MyClassFileTransformer 类,并实现 transform 方法,该方法将在类被加载时被调用: ```java public class MyClassFileTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { // 在这里修改字节码 return classfileBuffer; } } ``` 5. 在 transform 方法中,可以使用 ASM 库修改字节码。例如,下面的代码将在每个类的构造函数中添加一行输出语句: ```java public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { ClassReader classReader = new ClassReader(classfileBuffer); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES); // 创建一个 ClassVisitor,用于修改字节码 ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM7, classWriter) { @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); if ("<init>".equals(name)) { // 找到构造函数 methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); methodVisitor.visitLdcInsn("Hello, World!"); methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); } return methodVisitor; } }; // 使用 ClassVisitor 修改字节码 classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES); return classWriter.toByteArray(); } ``` 6. 编译项目,并将生成的 jar 文件作为 Java Agent 运行: ```bash java -javaagent:myagent.jar -jar myprogram.jar ``` 以上就是一个简单的 Java Agent 的设计和编写demo。在实际应用中,需要根据具体的需求进行进一步的开发和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值