JVM字节码文件解析

笔记参考尚硅谷宋红康:

JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ

1.概述

1.字节码文件的跨平台性

1. Java语言:跨平台的语言

  • 当Java源代码成功编译成字节码后,如果想在不同的平台上面运行,则无须再次编译
  • 这个优势不再那么吸引人了。Python、PHP、Ruby、Lisp等有强大的解释器。
  • 跨平台似乎已经快成为一门语言必选的特性。

2. Java虚拟机:跨语言的平台

Java虚拟机不和包括Java在内的任何语言绑定,它只与”Class文件”这种特定的二进制文件格式所关联。无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。可以说,统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁。

官方文档:

Java SE Specifications (oracle.com)

所有的JVM全部遵守Java虚拟机规范,也就是说所有的JVM环境都是一样的,这样一来字节码文件可以在各种JVM上运行。

 

3.想要让一个Java程序正确地运行在JVM中, Java源码就必须要被编译为符合JVM规范的字节码。

  • 前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件。
  • javac是一种能够将Java源码编译为字节码的前端编译器。
  • Javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析、及生成字节码。

Oracle的JDK软件包括两部分内容:

  • 一部分是将Java源代码编译成Java虚拟机的指令集的编译器。
  • 另一部分是用于实现Java虚拟机的运行时环境。

 

2.Java的前端编译器

 

Java源代码的编译结果是字节码,那么肯定需要有一种编译器能够将Java源码编译为字节码,承担这个重要责任的就是配置在path环境变量中的javac编译器。javac是一种能够将Java源码编译为字节码的前端编译器。

也就是平常我们使用的javac

HotSpot VM并没有强制要求前端编译器只能使用javac来编译字节码,其实只要编译结果符合JVM规范都可以被JVM所识别即可。

在Java的前端编译器领域,除了javac之外,还有一种被大家经常用到的前端编译器,那就是内置在Eclipse中的ECJ (Eclipse Compiler for Java)编译器。和Javac的全量式编译不同, ECJ是一种增量式编译器。

  • 在Eclipse中,当开发人员编写完代码后,使用”Ctrl+S”快捷键时, ECJ编译器所采取的编译方案是把未编译部分的源码逐行进行编译,而非每次都全量编译。因此ECJ的编译效率会比javac更加迅速和高效,当然编译质量和javac相比大致还是一样的
  • ECJ不仅是Eclipse的默认内置前端编译器,在Tomcat中同样也是使用ECJ编译器来编译jsp文件。由于ECJ编译器是采用GPLv2的开源协议进行源代码公开,所以,大家可以登录eclipse官网下载ECJ编译器的源码进行二次开发。
  • 默认情况下,IntelliJ IDEA使用javac编译器。(还可以自己设置为AspectJ编译器ajc)
  • 前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节移交给HotSpot的JIT编译器负责。

 

3.透过字节码指令看代码细节

1. 问题

① 类文件结构有几个部分?

② 知道字节码吗?字节码都有哪些? Integer x = 5;int y = 5;比较 x == y 都经过哪些步骤?

 

2.代码举例

package com.atguigu.java;

public class IntegerTest {
        public static void main(String[] args) {

            Integer x = 5;
            int y = 5;
            System.out.println(x == y);

            Integer i1 = 10;
            Integer i2 = 10;
            System.out.println(i1 == i2);//true

            Integer i3 = 128;
            Integer i4 = 128;
            System.out.println(i3 == i4);//false

    }
}

为什么会出现两个是true,一个是false?

看看字节码:

1.

iconst_5把常量5放入操作数栈

 

2.

接着调用了Integer.valueOf

IntegerCache是Interger的一个内部类,其中保存了一个静态数组

 

数组长度为-128到127之间。

也就是说只要在-128到127之间,就返回原值,否则就new一个Interger

所以会出现两个是true,一个是false。

3.

然后,就是放入局部变量表1的位置。

0的位置放这个

 

4.int y = 5

基本数据类型直接放

 

5.

放入局部变量表2的位置。

 

按理说应该输出一个false,但是存在自动拆箱,就输出true

System.out.println(x == y);

为了减少开发人员的工作,Java提供了自动拆箱与自动装箱功能。

自动装箱: 就是将基本数据类型自动转换成对应的包装类。

自动拆箱:就是将包装类自动转换成对应的基本数据类型。

例如:

Interger i = 10可以替代Interger i = new Interger(10);

这就是因为Java帮我们提供了自动装箱的功能,不需要开发者手动去new一个Integer对象;

package com.atguigu.java;

public class StringTest {
    public static void main(String[] args) {
        String str = new String("hello") + new String("world");
        String str1 = "helloworld";
        System.out.println(str == str1);
        String str2 = new String("helloworld");
        System.out.println(str == str2);
    }
}

 

问题主要在这两个append

package com.atguigu.java;

class Father {
    int x = 10;

    public Father() {
        this.print();
        x = 20;
    }
    public void print() {
        System.out.println("Father.x = " + x);
    }
}

class Son extends Father {
    int x = 30;
    // float x = 30.1F;
    public Son() {
        this.print();
        x = 40;
    }
    public void print() {
        System.out.println("Son.x = " + x);
    }
}

public class SonTest {
    public static void main(String[] args) {
        Father f = new Son();
        System.out.println(f.x);
    }
}

所以结果是什么?

成员变量(非静态的)的赋值过程:
① 默认初始化

② 显式初始化 /代码块中初始化

③ 构造器中初始化

④ 有了对象之后,可以“对象.属性”或”对象.方法”的方式对成员变量进行赋值。

所以:当main方法改为new Father();

public class SonTest {
    public static void main(String[] args) {
        Father f = new Father();
        System.out.println(f.x);
    }
}

看看字节码:很明显。

 

再换回

public class SonTest {
    public static void main(String[] args) {
        Father f = new Son();
        System.out.println(f.x);
    }
}
看看字节码:

先进行初始化,子类会调用父类的构造函数。

而在父类中,调用了print()方法,但是子类重写了print()方法,所以,使用子类的print()方法,而此时,在子类还未初始化。所以输出0.

再因为属性不存在多态性,所以f.x输出的是父类的20;

 

 

2.虚拟机的基石:Class文件

Class文件字节码文件里是什么?

源代码经过编译器编译之后便会生成一个字节码文件(存在几个内部类,就多生成几个字节码文件),字节码是一种二进制的类文件,它的内容是JVM的指令,而不像C、C++经由编译器直接生成机器码。

什么是字节码指令(byte code)?

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码,比如:

 

 

3.使用javap指令解析Class文件

1.解析字节码的作用

通过反编译生成的字节码文件,我们可以深入的了解java代码的工作机制。但是,自己分析类文件结构太麻烦了,除了使用第三方的jclasslib工具之外,oracle官方也提供了工具:javap。

javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息。

通过局部变量表,我们可以查看局部变量的作用域范围、所在槽位等信息,甚至可以看到槽位复用等信息。

 

2. javac -g操作

解析字节码文件得到的信息中,有些信息(如局部变量表、指令和代码行偏移量映射表、常量池中方法的参数名称等等)需要在使用javac编译成class文件时,指定参数才能输出。

比如,你直接javac xx.java,就不会在生成对应的局部变量表等信息,如果你使用javac -g xx.java就可以生成所有相关信息了。如果你使用的eclipse或IDEA,则默认情况下, eclipse、IDEA在编译时会帮你生成局部变量表、指令和代码行偏移量映射表等信息的。

 

3.javap的用法

javap的用法格式:javap <options> <classes>其中,classes就是你要反编译的class文件。在命令行中直接输入javap或javap -help可以看到javap的options有如下选项:

 

这里重组一下:

-help --help -?        //输出此用法消息
-version               //版本信息,其实是当前javap所在jdk的版本信息,不是class在哪个jdk下生成的。
-public                //仅显示公共类和成员
-protected             //显示受保护的/公共类和成员
-p -private            //显示所有类和成员
-package               //显示程序包/受保护的/公共类 和成员(默认)
-sysinfo               //显示正在处理的类的系统信息(路径,大小,日期,MD5 散列,源文件名)
-constants             //显示静态最终常量
-s                     //输出内部类型签名
-l                     //输出行号和本地变量表
-c                     //对代码进行反汇编
-v -verbose            //输出附加信息(包括行号、本地变量表,反汇编等详细信息)
-classpath <path>      //指定查找用户类文件的位置
-cp <path>             //指定查找用户类文件的位置
-bootclasspath <path>  //覆盖引导类文件的位置

 

使用此代码演示:

package com.yutian.java1;

public class JavapTest {
    private int num;
    boolean flag;
    protected char gender;
    public String info;

    public static final int COUNTS = 1;
    static{
        String url = "崩坏3天下第一";
    }
    {
        info = "java";
    }
    public JavapTest(){

    }
    private JavapTest(boolean flag){
        this.flag = flag;
    }
    private void methodPrivate(){

    }
    int getNum(int i){
        return num + i;
    }
    protected char showGender(){
        return gender;
    }
    public void showInfo(){
        int i = 10;
        System.out.println(info + i);
    }
}

 

-help –help -? //输出此用法消息

-version //版本信息,其实是当前javap所在jdk的版本信息,不是class在哪个jdk下生成的。

 

-public //仅显示公共类和成员

javap -public JavapTest.class

 

-protected //显示受保护的/公共类和成员

 

-p或-private //显示所有类和成员

 

-package //显示程序包/受保护的/公共类 和成员(默认)

 

-sysinfo //显示正在处理的类的系统信息(路径,大小,日期,MD5 散列,源文件名)

 

-constants //显示静态最终常量

 

-s //输出内部类型签名(默认不带私有,可以加上-p)

 

-l  //输出行号和本地变量表,(注意:如果直接javac xx.java来生成的class文件,不会在生成对应的局部变量表等信息)

 

-c //对代码进行反汇编(主要是Code属性)

 

-v或-verbose //输出附加信息(包括行号、本地(局部)变量表,反汇编等详细信息)(默认不带私有,可以加上-p)

 

-classpath <path> //指定查找用户类文件的位置

-cp <path> //指定查找用户类文件的位置

-bootclasspath <path>//覆盖引导类文件的位置

 

一般常用的是-v -l -c三个选项:

javap -l 会输出行号和本地变量表信息。

javap -c 会对当前class字节码进行反编译生成汇编代码。

javap -v classxx除了包含-c内容外,还会输出行号、局部变量表信息、常量池等信息。

 

详解:

javap -v -p JavapTest.class

 

 

javap -v -p 生成的,会把其列成类似方法,而jclass描述成两个<init>

 

args_size:方法接收参数的数量(默认存在this)

而static方法,在jclass显示

 

4.总结

1、通过javap命令可以查看一个java类反汇编得到的Class文件版本号、常量池、访问标识、变量表、指令代码行号表等等信息。不显示类索引、父类索引、接口索引集合、<clinit>()、<init>()等结构

2、通过对前面例子代码反汇编文件的简单分析,可以发现,一个方法的执行通常会涉及下面几块内存的操作:

  • (1) java栈中:局部变量表、操作数栈。
  • (2) java堆。通过对象的地址引用去操作。
  • (3) 常量池。
  • (4)其他如帧数据区、方法区的剩余部分等情况,测试中没有显示出来,这里说明一下。

 

3、平常,我们比较关注的是java类中每个方法的反汇编中的指令操作过程,这些指令都是顺序执行的,可以参考官方文档查看每个指令的含义

Chapter 6. The Java Virtual Machine Instruction Set (oracle.com)

 

参考资料:

尚硅谷宋红康:JVM全套教程:https://www.bilibili.com/video/BV1PJ411n7xZ

周志明:深入理解java虚拟机

张秀宏:自己动手写Java虚拟机 (Java核心技术系列)

Java虚拟机规范:Chapter 4. The class File Format (oracle.com)

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇