每日一博 | 从 1+1=2 来理解 Java 字节码
背景
前不久《深入理解Java虚拟机》第三版发布了,赶紧买来看了看新版的内容,这本书更新了很多新版本虚拟机的内容,还对以前的部分内容进行了重构,还是值得去看的。本着复习和巩固的态度,我决定来编译一个简单的类文件来分析Java的字节码内容,来帮助理解和巩固Java字节码知识,希望也对阅读本文的你有所帮助。
说明:本次采用的环境是OpenJdk12
编译“1+1”代码
首先我们需要写个简单的小程序,1+1的程序,学习就要从最简单的1+1开始,代码如下:
package top.luozhou.test; /** * @description: * @author: luozhou * @create: 2019-12-25 21:28 **/ public class TestJava { public static void main(String[] args) { int a=1+1; System.out.println(a); } }
写好java类文件后,首先执行命令javac TestJava.java
编译类文件,生成TestJava.class
。 然后执行反编译命令javap -verbose TestJava
,字节码结果显示如下:
Compiled from "TestJava.java" public class top.luozhou.test.TestJava minor version: 0 major version: 56 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #5.#14 // java/lang/Object."<init>":()V #2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream; #3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V #4 = Class #19 // top/luozhou/test/TestJava #5 = Class #20 // java/lang/Object #6 = Utf8 <init> #7 = Utf8 ()V #8 = Utf8 Code #9 = Utf8 LineNumberTable #10 = Utf8 main #11 = Utf8 ([Ljava/lang/String;)V #12 = Utf8 SourceFile #13 = Utf8 TestJava.java #14 = NameAndType #6:#7 // "<init>":()V #15 = Class #21 // java/lang/System #16 = NameAndType #22:#23 // out:Ljava/io/PrintStream; #17 = Class #24 // java/io/PrintStream #18 = NameAndType #25:#26 // println:(I)V #19 = Utf8 top/luozhou/test/TestJava #20 = Utf8 java/lang/Object #21 = Utf8 java/lang/System #22 = Utf8 out #23 = Utf8 Ljava/io/PrintStream; #24 = Utf8 java/io/PrintStream #25 = Utf8 println #26 = Utf8 (I)V { public top.luozhou.test.TestJava(); 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 8: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: iconst_2 1: istore_1 2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 5: iload_1 6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 9: return LineNumberTable: line 10: 0 line 11: 2 line 12: 9 }
解析字节码
1.基础信息
上述结果删除了部分不影响解析的冗余信息,接下来我们便来解析字节码的结果。
minor version: 0 次版本号,为0表示未使用 major version: 56 主版本号,56表示jdk12,表示只能运行在jdk12版本以及之后的虚拟机中
flags: ACC_PUBLIC, ACC_SUPER
ACC_PUBLIC
:这就是一个是否是public类型的访问标志。
ACC_SUPER
: 这个falg是为了解决通过 invokespecial
指令调用 super 方法的问题。可以将它理解成 Java 1.0.2 的一个缺陷补丁,只有通过这样它才能正确找到 super 类方法。从 Java 1.0.2 开始,编译器始终会在字节码中生成 ACC_SUPER 访问标识。感兴趣的同学可以点击这里来了解更多。
2.常量池
接下来,我们将要分析常量池,你也可以对照上面整体的字节码来理解。
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
这是一个方法引用,这里的#5
表示索引值,然后我们可以发现索引值为5的字节码如下
#5 = Class #20 // java/lang/Object
它表示这是一个Object
类,同理#14
指向的是一个"<init>":()V
表示引用的是初始化方法。
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
上面这段表示是一个字段引用,同样引用了#15
和#16
,实际上引用的就是java/lang/System
类中的PrintStream
对象。其他的常量池分析思路是一样的,鉴于篇幅我就不一一说明了,只列下其中的几个关键类型和信息。
NameAndType
:这个表示是名称和类型的常量表,可以指向方法名称或者字段的索引,在上面的字节码中都是表示的实际的方法。
Utf8
:我们经常使用的是字符编码,但是这个不是只有字符编码的意思,它表示一种字符编码是Utf8
的字符串。它是虚拟机中最常用的表结构,你可以理解为它可以描述方法,字段,类等信息。 比如:
#4 = Class #19 #19 = Utf8 top/luozhou/test/TestJava
这里表示#4
这个索引下是一个类,然后指向的类是#19
,#19
是一个Utf8
表,最终存放的是top/luozhou/test/TestJava
,那么这样一连接起来就可以知道#4
位置引用的类是top/luozhou/test/TestJava
了。
3.构造方法信息
接下来,我们分析下构造方法的字节码,我们知道,一个类初始化的时候最先执行它的构造方法,如果你没有写构造方法,系统会默认给你添加一个无参的构造方法。
public top.luozhou.test.TestJava(); 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 8: 0
descriptor: ()V
:表示这是一个没有返回值的方法。
flags: ACC_PUBLIC
:是公共方法。
stack=1, locals=1, args_size=1
:表示栈中的数量为1,局部变量表中的变量为1,调用参数也为1。
这里为什么都是1呢?这不是默认的构造方法吗?哪来的参数?其实Java语言有一个潜规则:在任何实例方法里面都可以通过this
来访问到此方法所属的对象。而这种机制的实现就是通过Java编译器在编译的时候作为入参传入到方法中了,熟悉python
语言的同学肯定会知道,在python
中定义一个方法总会传入一个self
的参数,这也是传入此实例的引用到方法内部,Java只是把这种机制后推到编译阶段完成而已。所以,这里的1都是指this
这个参数而已。
0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0
经过上面这个分析对于这个构造方法表达的意思也就很清晰了。
aload_0
:表示把局部变量表中的第一个变量加载到栈中,也就是this
。
invokespecial
:直接调用初始化方法。
return
:调用完毕方法结束。
LineNumberTable:
这是一个行数的表,用来记录字节码的偏移量和代码行数的映射关系。line 8: 0
表示,源码中第8行对应的就是偏移量0
的字节码,因为是默认的构造方法,所以这里并无法直观体现出来。
另外这里会执行Object
的构造方法是因为,Object
是所有类的父类,子类的构造要先构造父类的构造方法。
4.main方法信息
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: iconst_2 1: istore_1 2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 5: iload_1 6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 9: return LineNumberTable: line 10: 0 line 11: 2 line 12: 9
有了之前构造方法的分析,我们接下来分析main
方法也会熟悉很多,重复的我就略过了,这里重点分析code
部分。
stack=2, locals=2, args_size=1
:这里的栈和局部变量表为2,参数还是为1。这是为什么呢?因为main
方法中声明了一个变量a
,所以局部变量表要加一个,栈也是,所以他们是2。那为什么args_size
还是1呢?你不是说默认会把this
传入的吗?应该是2啊。注意:之前说的是在任何实例方法中,而这个main方法是一个静态方法,静态方法直接可以通过类+方法名访问,并不需要实例对象,所以这里就没必要传入了。
0: iconst_2
:将int
类型2推送到栈顶。
1: istore_1
:将栈顶int
类型数值存入第二个本地变量。
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
:获取PrintStream
类。
5: iload_1
: 把第二个int
型本地变量推送到栈顶。
6: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
:调用println
方法。
9: return
:调用完毕结束方法。
这里的LineNumberTable
是有源码的,我们可以对照下我前面描述是否正确:
line 10: 0
: 第10行表示 0: iconst_2
字节码,这里我们发现编译器直接给我们计算好了把2推送到栈顶了。
line 11: 2
:第11行源码对应的是 2: getstatic
获取输出的静态类PrintStream
。
line 12: 9
:12行源码对应的是return
,表示方法结束。
这里我也画了一个动态图片来演示main
方法执行的过程,希望能够帮助你理解:
总结
这篇文章我从1+1的的源码编译开始,分析了生成后的Java字节码,包括类的基本信息,常量池,方法调用过程等,通过这些分析,我们对Java字节码有了比较基本的了解,也知道了Java编译器会把优化手段通过编译好的字节码体现出来,比如我们的1+1=2,字节码字节赋值一个2给变量,而不是进行加法运算,从而优化了我们的代码,提搞了执行效率。
参考
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
CakePHP 4.0.1 发布,PHP 快速开发框架
CakePHP 是一个运用了诸如 ActiveRecord、Association Data Mapping、Front Controller 和 MVC 等著名设计模式的快速开发框架。该项目主要目标是提供一个可以让各种层次的 PHP 开发人员快速地开发出健壮的 Web 应用,而又不失灵活性。 在本月中旬进行了CakePHP 4.0.0发布之后,目前,CakePHP 4.0.1 发布也已正式发布,该版本是4.0 分支的维护版本,修复了多个社区报告的问题。 Bug修复 用户可以期望在 4.0.1 中进行以下更改。有关每次提交,可参阅更改日志。 升级工具的安装空间较小,并且现在与应用程序代码的冲突应该更少。 删除了 Eventdata payload上的 typehint。 删除了控制台选项的空字符串默认值。此举修复了 Argument::hasOption()对于可选选项始终返回 true 的问题。 在重定向和基本 diactoros 响应对象上设置了 CSRF 令牌。 当 route elements 具有正则表达式模式且参数为整数时,URL 生成不再发出类型警告。 现在,当未选择外键...
- 下一篇
Mozilla 发布 MDN Web 开发人员需求评估报告
Mozilla发布了其第一版全球年度 Web 设计人员和开发人员需求调查报告:MDN Web开发人员需求评估(Web DNA)。并表示,希望通过此报告来塑造 Web 平台的未来。 在此次报告中,他们对来自 173 个国家/地区的 28,000 多名开发人员和设计师进行了调查。Mozilla称,其将调查汇总在一起,以传达“开发人员和网络设计师的声音”。 同时,Mozilla方面还表示,他们分析了大众提供的数据,并确定了 28 种不同的需求, 然后将它们分为 14 个不同的主题。其中,在前 5 个需求中有 4 个与浏览器兼容性有关。而文档,调试,框架,安全性和隐私性则排在前 10 位。 Mozilla Web DNA 报告指出,“调查参与者提供的意见已经影响了浏览器供应商如何优先考虑功能开发,以满足网络上和网络外设计人员和开发人员的需求。通过每年制作这份报告,可以跟踪随着时间变化的需求和痛点,使所有利益相关者都能看到他们的努力对网络未来的影响。” Web DNA Report 2019完整版下载 参考消息: https://sdtimes.com/webdev/mozilla-web-dn...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8编译安装MySQL8.0.19
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- Mario游戏-低调大师作品
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS6,CentOS7官方镜像安装Oracle11G
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- 设置Eclipse缩进为4个空格,增强代码规范