一、关于 Java 编译
java 从代码编写到程序运行需要两次编译:第一次是 Javac 编译,第二次是 JIT + 解释器编译
- Javac 是前端编译:负责字节码优化、常量传播、公共表达式合并等
- AOT + JIT + 解释器是后端编译:负责字节码优化,存储空间优化,内联优化(inline)、CHA、逃逸分析、分支预测、锁粗化、锁消除等
前端编译优化经常遇到问题是什么?
很多面试题都会问题类、构造方法、静态代码块、非静态代码块执行顺序,以及 final 和常量,synchornized、javac 优化问题,这类问题实际上我们开发时也需要注意,毕竟出问题也一般不好定位。
解决这类问题的办法,实际上需要通过查看字节码完成,但字节码本质上是二进制,因此我们可以使用伪字节码工具,尽可能查看清楚的查看 class 文件,这里我们使用 ASM ByteCode Outline 、当然也可以使用 Java2Smali,实际上 smali 更加接近真实字节码,但该插件有个缺点会生成 smali 文件在 java 类目录下,因此需要你手动删除。
二、字节码查看工具
2.1 ASM ByteCode Outline
ASM ByteCode Outline 是常用的字节码可视化工具,能够很明确的找到相关字节码最终的生成调用逻辑。
public static String getA(){
String str = "id_";
str += "12345";
str += "_end";
return str;
}
public static String getB(){
StringBuilder sb = new StringBuilder();
sb.append("id_")
.append("123456")
.append("_end");
return sb.toString();
}
下面是利用ASM ByteCode Outline 反编译后的代码,从下面的代码中我们能看到很多bytecode指令,当然,由于java的编译是在java环境下执行的,本身java是基于栈内存模型的,因此下面的调用是“操作数栈”进行局部变量的存储和读取,当然你可能认为ASM ByteCode Outline不支持寄存器模型的反编译,其实这个说法还是有问题的,因为class文件转换为dex后才会将“栈模型”,显然这个转换的前提条件就是不满足的。
@groovyx.ast.bytecode.Bytecode
public static String getA() {
ldc "id_" //常量
astore 0 //存储到栈的位置 - java方法栈帧
_new 'java/lang/StringBuilder'
dup
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
aload 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ldc "12345"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
astore 0
_new 'java/lang/StringBuilder'
dup
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
aload 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ldc "_end"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
astore 0
aload 0
areturn
}
@groovyx.ast.bytecode.Bytecode
public static String getB() {
_new 'java/lang/StringBuilder'
dup
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
astore 0
aload 0
ldc "id_"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ldc "123456"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ldc "_end"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
pop
aload 0
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
areturn
}
注意: 从上面对比可知,str 通过 "+" 会使用 StringBuilder 完成该操作,每个 “+” 都会创建 StringBuilder 对象
当然,我们也可以看到很多INVOKEXXX的指令,其实这些指令是有区别的:
- INVOKESPECIAL: 负责调用构造方法和私有方法,因为构造方法和私有访问的访问权限和稳定性最强,在smail中,这个指令会变成invoke-direct。 正因为INVOKESPECIAL指令的稳定性很高,这就为后端编译如内联、标量化等提供了基础。注意,此指令和final修饰符没有任何关系。
- INVOKEVIRTUAL: 负责调用public、protected、默认方法,做此标记是因为这类方法可以被继承和重写,因此他的稳定性比较差,但是后端编译也不是毫无办法,后端编译时会进行CHA(类的层次分析),在确定稳定性后会进行优化,如果每个Activity页面仅有一个与之对应的子类,那么子类的方法后续可能也会被内联。同时,如果类的继承层次越深,那么优化的复杂度也会提升,因此实现继承时,最高3-4层即可。
- INVOKESTATIC: 负责调用静态方法,因为静态方法本身很稳定,因此也会很容易被优化。
- INVOKEINTERFACE: 顾名思义,调用接口的方法,由于存在多态情况,这种调用的优化难度也比较高。
- INVOKEDYNAMIC: Java7 新增的调用方式,其本质是配合MethodHandle调用,用于模拟方法调用提高动态性,,然而要注意的是,MethodHandle在Android平台的调用效率似乎不及反射,但在jvm上却比反射性能高。
三、javac常量问题
javac 常量问题属于编译时常量池问题,java 在编译时就会创建常量池,在 Android 7.0 之后,字符串常量池移到堆空间,字符串常量可以在运行时添加,因此 JIT 会做相应的处理。
public class AppTest {
private final int C = 2;
private int A = 1;
private final String D = "D";
private final String B = "B";
private String B2 = "B";
private String F = "F";
private final String E = "E";
private static final String AE = "AB"+"CDE";
private static AppTest appTest = new AppTest();
static {
FGH = "GHF";
}
public static String FGH = "FGH";
static {
FGH = "GFH";
}
{
FGH = "HGF";
mA = 2;
}
private int mA = 3;
{
mA = 1;
}
AppTest(){
}
public void helloWorld(){
int a = A + 3;
int b = 1 + 3;
int c = C + 3;
int d = b + 4;
String AB_1 = "AB";
String AB_2 = "A" + "B";
String AB_3 = "A" + B;
String AB_4 = "A" + B2;
String aA = "A";
String AB_5 = aA +"B";
String ABD = new String("A"+"B" +D);
}
public static void main(String[] args) {
AppTest appTest = new AppTest();
}
}
由于我们是面向Android 平台开发,下面我们通过smail来观察其实相对更加准确
在下面的代码中,方法内部".registers"的指令,和上面的bytecode的操作书栈有很多不同的地方,具体来说.registers并不存在单独的栈帧,而且空间相比栈帧要多一些,因此不会出现频繁出栈和入栈的情况,相比而言性能要好,这也是面试过程中经常要文档的,为什么DVM比JVM性能好,原因之一就是这个。
.class public Lcntest/AppTest;
.super Ljava/lang/Object;
.source "AppTest.java"
# static fields
.field private static final AE:Ljava/lang/String; = "ABCDE"
.field public static FGH:Ljava/lang/String;
.field private static appTest:Lcntest/AppTest;
# instance fields
.field private A:I
.field private final B:Ljava/lang/String;
.field private B2:Ljava/lang/String;
.field private final C:I
.field private final D:Ljava/lang/String;
.field private final E:Ljava/lang/String;
.field private F:Ljava/lang/String;
.field private mA:I
# direct methods
.method static constructor <clinit>()V // 静态变量的初始化方法
.registers 1
.prologue
.line 17
new-instance v0, Lcntest/AppTest;
invoke-direct {v0}, Lcntest/AppTest;-><init>()V
sput-object v0, Lcntest/AppTest;->appTest:Lcntest/AppTest;
.line 20
const-string v0, "GHF"
sput-object v0, Lcntest/AppTest;->FGH:Ljava/lang/String;
.line 22
const-string v0, "FGH"
sput-object v0, Lcntest/AppTest;->FGH:Ljava/lang/String;
.line 25
const-string v0, "GFH"
sput-object v0, Lcntest/AppTest;->FGH:Ljava/lang/String;
.line 26
return-void
.end method
.method public constructor <init>()V
.registers 4
.prologue
const/4 v2, 0x2
const/4 v1, 0x1
.line 3
invoke-direct {p0}, Ljava/lang/Object;-><init>()V //类的构造函数或者初始化方法
.line 5
iput v2, p0, Lcntest/AppTest;->C:I
.line 6
iput v1, p0, Lcntest/AppTest;->A:I
.line 8
const-string v0, "D"
iput-object v0, p0, Lcntest/AppTest;->D:Ljava/lang/String;
.line 10
const-string v0, "B"
iput-object v0, p0, Lcntest/AppTest;->B:Ljava/lang/String;
.line 11
const-string v0, "B"
iput-object v0, p0, Lcntest/AppTest;->B2:Ljava/lang/String;
.line 13
const-string v0, "F"
iput-object v0, p0, Lcntest/AppTest;->F:Ljava/lang/String;
.line 14
const-string v0, "E"
iput-object v0, p0, Lcntest/AppTest;->E:Ljava/lang/String;
.line 28
const-string v0, "HGF"
sput-object v0, Lcntest/AppTest;->FGH:Ljava/lang/String;
.line 29
iput v2, p0, Lcntest/AppTest;->mA:I
.line 31
const/4 v0, 0x3
iput v0, p0, Lcntest/AppTest;->mA:I
.line 33
iput v1, p0, Lcntest/AppTest;->mA:I
.line 34
return-void
.end method
.method public static main([Ljava/lang/String;)V
.registers 2
.param p0, "args" # [Ljava/lang/String;
.prologue
.line 55
new-instance v0, Lcntest/AppTest;
invoke-direct {v0}, Lcntest/AppTest;-><init>()V
.line 57
.local v0, "appTest":Lcntest/AppTest;
return-void
.end method
# virtual methods
.method public helloWorld()V
.registers 14
.prologue
.line 37
iget v11, p0, Lcntest/AppTest;->A:I
add-int/lit8 v6, v11, 0x3
.line 38
.local v6, "a":I
const/4 v8, 0x4
.line 39
.local v8, "b":I
const/4 v9, 0x5
.line 40
.local v9, "c":I
add-int/lit8 v10, v8, 0x4
.line 42
.local v10, "d":I
const-string v1, "AB"
.line 43
.local v1, "AB_1":Ljava/lang/String;
const-string v2, "AB"
.line 44
.local v2, "AB_2":Ljava/lang/String;
const-string v3, "AB"
.line 45
.local v3, "AB_3":Ljava/lang/String;
new-instance v11, Ljava/lang/StringBuilder;
invoke-direct {v11}, Ljava/lang/StringBuilder;-><init>()V
const-string v12, "A"
invoke-virtual {v11, v12}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v11
iget-object v12, p0, Lcntest/AppTest;->B2:Ljava/lang/String;
invoke-virtual {v11, v12}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v11
invoke-virtual {v11}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v4
.line 47
.local v4, "AB_4":Ljava/lang/String;
const-string v7, "A"
.line 48
.local v7, "aA":Ljava/lang/String;
new-instance v11, Ljava/lang/StringBuilder;
invoke-direct {v11}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v11, v7}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v11
const-string v12, "B"
invoke-virtual {v11, v12}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v11
invoke-virtual {v11}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v5
.line 50
.local v5, "AB_5":Ljava/lang/String;
new-instance v0, Ljava/lang/String;
const-string v11, "ABD"
invoke-direct {v0, v11}, Ljava/lang/String;-><init>(Ljava/lang/String;)V
.line 52
.local v0, "ABD":Ljava/lang/String;
return-void
.end method
总体上smail的可读性要稍微高一些,另外就是两者也有很多相似的部分。不过,ByteCode和Smali仍然存在很多缺陷,比如模数、常量池并不会直观的展示出来,这个时候可能需要010Editor等工具才能完整的展示。
从上面的伪字节码我们可以还原 javac 编译之后的代码
public class AppTest {
private static final String AE = "ABCDE"; //javac 优化,指向常量
private static AppTest appTest;
public static String FGH;
private final int C;
private int A;
private final String D;
private final String B;
private String B2;
private String F;
private final String E;
private int mA;
public static class_init(){
//类初始化函数,由ClassLoader去初始化
appTest = new AppTest();
FGH = "GHF"; //指向常量
FGH = "FGH"; //指向常量
FGH = "GFH"; //指向常量
}
public ppTest(){
super();
//下面语句会在super之后执行
{
C = 2; //指向常量
A = 1; //指向常量
D = "D";//指向常量
B = "B";//指向常量
B2 = "B";//指向常量
F = "F";//指向常量
E = "E";//指向常量
FGH = "HGF";//指向常量
mA = 2;//指向常量
mA = 3;//指向常量
mA = 1;//指向常量
}
//后面是构造方法其他代码,因为类外赋值的的变量在编译后会优先初始化
}
public void helloWorld(){
int a = A + 3; //a最终指向栈内存
int b = 4; //javac optimized 自动计算
int c = 5; //javac optimized 自动计算
int d = b + 4; //虽然指向常量,但javac并没有优化,原因待解,这里d最终指向栈内存,因为d属于基本类型
String AB_1 = "AB"; //javac optimized AB是常量,常量吃不存在,添加到常量池
String AB_2 = "AB"; //javac optimized javac 自动合并,合并后从常量池查找,复制
String AB_3 = "A" + B; //javac optimized javac 自动合并,合并后从常量池查找,复制
String AB_4 = "A" + B2; //javac 不会处理,AB_4 最终指向堆空间
String aA = "A"; //生成常量A,添加到常量池,并赋值给aA
String AB_5 = aA +"B"; //当前aA是局部变量引用常量,明显可以可以继续优化,但并没有,待解,此处AB_5最终指向堆空间
String ABD = new String("A"+"B" +D); //javac 自动合并 ABD,然后new String,导致ABD最终指向堆空间
}
public static void main(String[] args) {
AppTest appTest = new AppTest();
}
}
从上面的代码我们可知,构造方法外面的变量最终还是在构造方法中初始化,且在super调用之后插入的,这就意味着在源码中,构造方法中的变量执行是晚于构造方法外的变量的。这样做的好处是避免产生空指针。
三、运行时常量问题
java 常量池中,存在运行时常量池 主要分为:string.intern () , 字节小于 1Byte 的包装类型。
Integer ia = 200;
Integer ib = 200;
System.out.println(ia==ib); //false
Integer ic = 100;
Integer id = 100;
System.out.println(ic==id); //true
final String A = "A";
String ABC = A + "BC" ;
System.out.println(ABC=="ABC");
String c = new String("c") +new String("d");
System.out.println(c == c.intern()); //true
String s = new String("a") + new String("b");
System.out.println(s == s.intern()); //t
编译后的代码,从下面的代码中我们看到,在1Byte范围内的变量会被自动转换,这也是面试过程中最长问到的问题
// class version 51.0 (51)
// access flags 0x21
public class io/rmiri/keepalive/TestJavac {
@groovyx.ast.bytecode.Bytecode
public void <init>() {
aload 0
INVOKESPECIAL java/lang/Object.<init> ()V
return
}
@groovyx.ast.bytecode.Bytecode
public static void main(String[] a) {
sipush 200
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
astore 1
sipush 200
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
astore 2
getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
aload 1
aload 2
if_acmpne l0
iconst_1
_goto l1
l0
iconst_0
l1
INVOKEVIRTUAL java/io/PrintStream.println (Z)V
bipush 100
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
astore 3
bipush 100
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
astore 4
getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
aload 3
aload 4
if_acmpne l2
iconst_1
_goto l3
l2
iconst_0
l3
INVOKEVIRTUAL java/io/PrintStream.println (Z)V
ldc "A"
astore 5
ldc "ABC"
astore 6
getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
aload 6
ldc "ABC"
if_acmpne l4
iconst_1
_goto l5
l4
iconst_0
l5
INVOKEVIRTUAL java/io/PrintStream.println (Z)V
_new 'java/lang/StringBuilder'
dup
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
_new 'java/lang/String'
dup
ldc "1"
INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
_new 'java/lang/String'
dup
ldc "2"
INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
astore 7
getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
aload 7
aload 7
INVOKEVIRTUAL java/lang/String.intern ()Ljava/lang/String;
if_acmpne l6
iconst_1
_goto l7
l6
iconst_0
l7
INVOKEVIRTUAL java/io/PrintStream.println (Z)V
_new 'java/lang/StringBuilder'
dup
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
_new 'java/lang/String'
dup
ldc "a"
INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
_new 'java/lang/String'
dup
ldc "b"
INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
astore 8
getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
aload 8
aload 8
INVOKEVIRTUAL java/lang/String.intern ()Ljava/lang/String;
if_acmpne l8
iconst_1
_goto l9
l8
iconst_0
l9
INVOKEVIRTUAL java/io/PrintStream.println (Z)V
return
}
}
其中对于 intern 用法这里必须强调一下
java 7 中 string 常量在堆空间,因此如果是如下合成的变量本身是存在堆空间中,但是 intern 调用之后,会将 “cd” 引用添加至常量池中,因此,就有可能出现局部变量和常量相等的情况
String c = new String( "c" ) +new String( "d" );
包装类型,我们看到字节码中 Interge.valueOf (),该方法对 -128 到 127 之间的数据有特殊处理
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
四、指令消除问题
这些问题很多,比如下面的代码会造成死循环,但是原因是是什么呢?
int i = 10;
while(i > 0){
i = i--;
}
说实话,这种代码其实对于编译器而言,其实也是有一定的难度。编译器本身要避免二义性,对于不同的语言或许结果也有差异。不过,对于这种钻牛角尖的问题,最好也通过字节码角度来查看,当然也要平时积累。
这里我们就不贴代码了。
当然,这个问题还算简单的,之前有大佬的博客里是当前类中创建当前类的对象,后续补充到这里。
四、总结
本篇就到这里,这里我们简单了解下编译,其实很多问题都可以从字节码中找到答案。对于编译器,其本身也会存在一些规则,一些问题靠推理其实很难解释,正如大千世界的app,没有一个是万能的。
注意:本篇后续还会修改,不要转载。