-
2007-06-30
Javac背后的故事——创建对象
要让Java这个面向“对象”的世界正常运作,创建对象就是一项不可或缺的操作。
public class NewMain {
public static void main(String[] args) {
new Object();
}
}
用javap反编译上面的代码,我们可以得到下面的指令,这里省去了javac暗中创建的构造函数。
public class NewMain extends java.lang.Object{
...
public static void main(java.lang.String[]);
Code:
0: new #3; //class java/lang/Object
3: invokespecial #8; //Method java/lang/Object."<init>":()V
6: return
}
从这段代码中,我们可以清晰的看出创建对象(new)和调用构造函数(invokespecial)两个过程。关于这个问题,我在《对象的生命》中曾经进行过讨论。
既然javac将一个new的动作被解释为两条指令,那在JVM的层面上,我们当然就可以将它们分开。下面是一段没什么实际用途的代码,只是证明这个观点可行性。
public class NewGenerator {
public static void main(String[] args) throws Exception {
String className = "New";
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(Opcodes.V1_2, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);
Method m = Method.getMethod("void main (String[])");
GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, m, null, null, cw);
mg.newInstance(Type.getType(Object.class));
Label label = mg.newLabel();
mg.ifNonNull(label);
mg.mark(label);
mg.getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
mg.push("new object is not null");
mg.invokeVirtual(Type.getType(PrintStream.class), Method.getMethod("void println(java.lang.String)"));
mg.pop();
mg.returnValue();
mg.endMethod();
cw.visitEnd();
OutputStream os = null;
try {
os = new FileOutputStream(className + ".class");
os.write(cw.toByteArray());
} finally {
if (os != null) {
os.close();
}
}
}
}
这段代码生成的类是能够运行的,有兴趣的可以自己试一下。这段代码的作用是new出一个对象之后,如果这个对象非空的话,就会产生一个输出:
new object is not null
当然,如果尝试用这个对象做一些其它的操作,会有错误等待着我们,因为这个对象并不是一个完整的对象。在JVM规范中有相关的解释:new指令并不能完整创建出一个新的对象,直到对未初始化的对象调用了实例初始化方法才会完成实例的创建。这段代码也正好符合《对象的生命》中的解释,它只是负责做出申请内存。当然,在JVM中,它的实际工作要略多一些,如果这个对象的类没有加载,就会加载相应的类。
-
2007-06-18
Javac背后的故事——空类
程序员最熟悉的是源代码,但是要让程序真正的发挥功效,少不了编译器的帮助。javac的作用就是将Java代码编译为JVM指令。由于Java语言和JVM同出一门,所以,稍微熟悉一下,我们便不难发现,二者几乎是直接对应的。当然,为了简化代码的编写,javac除了直接翻译之外,还暗地里帮我们做了不少工作,我们从最简单的情况看起。
public class Test {
}
我们用javac编译这段代码,javap可以帮助我们反编译生成的类文件。
javap -c Test
下面就是反编译的结果。
public class Test extends java.lang.Object{
public Test();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
}
抛开指令具体的内容,上面反编译的结果清清楚楚的告诉我们,我们编写的这个空类一点都不空,因为其中还有一个构造函数。这就是javac替我们做的工作。没错,这是javac做的,但未必是JVM要求的。其实,JVM上运行的类,完全可以没有构造函数。不过,前面的例子已经明明白白的告诉我们,因为javac的作用,直接用Java语言是无法构造出真正的空类。那我们就不妨直接从字节码入手,借助ObjectWeb ASM构造真正的“空“类。
public class NoCtorGenerator {
public static void main(String[] args) throws Exception {
String className = "NoCtor";
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(Opcodes.V1_2, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);
cw.visitEnd();
try {
os = new FileOutputStream(className + ".class");
os.write(cw.toByteArray());
} finally {
if (os != null) {
os.close();
}
}
}
}
借助javap,我们可以看到生成的结果,确实没有构造函数。
public class NoCtor extends java.lang.Object{
}
不过,因为没有构造函数存在,我们并不能用这个类创建对象,但是,下面的代码证明了这个类生成的类确实可用。
public class NoCtorMain {
public static void main(String[] args) {
System.out.println(NoCtor.class);
}
}
运行这段代码,我们可以得到下面的输出:
class NoCtor
关于Java虚拟机的指令,可以参考《深入Java虚拟机》,而ObjectWeb ASM的入门,可以参考我的《Hello, ASM——代码生成》。 -
2006-12-10
Hello, ASM——代码生成
这里要说的ASM,并不是指汇编语言,而是一个操作Java bytecode的框架。对于Java平台而言,bytecode便是它的“汇编语言”,所以,ASM这个名字倒也算是实至名归。ASM本身很强大,有不少软件和框架选择它作为底层的实现,比如cglib。在这篇blog中,主要来关注一下它在代码生成方面的威力。
在起步阶段,Hello World总是一个很好的选择,也就是说,我们生成的目标代码是这样的:
public class AsmExample {
public static void main(String[] args) {
System.out.println("Hello, World");
}
}在Java中,代码是以类的形式进行组织的,.class文件便是bytecode的载体。对照上面这段代码,首先,我们需要一个类。
public class AsmMain {
public static void main(String[] args) {
ClassWriter cw = new ClassWriter(true);
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "AsmExample", null, "java/lang/Object", null);...
cw.visitEnd();
}
}在上面这段代码中,ClassWriter就是ASM中用来生成bytecode形式的类。在这里,我们要为我们生成的类设置一些属性,比如类名、访问级别和超类,以及在bytecode层次上需要的版本号等等。至此,对应的Java代码如下:
public class AsmExample {
}
有了类,接下来就是对应的方法了,先来看看基本的结构:
Method m = Method.getMethod("void main (String[])");
GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, m, null, null, cw);
...
mg.returnValue();
mg.endMethod();
首先我们设置了一个方法的签名,包括方法名,参数和返回类型。我们要生成这个方法,还需要设置一些方法的属性,比如访问级别等等,通过cw这个参数,方法同类关联在了一起。到这里,对应的Java代码是这样的:
public class AsmExample {
public static void main(String args[]) {
}
}前面所做的都是搭建静态结构的工作,接下来,我们要进入的才是让程序动起来的部分。
mg.getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
mg.push("Hello world!");
mg.invokeVirtual(Type.getType(PrintStream.class), Method.getMethod("void println (String)"));在这里,我们面对的实际上是JVM的指令,所以,如果面对汇编一样,所有的一切都一步一步说清楚。
首先是获得System.out。我们通过getStatic这个方法实现,它表示从哪个类中取出哪个static字段,其类型是什么。而且实际上,这条指令执行的结果是将这个取出的字段推到了堆栈上。随后,我们在把“Hello world!”也推入堆栈之中,很显然,这一切都是在为调用方法做准备。
对于参数(这里的“Hello world!”)入栈,我们很容易接受,但为什么要把System.out也送入堆栈呢?再次提醒一下,这里我们是在JVM一级进行思考,在这里,方法调用被打回了最原始的形态,在Java程序中被隐藏的this这时也要作为参数显式传递,也就是说,方法调用变成了这样:
println(System.out, "Hello world!");万事俱备,调用方法。在Java中,方法调用需要区分类方法和实例方法,它们在虚拟机中有着不同的指令,这里我们要调用的是实例的方法,所以,这里用的是invokeVirtual,指定了类型,指定了方法,方法就可以调用了。如果要调用类的方法,也就是static方法,那就需要让invokeStatic上阵了。
对比一下invokeVirtual和invokeStatic的API定义,我们不难发现,它们之间实际上没有什么区别,之所以要弄出两个来,与Java的设计不无关系,它把属于类的东西看作了一种特殊的东西,没有统一到对象体系之中。如果为Ruby设计虚拟机,可以消除这样的问题,因为在Ruby中,类的方法就是类对象的实例方法,这样将类的东西统一到对象体系之中,不必额外区分。
到这里,我们的目标便已完全实现:
public class AsmExample {
public static void main(String args[]) {
System.out.println("Hello world!");
}
}之后,我们可以把定义的类转为字节,至于是加载到虚拟机中运行,还是保存到文件中,那就由自己的喜好了。
byte[] code = cw.toByteArray();和ASM打交道,需要我们放低自己姿态,站在指令一级进行思考。比如,在这个层次上,实现判断语句,就需要设置label,然后进行相应的跳转;这里没有循环语句,需要自己用判断加跳转打造循环结构。不过,总的来说,很容易同Java程序对应上,就像我们上面所做的那样。《深入Java虚拟机》可以让我们更好的了解JVM,也可以让帮助我们更好的理解ASM的程序。
有几个帮手可以让我们更好进行bytecode生成这个游戏。javap,JDK带的一个工具,可以用来反汇编Java bytecode。在接触ASM的最初,我们对指令不是很熟悉的时候,可以考虑先把自己的目标写成Java程序,编译之后用“javap -c”来查看,所有的指令便一览无余,我们就可以照方抓药了。jad,它为我们提供了一个将Java class文件反编译为Java文件,通过它,我们就可以知道生成的bytecode究竟是不是自己想要的,我所展示与生成过程对应的Java代码便是借助于jad的力量完成的。
ASM很强大,这里只介绍了ASM中的代码生成,实际上,就连代码生成这一项工作介绍的都不那么完整,ASM还提供了另外一种生成方式,不过,用起来不如这里的GeneratorAdapter,需要更多的JVM指令的智慧,优势在于速度稍快一些。







