• 要让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中,它的实际工作要略多一些,如果这个对象的类没有加载,就会加载相应的类。
  • 程序员最熟悉的是源代码,但是要让程序真正的发挥功效,少不了编译器的帮助。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——代码生成》。

  • 这里要说的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指令的智慧,优势在于速度稍快一些。