Java中的编译与反编译

编译

       编程语言有高级和低级之分。所谓低级语言就是指可以被计算机理解并执行的语言,例如机器语言。而高级语言计算机无法直接理解执行,所以才会有中间的“翻译”过程。在计算机发展过程中,一开始使用的都是机器语言,但是机器语言无论是阅读还是编写都极其不方便,而且一旦出现错误,很难排查,所以进一步发展出了汇编语言,相比有机器语言,它有一定的可读性,但是它仍然会有大量的指令,编写很复杂,后来出现了C语言,Java语言等等这些高级语言。

       高级语言的出现,方便了程序员的编码和代码理解,而且出现问题能够及时方便地排查问题,高级语言的语义更加倾向于人类自然语言,所以编码变得不再那么复杂。但是相应的,它的效率相对低下,因为需要有“翻译”的过程。高级语言无法被计算机直接解读,只能借助于编译器,将高级语言写出的源代码编译成计算机可以理解并执行的指令代码,这中间的转化就会浪费一定的时间。这种将高级语言转化成计算机能够理解执行的指令代码的过程就是编译。

Java的编译介绍

       Java的编译有些不同,因为Java的特性是一次编写,到处运行。做到这种效果的主要依据就是JVM。Java的编译是分为两个阶段的,首先,利用JDK自带的编译器,将源代码经过词法分析,语法分析直至语义分析,然后就会产生一个class文件。这段过程称之为前端编译,此时产生的class文件还无法被计算机识别执行,只能算是整个编译过程中产生的一个中间产物。

       然后JVM将读取到的二进制文件进行深度编译,将其编译成与具体平台相关的指令代码,这个过程是后端编译,它主要依赖于JVM。前端编译是与操作系统平台无关的,最终生成的class文件是可以在各个JVM平台进行深度编译;而后端编译就需要跟具体操作系统平台相关了,因为JVM有不同平台的版本,可以将这种统一格式的class文件进一步深度编译,将其转换成与具体平台相关的指令代码。

前后端编译过程.png

       对于编译器,Java内置的有javac工具,此外很多IDE工具也内置了编译工具,但是这些都是前端编译器,主要功能就是把【.java】文件编程成【.class】文件。

词法分析器

       这个阶段是将源程序文件从左到右一个字符一个字符地读入,将字符序列转换为标记(token)序列的过程。这里的标记是一个字符串,是构成源代码的最小单位,该过程中,词法分析器还会对标记进行分类。

       词法分析器通常不会关心标记之间的关系(分析关系主要在语法分析阶段)。如:源程序中会有很多括号,但是词法分析器只是将其识别为标记,但是它并不关心这些括号是否能正确匹配(即:如果只有“{”,而没有“}”,词法分析中不会发现问题)。

语法分析器

       它是在词法分析的基础上,将单词序列组合成各种短语,如“程序”、“语句”、“表达式”等等。语法分析能够判断源程序在结构上是否正确。

语义分析

       该阶段是程序编译的一个逻辑阶段。它是对结构正确的源程序进行上下文有关性质的审查。它主要是审查源程序是否含有语义错误,同时也为代码的生成阶段收集类型信息。

       语义分析中的一个很重要的部分就是类型审查,比如很多语言要求数组下标必须为整数,如果使用浮点数作为下标,编译器就会报错;再比如很多程序允许某些类型之间能够进行自动转换等等。

       经历过上面的过程之后,就开始生成中间代码,中间代码具有两个很重要的性质:易于生成、能够轻松翻译成目标机器上的语言。著名的解语法糖操作就是在javac中完成的

后端编译

       Java在经历过前端编译之后,如果需要执行编译后的class文件,需要借助于JVM,JVM会将class文件中的内容逐条翻译成机器指令,这个解释的过程就是JVM的解释器(Interpreter)的功劳。很明显,这个解释是比较浪费时间的,为了提高这种解释的效率,Java引入了JIT技术。

       虽然引入了JIT,Java仍然使用解释器进行代码解释,但是在解释的过程中,随着代码的不断执行,会识别出代码中执行比较频繁的代码段,这段代码就会被标记为“热点代码”。JIT就会将这段热点代码编译后的机器代码进行优化后,缓存起来,下次再执行到这段代码,直接跳过编译过程,使用缓存的机器码。

       HotSpot虚拟机中内置了两种JIT编译器:Client Compiler和Server Compiler。目前主流的方式就是采用其中一种编译器与解释器配合工作的方式。那为什么不将其全部编译成热点代码呢?主要是出于资源最大化利用的考虑:在程序中,不可能所有代码都是热点代码,真正频繁执行的代码只是占据很少一部分,如果将那些只执行了一遍就再也不执行的代码也进行缓存,完全就是在浪费缓存资源。另外在将代码转换成热点代码过程中是需要经过一个编译过程的,如果这种只执行一次的代码也要编译,其实也是浪费时间。

JIT工作原理.png

热点检测

要想触发JIT编译,就必须满足热点代码的检测,目前主要有两种热点代码探测的方式:

  • 基于采样的方式探测(Simple Based Hot Spot Detection):周期性的检测各个线程的栈顶,如果发现某个方法经常出现在栈顶,就可以认为是热点方法。它的缺点很明显:无法精确确认一个方法的热度,另外也容易受到线程阻塞或者别的原因干扰。

  • 基于计数器的热点探测(Counter Based Hot Spot Detection):虚拟机会为每个方法,甚至是每个代码块建立计数器,统计方法和代码块的执行次数,一旦超过某个阈值就认为是热点方法,触发JIT编译。

HotSpot虚拟机采用的就是上面第二种探测方式,准备了两个计数器:方法计数器和回边计数器。它们分别对应方法调用次数统计和代码循环执行次数统计。

编译优化

       其实JIT除了具有缓存的功能,还会对代码做各种优化,例如:逃逸分析、锁消除、锁膨胀、方法内联、空值检查消除、类型检测消除、公共子表达式消除等等。可以搜索相关概念介绍,了解其原理,这里暂时不再赘述。

反编译

       反编译顾名思义就是编译的逆向过程,将编译后的class文件转换成最初编写的源代码的形式,这个过程就是反编译。JDK内部有一个反编译的工具javap,此外还有jad和cfr等反编译工具,根据习惯,可以选用不同的反编译工具。反编译一般很少用到,只有在进行底层源码分析的时候才会用到。

javap

       javap是JDK自带的一个反编译工具,但是javap与其他两个反编译工具最大的特点就是:javap反编译出来的并不是java文件,生成的反编译后的内容也不像其他两个工具那样容易理解。例如这里以JDK7中的switch新添加了对String的支持为例,我们可以看看JDK到底是如何支持String的:

public class SwitchDemoString {
   public static void main(String[] args) {
     String str = "world";
     switch(str){
       case "hello":
         System.out.println("Hello");
         break;
       case "world":
         System.out.println("World");
         break;
       default:
         break;
     }
   }
}

执行编译并进行反编译后控制台得到如下结果:

root@ubuntu:/usr/local/workspace/demo2# javap SwitchDemoString
Compiled from "SwitchDemoString.java"
public class SwitchDemoString {
 public SwitchDemoString();
 public static void main(java.lang.String[]);
}

默认情况下,如果没有指定参数,它会打印出类中public和protected修饰的方法和字段。javap有一个常用的参数是“-c”,执行后结果如下:

root@ubuntu:/usr/local/workspace/demo2# javap -c SwitchDemoString
Compiled from "SwitchDemoString.java"
public class SwitchDemoString {
 public SwitchDemoString();
 Code:
 0: aload_0
 1: invokespecial #1                  // Method java/lang/Object."<init>":()V
 4: return
​
 public static void main(java.lang.String[]);
 Code:
 0: ldc           #2                  // String world
 2: astore_1
 3: aload_1
 4: astore_2
 5: iconst_m1
 6: istore_3
 7: aload_2
 8: invokevirtual #3                  // Method java/lang/String.hashCode:()I
 11: lookupswitch  { // 2
 99162322: 36
 113318802: 50
 default: 61
 }
 36: aload_2
 37: ldc           #4                  // String hello
 39: invokevirtual #5                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
 42: ifeq          61
 45: iconst_0
 46: istore_3
 47: goto          61
 50: aload_2
 51: ldc           #2                  // String world
 53: invokevirtual #5                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
 56: ifeq          61
 59: iconst_1
 60: istore_3
 61: iload_3
 62: lookupswitch  { // 2
 0: 88
 1: 99
 default: 110
 }
 88: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
 91: ldc           #7                  // String Hello
 93: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
 96: goto          110
 99: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
 102: ldc           #9                  // String World
 104: invokevirtual #8                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
 107: goto          110
 110: return
}

       可以看到如果稍微有点指令基础的,这段代码还是可以看到一点眉目的,但是就大多数人而言,可能阅读起来就比较费晦涩。javap还有很多参数,可以运行javap -help获取详细使用介绍。

jad

       因为jad很久没有更新了,所以对于JDK7之前的版本,反编译的时候问题不大,但是JDK7中就已经出现了偶尔不支持的问题,到JDK8的lambda表达式的时候,它就彻底失效了,如果有兴趣可以去网上下载jad工具。使用就跟javap正常使用一样,利用命令即可 jad Xxx.class

cfr

       cfr是一个jar包工具,它需要利用java命令,结合参数jar来使用,另外它还有很多参数,可以运行java -jar cfr_0_132.jar --help来获取各个参数的使用说明。

Switch反编译

对于JDK7中switch引入了String类型的支持,来看以下反编译后的结果:

java -jar cfr_0_132.jar SwitchDemoString.class --decodestringswitch false
/*
* Decompiled with CFR 0_132.
*/
import java.io.PrintStream;

public class SwitchDemoString {
   public static void main(String[] arrstring) {
       String string;
       String string2 = string = "world";
       int n = -1;
       switch (string2.hashCode()) {
           case 99162322: {
               if (!string2.equals("hello")) break;
               n = 0;
               break;
           }
           case 113318802: {
               if (!string2.equals("world")) break;
               n = 1;
           }
       }
       switch (n) {
           case 0: {
               System.out.println("Hello");
               break;
           }
           case 1: {
               System.out.println("World");
               break;
           }
       }
   }
}

       可以看到,它编译出来的内容就比较好理解了,而且我们也看到了,实际上switch对String的支持,最终还是去比较String对应的hashCode。不过因为hashCode可能会出现碰撞的情况,所以在case里面还使用了equals比较。其实对于switch-case语句,最终底部比较的都是int数据,无论是char、byte、short其实都是可以转换成int类型的,而String的hashCode正好返回的也是一个int类型数据。

lambda表达式反编译

public class LambdaDemo {

    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("线程已经运行"), "t1");
        t.start();
    }

}

反编译后的效果如下:

root@ubuntu:/usr/local/workspace/demo2# java -jar cfr_0_132.jar LambdaDemo.class --decodelambdas false
/*
 * Decompiled with CFR 0_132.
 */
import java.io.PrintStream;
import java.lang.invoke.LambdaMetafactory;

public class LambdaDemo {
    public static void main(String[] arrstring) {
        Thread thread = new Thread((Runnable)LambdaMetafactory.metafactory(null, null, null, ()V, lambda$main$0(), ()V)(), "t1");
        thread.start();
    }

    private static /* synthetic */ void lambda$main$0() {
        System.out.println("\u7ebf\u7a0b\u5df2\u7ecf\u8fd0\u884c");
    }
}

lambda的表达式实际上是借助LambdaMetafactory来实现的,具体不做详细分析,以后会单独介绍。

String的+运算反编译

public class StringDemo {

    public static void main(String[] args) {
        String a = "Hello";
        String b = "World";
        String c = "Hello" + "World";
        String d = a + "World";
        String e = a + b;
    }

}

这里我使用了两种反编译工具:

cfrjar包的反编译结果:

root@ubuntu:/usr/local/workspace/demo2# java -jar cfr_0_132.jar StringDemo.class
/*
 * Decompiled with CFR 0_132.
 */
import java.lang.invoke.CallSite;
import java.lang.invoke.StringConcatFactory;

public class StringDemo {
    public static void main(String[] arrstring) {
        String string = "Hello";
        String string2 = "World";
        String string3 = "HelloWorld";
        CallSite callSite = StringConcatFactory.makeConcatWithConstants(new Object[]{"\u0001World"}, string);
        CallSite callSite2 = StringConcatFactory.makeConcatWithConstants(new Object[]{"\u0001\u0001"}, string, string2);
    }
}

运用idea的一个asm-bytecode插件得到的结果是:

/*
 * Decompiled with CFR 0_124.
 */
package com.still_loving.interview.thread;

public class StringDemo {
    public static void main(String[] args) {
        String a = "Hello";
        String b = "World";
        String c = "HelloWorld";
        String d = new StringBuilder().append((String)a).append((String)"World").toString();
        String e = new StringBuilder().append((String)a).append((String)b).toString();
    }
}

       由此可以得出结论,对于String字面量上的相加,编译后直接就会变成拼接后的字符串,而对于存在字符串变量的相加,会new StringBuilder,利用它的append方法拼接,然后再toString返回。

java10中本地变量的反编译

public class VarDemo {
    public static void main(String[] args) {
        //初始化局部变量
        var str = "hello";
        var list = new ArrayList<String>();
        
        list.add("Java");
        list.add("Python");

        //使用增强型for循环
        for (var s : list) {
            System.out.println(s);
        }

        //传统for循环
        for (int i = 0, len = list.size(); i < len; i++) {
            System.out.println(list.get(i));
        }
    }
}

cfr反编译后的结果如下:

root@ubuntu:/usr/local/workspace/demo2# java -jar cfr_0_132.jar VarDemo.class
/*
 * Decompiled with CFR 0_132.
 */
import java.io.PrintStream;
import java.util.ArrayList;

public class VarDemo {
    public static void main(String[] arrstring) {
        String string = "hello";
        ArrayList<String> arrayList = new ArrayList<String>();
        arrayList.add("Java");
        arrayList.add("Python");
        for (String string2 : arrayList) {
            System.out.println(string2);
        }
        int n = arrayList.size();
        for (int i = 0; i < n; ++i) {
            System.out.println((String)arrayList.get(i));
        }
    }
}

其实反编译后的结果跟我们正常传统写法一样,没有太大区别,所以var只是一种语法糖。

语法糖(Syntactic Sugar),也称糖衣语法,由英国计算机科学家Peter.J.Landin发明的一个术语,指在计算机语言中添加某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用,语法糖让程序更加简洁,有更高的可读性。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,265评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,078评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,852评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,408评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,445评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,772评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,921评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,688评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,130评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,467评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,617评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,276评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,882评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,740评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,967评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,315评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,486评论 2 348

推荐阅读更多精彩内容

  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,564评论 3 83
  • 三重:代码、底层内存、源码 第一阶段:开发常用JavaSE基础、IDE、Maven、Gradle、SVN、Git、...
    guodd369阅读 16,329评论 1 44
  • 老葛是一个极瘦的人,我比较胖。虽然我们都是180的身高,但同学总说他比较显个儿。大一的时候一起参加十佳歌手比赛,一...
    别人都叫侯哥阅读 290评论 3 1
  • 1.2-ARKit概述及特点介绍 1.ARKit是2017年6月6日,苹果发布iOS11系统所新增框架,它能够帮助...
    泥孩儿0107阅读 270评论 0 0
  • 虽不语,但倾城 人,在有些时候, 在有些时候 都会这样子, 碰到过于喜欢的东西, 总是会刻意的若即若离,...
    别具一格格阅读 503评论 2 5