Skip to content

性能微优化-编译Java代码

About 1492 wordsAbout 5 min

性能新书

2026-08-19

第5章的性能优化策略适合各种开发语言,应用这些策略有较大的系统性能提升。有些场景下,核心系统或者工具类性能有极致的要求,这时候可以应用Java 自身的性能最佳实践,这些实践需要以Java编译和运行作为基础知识,掌握字节码和JIT等知识。

学完本章,将掌握如下知识

  • Java编译,把Java源码编译成字节码,并在编译过程中做的性能优化。
  • 字节码的基本知识,包括如何查看字节码,以及字节码的常用指令。本章的部分性能优化策略需要结合字节码解释。同时Java系統很多高级功能均是通过动态字节码生成实现
  • JIT基本知识,包括了解JIT的运行过程,以及观察JIT。通过对核心系统或者工具的运行的JIT日志进行分析,确保实现预期的Java优化。
  • Java语言中的一些高性能实践,可以应用核心系统或者工具库提高性能,比如String的最佳使用办法,位运算等

前三节涉及到JVM的知识,读者可以先阅读6.4节以后的内容,如果有不理解,在查看前3节的相应内容

编译Java代码

Java的编译通常有

  • 前端编译Javac,把Java源码编译成字节码,字节码可以被JVM解释执行。
  • 即时编译JIT(Just-In-Time),字节码通过JVM执行过程中,动态编译成机器码的过程,JIT通常会分析系统的热点,对热点代码会再次尝试更加激进的优化措施从而提高Java系统性能。
  • 提前编译AOT(Ahead-Of-Time),将Java源码编译成机器码后通过操作系统执行,优点是启动速度快,执行速度快,缺点是牺牲了平台无关性,另外,有些优化需要在运行过程中分析确认,这时候只有JVM才能进步一优化,而AOT做不到。

这里的前端编译是指将Java源码编译成Java字节码的过程,JDK提供javac命令将Java源程序编译成class

${java_home}/bin/javac Hello.java

前端编译Java源程序不一定是来自文件,比如,可以使用javax.tools.JavaCompiler(JDK6开始支持)类,将字符串源码编译成字节码存到Test.class里

public class CompileString {
    public static void main(String[] args) throws IOException {
        //获取一个JavaCompiler
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //编译的目标Java源码
        JavaStringObject stringObject =
                new JavaStringObject("Test.java", getSource());
        //编译输出目录
        DiagnosticCollector<JavaFileObject> diagnostics =
                new DiagnosticCollector<>();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
        String classes = System.getProperty("user.dir")+"/jmh-java/target/classes";
        File classesFile = new File(classes);
        fileManager.setLocation(CLASS_OUTPUT,Arrays.asList(classesFile));
        //配置编译任务
        JavaCompiler.CompilationTask task = compiler.getTask(null,
                fileManager, diagnostics, null, null, Arrays.asList(stringObject));
        //开始编译
        boolean success = task.call();
        System.out.println(success?"编译成功":"编译失败");
        diagnostics.getDiagnostics().forEach(System.out::println);

    }

    public static String getSource() {
        return "public class Test {"

                + " }";
    }

 
}

这里有几个类需要重点说明

说明
JavaCompiler用于编译java代码,javac命令也会调用JavaCompiler。diagnostics用于在编译过程中保留调试,警告,或者错误信息。
StandardJavaFileManager对象用于管理源码和编译后输出文件。本例子设定了CLASS_OUTPUT 目录
JavaStringObject是自定义的一个对象继承了SimpleJavaFileObject,用于代表Java源代码

JavaStringObject定义如下,name表示Java名,source表示其源码:

public class JavaStringObject extends SimpleJavaFileObject {
    private final String source;

    protected JavaStringObject(String name, String source) {
        super(URI.create(name), Kind.SOURCE);
        this.source = source;
    }

    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors)
            throws IOException {
        return source;
    }
}

JavaStringObject最重要的方法是实现了getCharContent,提供Java代码。这个例子里,Java代码以字符的形式提供,而不是文件。

为了编译JavaStringObject,需要创建 CompilationTask,并执行call方法 ,如下代码

JavaCompiler.CompilationTask task = compiler.getTask(null,
                                                     fileManager, diagnostics, options, null, 
                                                     Arrays.asList(stringObject));
boolean success = task.call();
System.out.println(success?"编译成功":"编译失败");
diagnostics.getDiagnostics().forEach(System.out::println);

task.call 如果返回true,表示编译成功,可以在/jmh-java/target/classes 下找到编译好的Test.class.

代码最后,打印出编译过程中的调试,告警,或者错误信息

在Java源码编译成字节码过程中,Javac会处理Java的语法糖,这是通过com.sun.tools.javac.comp.Lower类来处理,比如

1) 条件编译。如下代码FLAG是final类型,因此编译时候可以省去这个代码

static final boolean  FLAG = false;
public void run(){
  if(FLAG){
    System.out.println("hello");
  }
}

通过com.sun.tools.javac.comp.Lower.visitIf 来处理

编译结果,run方法是个空方法。

public void run() {
}

如果你想查看com.sun.tools.javac.comp.Lower.visitIf源码,你可在visitIf代码打上断点,并修改CompileString的getSouce方法,如下

public static String getSource() {
        return " public class Test {"
                +"static final boolean  FLAG = false;" 
                + " public void run(){" 
                + "  if(FLAG){"
                + "    System.out.println(\"hello\");" 
                + "  }" 
                + "}"
                + "}";
    }

以Debug方式运行CompileString,可以看到JavaCompile进入visitIf代码的cond.type.isFalse() 分支

2) 最常见的语法糖之一是foreach,如下代码

List<Integer> list = Arrays.asList(1,2);
for(Integer key:list){
  System.out.println(key);
}

编译成class后,用eclipise或者idea等IDE直接打开class,,可以看到反编译后的代码

List<Integer> list = Arrays.asList(1, 2);
Iterator var2 = list.iterator();

while(var2.hasNext()) {
  Integer key = (Integer)var2.next();
  System.out.println(key);
}

这段通过com.sun.tools.javac.comp.Lower.visitIterableForeachLoop方法处理。

3)常量折叠

static final int  b = 3;
public void run(){
  int a = 1+b;
  System.out.println(a);
}

编译代码后是, ConstFold.fold

public void run() {
  int a = 4;
  System.out.println(a);
}

4) 装箱和拆箱,这个功能是在代码在Lower.unBox处理

Integer i = 1;

编译代码

Integer i = Integer.valueOf(1);

注意,当使用IDE反编译class的时候,仍然看到的是Integer i=1, 这是IDE反编译工具对反编译结果的显示做了还原处理

其他编译时期生成的还有默认构造函数,语法糖还包括字符串+ 改成使用StringBuilder::append方法(注意,JDK9后将调整,会在6.4里描述此处改动),Lambda语法,断言Assert语法糖,枚举处理,以及switch支持String等语法糖.

知行合一