专栏首页一猿小讲程序员进阶系列:你真的懂 HelloWorld 吗?

程序员进阶系列:你真的懂 HelloWorld 吗?

作为入了门的 Java 程序员,相信在脑海中都能够秒写出 HelloWorld.java,都知道编译成 HelloWorld.class,然后就可以跨平台执行了。

常言道:知人知面不知心。敢问,你真的懂 HelloWorld.class 吗?你真的懂她的内心吗?

不清楚,也无所谓,只因有一颗求知的心。

先让慌乱的内心平静下来,跟随小猿的脚步,一起从字节码层面看看 HelloWorld。希望通过此篇分享对字节码文件有个全局的认识,并对 HelloWorld 执行原理有个大致的了解。

1

准备:工欲善其事必先利其器

首先具备 Java 环境(能打开此文章,说明你肯定具备此环境)。

能开发代码的工具(不强求IntelliJ IDEA),然后写出如下图 HelloWorld.java 就可以。

编译 HelloWorld.java 源文件,生成对应的字节码文件。

然后需要一个能查看 class 文件的工具(不强求UltraEdit,只要能查看 16 进制的文件就行,俗称:Hex Viewer),如果按照默认记事本,打开 class 文件的效果是这样子的。

这打开的方式肯定不对,换种开启的方式,用 UltraEdit(本文统称 UE) 进行打开。

虽然不是乱码,但是还是看不懂啊,不过仔细瞧。引入眼帘的便是开头的 CA FE BA BE(咖啡宝贝) ,这个东西叫做魔数。

每个 class 文件的头 4 个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 class 文件。

如若要是这么说下去,估计都会彻底疯掉,换种方式进行分解。

接下来对 HelloWorld.class 文件进行反编译,当然推荐可以使用工具 ClassPy、JavaClassViewer、jclasslib 查看 class 文件结构,本次就用 jdk 自带的命令 javap 来查看 class 文件的结构,并把反编译的内容重定向输出到文件 hello_javap.txt 中。

javap -v HelloWorld.class >> hello_javap.txt

javap 是 Java class 文件分解器,可以反编译,也可以查看 java 编译器生成的字节码,用于分解 class 文件,可以解析出当前类对应的 code 区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。

上面的所有的准备工作,皆是为了得到 hello_javap.txt 文件。

Classfile /D:/workspace/codeonce/out/production/codeonce/think/twice/code/once/HelloWorld.class
  Last modified 2020-8-23; size 578 bytes
  MD5 checksum 20602b9ebb70bbd1247c77f3729ec8d5
  Compiled from "HelloWorld.java"
public class think.twice.code.once.HelloWorld
  SourceFile: "HelloWorld.java"
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        //  java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            //  Hello World!
   #4 = Methodref          #24.#25        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            //  think/twice/code/once/HelloWorld
   #6 = Class              #27            //  java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lthink/twice/code/once/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          //  "<init>":()V
  #21 = Class              #28            //  java/lang/System
  #22 = NameAndType        #29:#30        //  out:Ljava/io/PrintStream;
  #23 = Utf8               Hello World!
  #24 = Class              #31            //  java/io/PrintStream
  #25 = NameAndType        #32:#33        //  println:(Ljava/lang/String;)V
  #26 = Utf8               think/twice/code/once/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public think.twice.code.once.HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lthink/twice/code/once/HelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return        
      LineNumberTable:
        line 5: 0
        line 6: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}

如此这般,天书一样,着实让人头大... ...心莫慌,再次让慌乱的内心平静下来,跟随小猿的脚步,一起去分析字节码文件,尝试彻底搞懂它。

2

解剖:化繁为简,逐个拆解。

一:Classfile 文件信息

Classfile /D:/workspace/codeonce/out/production/codeonce/think/twice/code/once/HelloWorld.class //class文件的路径
  Last modified 2020-8-23; size 578 bytes //最后一次修改时间以及该class文件的大小
  MD5 checksum 20602b9ebb70bbd1247c77f3729ec8d5 //该类的MD5值
  Compiled from "HelloWorld.java" //编译自源文件名

这块感觉不用详细解释,仔细去看,应该都能懂。

第 1 行:class 文件的路径
第 2 行:最后一次修改时间;该 class 文件的大小。
第 3 行:MD5 checksum 值,例如下载文件的场景下会用于检查文件完整性,检测文件是否被恶意篡改。
第 4 行:编译自 HelloWorld.java 源文件。

二:类主体部分定义信息

public class think.twice.code.once.HelloWorld  //包名及类名
  SourceFile: "HelloWorld.java" //源文件名
  minor version: 0 //次版本号
  major version: 52 //主版本号,52 对应 JDK 1.8
  flags: ACC_PUBLIC, ACC_SUPER //该类的权限修饰符(访问标志)

重点关注第 3、4 两行,为什么要重点关注呢?业务开发中估计多数都遇到过 Unsupported major.minor version 的错误。其实就是通过高版本的 JDK 进行编译(例如 JDK 1.8),然后跑在低版本的 JDK 上(JDK 1.5),就会报版本不支持。

为了使用方便,特意整理一 JDK 各版本图,请拿走不谢。

三:常量池信息

Constant pool: // 常量池,#数字相当于是常量池里的一个索引
   #1 = Methodref          #6.#20         //  java/lang/Object."<init>":()V //方法引用
   #2 = Fieldref           #21.#22        //  java/lang/System.out:Ljava/io/PrintStream; //字段引用
   #3 = String             #23            //  Hello World!
   #4 = Methodref          #24.#25        //  java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            //  think/twice/code/once/HelloWorld //类引用
   #6 = Class              #27            //  java/lang/Object //类引用
   #7 = Utf8               <init>          
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lthink/twice/code/once/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          //  "<init>":()V //返回值
  #21 = Class              #28            //  java/lang/System
  #22 = NameAndType        #29:#30        //  out:Ljava/io/PrintStream; 
  #23 = Utf8               Hello World!
  #24 = Class              #31            //  java/io/PrintStream
  #25 = NameAndType        #32:#33        //  println:(Ljava/lang/String;)V
  #26 = Utf8               think/twice/code/once/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V

#数字相当于是常量池里的一个索引,例如上面代码段里 #1 代表的是一个方法引用,并且该引用由 #6.#20 构成。

#1 = Methodref          #6.#20         //  java/lang/Object."<init>":()V //方法引用

#6 = Class              #27            //  java/lang/Object //类引用
#27 = Utf8               java/lang/Object

#20 = NameAndType        #7:#8          //  "<init>":()V //返回值
#7 = Utf8               <init>          
#8 = Utf8               ()V

在 JVM 规范中常量类型定义了很多,本次只汇总遇到的几个。

四:构造方法信息

public think.twice.code.once.HelloWorld();
    descriptor: ()V  //方法描述符,这里的V表示void
    flags: ACC_PUBLIC  //权限修饰符
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0       // aload_0 把this装载到了操作数栈中
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return        
      LineNumberTable: //行号表
        line 3: 0      //源代码的第 3 行,0 代表字节码里的 0
      LocalVariableTable:  // 本地变量表
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lthink/twice/code/once/HelloWorld; // 索引为0,变量名称为 this

descriptor:方法入参和返回描述; flags:访问权限控制符为 public; stack:方法对应栈帧中的操作数栈的深度为 1; locals:本地变量数量为 1; args_size:参数数量为 1; aload:从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶; invokespecial:调用一个初始化方法; LineNumberTable、LocalVariableTable:前者代表行号表,是为调试器提供源码行号与字节码的映射关系;后者代码本地变量表,存放方法的局部变量信息,属于调试信息。

思考一:通过这段字节码信息,印证了一个准则:在没有显示声明构造的情形下,Java 会默认提供无参构造方法。

思考二:虽然是无参构造器,为什么 args_size 的值是 1 呢?是因为无参构造器和非静态方法调用会默认传入 this 变量参数,其中 aload_0 即表示的 this。

五:main 方法的信息

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return        
      LineNumberTable:
        line 5: 0
        line 6: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;

通过 descriptor 、flags 能直观的能够读懂 main 方法的入参,返回值以及访问修饰符;通过 LocalVariableTable 运行时候的局部变量表,能够看到 main 函数的 args 参数保存在了 LocalVariableTable 中。

3

解剖:main 方法的运行流程。

重点关注 main 方法中的如下指令(红色圈住部分)

(一)指令 getstatic #2

表示从索引位置 2 获取静态变量,而 #2 又是引用 #21.#22 构成。

#2 = Fieldref           #21.#22        //  java/lang/System.out:Ljava/io/PrintStream; //字段引用

#21 = Class              #28            //  java/lang/System
#28 = Utf8               java/lang/System

#22 = NameAndType        #29:#30        //  out:Ljava/io/PrintStream;
#29 = Utf8               out
#30 = Utf8               Ljava/io/PrintStream;

兜了一大圈,其实 getstatic #2 指令就是为了拿到输出对象流。

(二)指令 ldc #3

指令 ldc #3 是把常量压入栈中,#3 对应的是字符串 Hello World。

#3 = String             #23            //  Hello World!
#23 = Utf8               Hello World!

(三)指令 invokevirtual #4

invokevirtual #4 是方法引用,查表过去就是 #24.#25

#4 = Methodref          #24.#25        //  java/io/PrintStream.println:(Ljava/lang/String;)V

#24 = Class              #31            //  java/io/PrintStream
#31 = Utf8               java/io/PrintStream

#25 = NameAndType        #32:#33        //  println:(Ljava/lang/String;)V
#32 = Utf8               println
#33 = Utf8               (Ljava/lang/String;)V

#24 则是类引用 #31 java/io/PrintStream,#25 则是方法 println((Ljava/lang/String;)V) 的引用,这里其实是在执行打印操作。

最后,贴一个字节码里的指令与源代码的一个对应关系图。

4

寄语写最后

本次,主要对 Java 字节码有个简单的认识,让大家从字节码角度看看 HelloWorld,看似很容易的入门程序,背后的原理确实不简单。希望通过本次分享,大家对 Java 字节码不再陌生,也希望大家能够学以致用,能够亲自去分析 i++、++i ;字符串拼接效率等诸多场景执行原理。

另外,在 Java 的世界里,有 Java Language Specificatio、Java Virtual Machine Specification 两种规范,直译过来就是 Java 语言规范以及 JVM 规范,本次主要参考 JVM 规范。

闲暇之余,推荐大家多读一读:

https://docs.oracle.com/javase/specs/index.html
https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf

好了,本次就谈到这里,一起聊技术、谈业务、喷架构,少走弯路,不踩大坑。会持续输出原创精彩分享,敬请期待!

推荐阅读:

Java线程池深度揭秘

彻底搞懂 Java 线程池,干啥都不再发憷

Java程序跑的快,全要靠线程带

fastjson的这些坑,你误入了没?

真实|技术人员该如何站好最后一班岗?

Java 8 的这些特性,你知道吗?

改掉这些坏习惯,还怕写不出健壮的代码?(一)

改掉这些坏习惯,还怕写不出优雅的代码?(二)

改掉这些坏习惯,还怕写不出优雅的代码?(三)

改掉这些坏习惯,还怕写不出健壮的代码?(四)

改掉这些坏习惯,还怕写不出精简的代码?(五)

改掉这些坏习惯,还怕写不出精简的代码?(六)

本文分享自微信公众号 - 一猿小讲(yiyuanxiaojiangV5),作者:一猿小讲

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-08-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 程序员进阶系列:多图教你掌握JVM

    提及 JVM 这个词,估计大家都能简单说两句,但是根据身边朋友以及诸多粉丝提出的疑问,能系统讲出来 JVM 的却真心很少。

    一猿小讲
  • 程序员进阶系列:年少不懂爱家家,懂了已是猿中人。

    时隔多年,回想起那个面试场景,忍不住要感叹:年少不懂i++(爱家家),如今懂了却已是老码农(双鬓白)。

    一猿小讲
  • 【漫画】程序员们想说的是啥,你真听懂了吗?

    编程软文
  • Android应用实战,不懂代码也可以开发

    通过上一期的学习,我们成功开发了Android学习的第一个应用程序,不仅可以在Android模拟器上运行,同时还能在我们的Android手机上运行,是不...

    分享达人秀
  • 程序员的你,真的会写 commit 信息吗?

    作为一名优秀的程序员,作为一个优秀的团队,作为一家优秀的软件公司,不可能不用版本控制工具。

    程序员小跃
  • 深度剖析Kubernetes动态准入控制之Initializers

    Author: xidianwangtao@gmail.com Admission Controll的最佳配置 配置过kube-apiserver的同学一...

    Walton
  • 108个程序员的笑话,你都看得懂吗?

    1、程序猿最烦两件事,第一件事是别人要他给自己的代码写文档,第二件呢?是别人的程序没有留下文档。 2、程序猿的读书历程:x语言入门—>x语言应用实践—>x语言高...

    智能算法
  • 程序员进阶系列:实战自己动手编译 JDK

    如若针对业务开发而言,编译或者深入 JDK 源码,感觉没太大意义,但是若想要深入了解 Java 虚拟机的一些实现原理,那么自己动手编译 JDK 就显着非常有意义...

    一猿小讲
  • JAVA简介及特性

    兮动人
  • Java学习笔记_零基础系列(三)Java的5种语言特性

    Java语言屏蔽了指针概念,程序员不能直接操作指针,或者说程序员不能直接操作内存。这种方式有优点也有缺点:

    牛仔码农
  • 要成为一个 Java 架构师得学习哪些知识?

    既然java架构师, 首先你要是一个高级java攻城尸, 熟练使用各种框架,并知道它们实现的原理。 jvm虚拟机原理、调优,懂得jvm能让你写出性能更好的代码;...

    春哥大魔王
  • 震惊:编程或者软件开发竟然不算知识?

    自从上次看了业界大神阮一峰发的那篇文章之后,给我的印象非常的深刻,一直想写文章跟大家交流和探讨一下,那就是:软件开发到底算不算知识呢?

    非著名程序员
  • 为什么我坚持使用 JavaScript 函数声明

    时光溯回到上世纪 90 年代晚期,在初次接触 JavaScript 时,老师教我们使用函数声明写下Hello World,它看上去是这样的······ fun...

    CSDN技术头条
  • 轻松理解AOP(面向切面编程)

    本文主要介绍AOP思想,而不是Spring,Spring在本文只做为理解AOP的工具和例子,所以也不打算介绍Spring的Aspect、Join point、...

    bear_fish
  • Java 8的Stream代码,你能看懂吗?

    在Java中,集合和数组是我们经常会用到的数据结构,需要经常对他们做增、删、改、查、聚合、统计、过滤等操作。相比之下,关系型数据库中也同样有这些操作,但是在Ja...

    Java3y
  • Java程序猿 :2016 年终小结010203

    剽悍一小兔
  • 「零门槛多语言 Python/C/C# 通用思想学习系列」第一篇:经典HelloWorld

    在编程语言的学习中,有经验的童鞋可能会知道,当学习完一门语言后,学习其它语言的时候会感觉到轻松;这是因为在编程语言中,很多语法及功能都及其类似,不同的语言往往是...

    公众号 碧油鸡
  • 一个五年开发经验过来人谈如何自学 Python?

    其实python非常适合初学者入门。相比较其他不少主流编程语言,有更好的可读性,因此上手相对容易。自带的各种模块加上丰富的第三方模块,免去了很多底层的工作,可以...

    企鹅号小编
  • 项目管理工具Maven1

    Maven是apache下的开源项目,项目管理工具,管理java项目。

    Java学习

扫码关注云+社区

领取腾讯云代金券