通过 javap 命令查看 class 文件的字节码内容
写一个 java 程序
- public class JvmTest
- {
- public static void main( String[] args )
- {
- int a = 2;
- int b = 5;
- int c = b-a;
- System.out.println(c);
- }
- }
然后进行编译
通过 javap 命令查看 class 文件中的字节码内容
javap 用法: javap <options> <classes>
javap 命令中, 可能的选项包括
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的 / 公共类和成员
-package 显示程序包 / 受保护的 / 公共类和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
查看生成的 test.txt 文件内容大致分为 4 个部分:
第一部分: 显示了生成这个 class 的 java 源文件, 版本信息, 生成时间等.
第二部分: 显示了该类中所涉及到常量池, 共 26 个常量.
第三部分: 显示该类的构造器, 编译器自动插入的.
第四部分: 显示了 main 方的信息.(这个是需要我们重点关注的)
常量池
官网文档: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4-140
constant_pool 表中的每个项目都 必须以一个 1 字节的标签开头, 指示 cp_info 条目的类型.
info 数组的内容随的值而变化 tag. 有效标签及其值在表中列出.
每个标签字节后必须跟两个或多个字节, 以提供有关特定常数的信息.
附加信息的格式随标签值的不同而不同.
Constant Type(常量类型) | Value(值) | 说明 |
CONSTANT_Class | 7 | 类或接口的符号引用 |
CONSTANT_Fieldref | 9 | 字段的符号引用 |
CONSTANT_Methodref | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref | 11 | 接口中方法的符号引用 |
CONSTANT_String | 8 | 字符串类型常量 |
CONSTANT_Integer | 3 | 整型常量 |
CONSTANT_Float | 4 | 浮点型常量 |
CONSTANT_Long | 5 | 长整型常量 |
CONSTANT_Double | 6 | 双精度浮点型常量 |
CONSTANT_NameAndType | 12 | 字段或方法的符号引用 |
CONSTANT_Utf8 | 1 | UTF-8 编码的字符串 |
CONSTANT_MethodHandle | 15 | 表示方法句柄 |
CONSTANT_MethodType | 16 | 标志方法类型 |
CONSTANT_InvokeDynamic | 18 | 表示一个动态方法调用点 |
描述符
1. 字段描述符
官网: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.2
字段描述符表示类, 实例或局部变量的类型.
基本类型中, L 和; 的对象类型, 和 [ 的数组类型都是 ASCII 字符.
ClassName 表示以内部形式编码的二进制类或接口名称.
表中显示了字段描述符作为类型的解释.
表示数组类型的字段描述符只有在表示维数小于等于 255 的类型时才有效.
FieldType term(FieldType 术语 ) | Type(类型) | Interpretation(解释) |
B | byte | 有符号字节 |
C | char | 基本多语言平面中的 Unicode 字符代码点,使用 UTF-16 编码 |
D | double | 双精度浮点值 |
F | float | 单精度浮点值 |
I | int | 整数 |
J | long | 长整数 |
L ClassName ; | reference | 类 < span class="emphasis" ztid="248" ow="125" oh="17">ClassName 的一个实例 |
S | short | 短类型 |
Z | boolean | true 要么 & nbsp;false |
[ | reference | 一维数组 |
类型的实例变量的描述符字段 int 是根本 I.
类型的实例变量的字段描述符 Object 为 L java/lang/Object;. 请注意, Object 使用了类的二进制名称的内部形式.
多维数组类型的实例变量的字段描述符 double[][][] 为 [[[D.
2. 方法描述符
官网: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3.3
方法描述符中包含零点或多个参数的描述符, 表示该类型的参数, 该方法需要和返回的描述符, 表示该值的类型 (如果有的话), 该方法返回
字符 V 表示该方法不返回任何值 (其结果为 void).
示例:
方法的方法描述符:
object m(int i, double d, Thread t) {...}
在字节码文件中
(IDLjava/lang/Thread;)Ljava/lang/Objec
需要注意的是的二进制名称的内部形式 Thread 和 Object 使用.
一种方法, 描述符是有效的, 只有当它表示具有 255 或更低的总长度, 其中该长度包括在实例或接口方法调用的情况下, 这种情况的贡献方法参数.
通过将各个参数的贡献相加来计算总长度, 其中 long 或 double 类型的参数对长度贡献两个单位, 而其他类型的参数则贡献一个单位.
无论方法描述器是类方法还是实例方法, 方法描述符都是相同的.
尽管为此传递了实例方法, 但对引用对象的引用除了其预期的参数外, 该事实并未反映在方法描述符中.
对此的引用由调用实例方法的 Java 虚拟机指令隐式传递.
解读方法字节码
主要关注的第四部分
图解
研究 i++ 与 ++i 的不同
i++ 表示, 先返回再 + 1,++i 表示, 先 + 1 再返回.
编写测试代码:
- public class Test2 {
- public static void main(String[] args) {
- new Test2().method1();
- new Test2().method2();
- }
- public void method1(){
- int i = 1;
- int a = i++;
- System.out.println(a); // 打印 1
- }
- public void method2(){
- int i = 1;
- int a = ++i;
- System.out.println(a);// 打印 2
- }
- }
字节码文件
我们主要看的是 method1 和 method2 两个方法的字节码
图解对比
由上图, 可以看见 i++ 与 ++i 在第二步和第三步的时候是颠倒过来的
区别:
i++
只是在本地变量中对数字做了相加, 并没有将数据压入到操作栈
将前面拿到的数字 1, 再次从操作栈中拿到, 压入到本地变量中
++i
将本地变量中的数字做了相加, 并且将数据压入到操作栈
将操作栈中的数据, 再次压入到本地变量中
字符串拼接
字符串的拼接在开发过程中使用是非常频繁的, 常用的方式有三种:
+ 号拼接: str+"456"
StringBuilder 拼接
StringBuffer 拼接
StringBuffer 是保证线程安全的, 效率是比较低的, 我们更多的是使用场景是不会涉及到线程安全的问题的, 所以更多的时候会选择 StringBuilder, 效率会高一些
比较 + 号拼接与 StringBuilder 那个效率高
代码
- package cn.itcast.jvm;
- public class Test3 {
- public static void main(String[] args) {
- new Test3().m1();
- new Test3().m2();
- }
- public void m1(){
- String s1 = "123";
- String s2 = "456";
- String s3 = s1 + s2;
- System.out.println(s3);
- }
- public void m2(){
- String s1 = "123";
- String s2 = "456";
- StringBuilder sb = new StringBuilder();
- sb.append(s1);
- sb.append(s2);
- String s3 = sb.toString();
- System.out.println(s3);
- }
- }
字节码
m1 的字节码
m2
从解字节码中可以看出, m1() 方法源码中是使用 + 号拼接, 但是在字节码中也被编译成了 StringBuilder 方式.
所以, 可以得出结论, 字符串拼接,+ 号和 StringBuilder 是相等的, 效率一样.
比较使用 + 号循环拼接和使用 StringBuilder.append() 方法循环拼接那个效率高
代码
- package cn.itcast.jvm;
- public class Test4 {
- public static void main(String[] args) {
- new Test4().m1();
- new Test4().m2();
- }
- public void m1(){
- String str = "";
- for (int i = 0; i < 5; i++) {
- str = str + i;
- }
- System.out.println(str);
- }
- public void m2(){
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < 5; i++) {
- sb.append(i);
- }
- System.out.println(sb.toString());
- }
- }
字节码
m1
m2
可以看到, m1() 方法中的循环体内, 每一次循环都会创建 StringBuilder 对象, 效率低于 m2() 方法.
来源: http://www.bubuko.com/infodetail-3456870.html