赵玉伟的博客

从类加载到动态编译

类加载

编译: java的源文件(.java)首先会被编译器编译成固定格式的二进制文件(.class),这是编译阶段,大部分情况下, 这部分编译会先于加载前执行; 部分情况下, 会在运行阶段“按需”编译, 称为动态编译。
加载:class文件生成后,可能存在磁盘上,可能存在于网络的某个位置上。 接下来,会进入加载阶段, 该阶段就是jvm去“读”这些标准格式的class文件, 然后将其解析,执行<cinit>指令(类的初始化),分配常量池池内存和方法区内存、生成class对象。加载会“按需加载”,具体来说就是new对象,获取static(getstatic)方法或属性,存储static(putstatic)属性时,会加载所需的类。
执行:执行<init>指令(对象初始化), 对象在内存中生成后,就可以执行业务逻辑,直到被垃圾回收器回收之前,会一直驻留在内存。
卸载:加入我们的内存无限大,理论上讲对象是不需要被回收的,但是由于内存有限,所以当对象被持续的分配,内存被占用到一定程度后,垃圾回收器会开始工作把不再使用的对象回收,腾出内存空间给将要分配的对象。一张简图:

生命周期

一个对象的生命周期如下:

触发时机

网上以及在《jvm虚拟机》中有很多关于生命周期的描述,在这里简要描述描述下:
类记载时机:
1、jvm遇到new、 getstatic、 putstatic、invokestatic四条指令时;
2、使用java.lang.reflect包的方法,进行反射调用时(在某个对象上执行某个方法,方法属于类,不属于对象);
3、父类先于子类加载, A类依赖B类,B类先于A类加载;
4、子类引用父类的静态字段, 父类会被加载并且初始化,子类不会;对于静态字段, 只有直接定义该字段的类会被初始化;
5、通过数组引用类,不会触发该类的加载;
6、常量(public static final )在编译阶段会被放倒常量池中(在class文件中,会有一段常量池的描述),所以常量和类加载无关,只有直接引用到定义常量的类时,才触发常量所在类的初始化。

连接

验证、准备、解析总称为连接

验证

jvm需要确保class文件的正确性,要对class进行严格校验, 校验也是jvm对自身的保护。
1、文件格式的校验: 确保class的文件格式符合jvm的规范,比如:前四个字节、常量字节、方法字节……
2、元数据校验:格式ok之后,确保在格式范围内填充的数据的数据类型符合jvm的语法规范;
3、对数据流河控制流的校验分析,该阶段主要面向方法,确保方法在运行时不会危害到jvm的安全性,只能在“容许的范围”内运行
4、符号引用校验:讲类内的符号引用转化成直接引用(class中的符号是对外部类的引用的描述, 需要验证类与类之间的完整性),确保类之外的信息的正确性;

准备

验证ok后,class文件达到可用的状态,需要对类内部的属性(该阶段只分配类变量)分配默认值,8个基本类型和引用类型不一一列举,默认值如下:0、0L、null、false等。 注意: 只是分配类的默认值, 比如: public static int a = 123, 只是分配a = 0, 不是123; 另外, 实例变量的值不会被分配。

解析

解析其实是替换,可以参考验证中的第4条;就是将已经分配常量池的的基础上,将符号替换为直接引用的过程。 涉及到:类解析、接口解析、字段解析、方法解析等。

类初始化

类的初始化,执行类的 指令; 按照在类中定义的顺序, 给类变量分配值, 执行静态语句块;父类先于子类执行;如果类中没有定义静态属性或者静态块, 这一步不需要执行。

以上流程走完之后, 一个对象的meta信息生成完成, 之后便可以通过meta信息构建出我们要使用的实例。

加载模式

采用双亲委派模型,为了确保唯一性,也可以理解成成“同源策略”, 这里的同源指的是是否由同一个类加载器加载。 试想, 对于一个对象来说, 由bootstrap classloader加载和由自定义的类加载器加载是不想等的。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

动态编译

大部分情况,先编译、然后再加载运行可以满足业务场景。不过,在一些容器或者框架、或者序列化的协议中,有时候程序启动的时候, 只是提供了处理流程,对于将要使用的类并不清楚,这个时候, 需要动态编译,即在运行的时候,按需生成需要的类的二进制文件,然后执行加载。比如:在动态代理中,client只是拿到了接口的class,而接口是不能被实例化了,此时,需要根据接口生成对应的二进制文件,然后实例化。参看以下code,出处sun.misc.ProxyGenerator.generateProxyClass:

1
2
3
4
5
6
7
8
9
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
dout.writeInt(0xCAFEBABE);
dout.writeShort(CLASSFILE_MINOR_VERSION);
dout.writeShort(CLASSFILE_MAJOR_VERSION);
......
for (FieldInfo f : fields) {
f.write(dout);
}

以上代码就是按照class文件的格式,生成一个二进制文件。 能够完成这一任务,需要对class文件的格式(包括出现的顺序)非常了解,稍微有一点点不慎,就会出错。 所以,这种动态编译生成class文件的方式慎用,因为效率低下,容易出错,而且一旦生成改动的成本不亚于新编译一个文件。所以,除了这种最原始的机制外,还有以下几个成熟并且比较常用工具可以完成动态创建类或者扩展类的工作。 javassist、 ASM、cglib 。 javassist提供了api,在使用的时候可以像操作普通对象一样生成所需的类,所以,其是在二进制的基础上又抽象了api层,效率上比直接操作字节码性能上会差一些, 具体差多少要经过测试。而cglib基于ASM,其实就是对ASM做了一次封装,直接操作字节码,spring中的AOP机制,提供了两种方式的实现, 一种是java的动态代理、另外一种是基于cglib,cglib的优势是proxy不需要像动态代理一样必须实现接口。

javassist

javassist的使用细节可以搜索一下,几个比较重要的抽象:
ClassPool: 容器,类似于工厂
ClassPath: classpath
CtClass: class的编辑者或创建者,具体的操作方
CtMethod: 类中的某个方法
以上参考链接:https://my.oschina.net/GameKing/blog/794580

一个demo,加深理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.qiakr.local.gen;
import java.lang.reflect.Method;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtField.Initializer;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.Modifier;
public class JavassistGenerator {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
// 创建类名
CtClass ctClass=pool.makeClass("com.qiakr.local.TargetClass");
// 创建属性
CtField ctField=new CtField(pool.get("java.lang.String"), "name", ctClass);
ctField.setModifiers(Modifier.PRIVATE);
// 为属性添加方法
ctClass.addMethod(CtNewMethod.setter("setName", ctField));
ctClass.addMethod(CtNewMethod.getter("getName", ctField));
ctClass.addField(ctField, Initializer.constant("default"));
//生成方法
StringBuffer body=new StringBuffer();
CtMethod ctMethod=new CtMethod(CtClass.voidType,"execute",new CtClass[]{},ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
body=new StringBuffer();
body.append("{\n System.out.println(name);");
body.append("\n System.out.println(\"execute ok\");");
body.append("\n return ;");
body.append("\n}");
ctMethod.setBody(body.toString());
ctClass.addMethod(ctMethod);
Class<?> c=ctClass.toClass();
//生成对象
Object o=c.newInstance();
// 调用方法
Method method=o.getClass().getMethod("execute", new Class[]{});
method.invoke(o, new Object[]{});
}
}

使用的方式比较简单,不依赖其他类库,maven中直接添加以下依赖即可:

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.18.1-GA</version>
</dependency>

所以,javassist的特点是使用方便,易于理解。 学习链接可以参考以下:
http://blog.csdn.net/sadfishsc/article/details/9999169

cglib、ASM

cglib更加工程化,spring中采用了cglib,学习文章可以参考:
http://blog.csdn.net/fenglibing/article/details/7080340

StandardJavaFileManager

有时候, 我们的程序在运行时,并不知道某些源码的存在;在某个条件下,把源码(.java)从硬盘或者网络“装”到我们的程序中,此时,可以采用jdk提供的StandardJavaFileManager,该类从版本1.6开始提供。api可以参考javax.tools包的StandardJavaFileManager。一个demo, 从网上扒了一个demo,本地跑一下,注意加载的时候的类路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.qiakr.local.gen;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
public class Test {
public static void main(String[] args) throws Exception{
String currentDir = System.getProperty("user.dir");
// 將源码写入文件中
String src = "package com.qiakr.local.gen;"
+ "public class TestCompiler {"
+ " public void disply() {"
+ " System.out.println(\"Hello\");"
+ "}}";
String filename = currentDir + "/src/main/java/com/qiakr/local/gen/TestCompiler.java";
File file = new File(filename);
FileWriter fw = new FileWriter(file);
fw.write(src);
fw.flush();
fw.close();
// 使用JavaCompiler 编译java文件
JavaCompiler jc = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = jc.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> fileObjects = fileManager.getJavaFileObjects(filename);
CompilationTask cTask = jc.getTask(null, fileManager, null, null, null, fileObjects);
cTask.call();
fileManager.close();
// 使用URLClassLoader加载class到内存
System.out.println(currentDir);
URL[] urls = new URL[] { new URL("file:/" + currentDir + "/src/main/java/") };
URLClassLoader cLoader = new URLClassLoader(urls);
Class<?> c = cLoader.loadClass("com.qiakr.local.gen.TestCompiler");
cLoader.close();
// 利用class创建实例,反射执行方法
Object obj = c.newInstance();
Method method = c.getMethod("disply");
method.invoke(obj);
}
}

以上是工具,只是基础; 动态编译的应用场景,关键看所处的业务场景下,用动态编译能否快速的解决问题,让所处理的类透明化。