Class文件结构及深入字节码指令

JVM系列文章目录

初识JVM

深入理解JVM内存区域

玩转JVM对象和引用

JVM分代回收机制和垃圾回收算法

细谈JVM垃圾回收与部分底层实现

Class文件结构及深入字节码指令

玩转类加载和类加载器

方法调用的底层实现

Java语法糖及底层实现

GC调优基础知识工具篇之JDK自带工具

GC调优基础知识工具篇之Arthas与动态追踪技术

JVM调优之内存优化与GC优化

JVM调优之预估调优与问题排查

JVM调优之玩转MAT分析内存泄漏

直接内存与JVM源码分析

JVM及时编译器



前言

本文基于JDK1.8,是博主个人的JVM学习记录,欢迎各位指正错误的地方


Class文件


JVM无关性

大家都知道Java一处编译,到处运行(平台无关性,归功于JVM)。JVM的语言无关性则归功于Class文件,因为JVM只与Class文件绑定,不是与Java文件绑定,换句话说任何能被编译成class文件的语言都可以被JVM解析。

在这里插入图片描述

Class类文件

各位有兴趣可以深入了解一下,在了解之前,各位得先准备点工具方便接下来查看Class类文件。


工具介绍

  • Sublime:查看16进制Class源文件
  • javap:这个是JDK自带的反编译工具,将Class文件解析成可读的文件格式。一般使用- v参数查看信息即可,当然你也可以使用-p 参数查看一些私有的字段和方法。
    javap -v class文件名 
    
  • jclasslib:这是一个可视化的工具,它会将信息分门别类,方便更直观的查看Class文件信息。如果你是用的是idea的话,可以在插件市场(plugins)上搜索到它,将其作为插件安装即可。
    在这里插入图片描述

Class文件格式

这里我先贴一张图,如果看不清的话我这边同时也在CSDN上传了该图的资源(Class文件结构)。在这里插入图片描述
根据根据上面的图,在结合我下面提供的代码和Class文件解析来对照说明。

代码如下:

package ex6;

/**
 * @author abfeathers
 * 字节码分析
 */
public class ByteCode {
    public ByteCode(){
    }
}

在Sublime中打开编译好的Class文件,显示如下:
在这里插入图片描述
它使用一种类似于c语言的伪数据结构来存储数据,这种数据结构中只有两种类型:无符号数
无符号数:属于基本数据类型,以U1(一个字节,一个字节是两位16进制数据)、U2(两个字节)、U4(四个字节)、U8(8个字节)。

结合上文的Class类结构图,我这个特别介绍以下几个类型:

  1. magic:魔数,U4(占4个字节),如下图的cafe babe ,这个无符号数的意义在于告诉虚拟机这是一个Class文件。
    在这里插入图片描述

  2. minor_version和major_version:minor_version,U2(占两个字节),表示小版本号;major_version,U2(占两个字节),表示大版本号;这两个无符号数,共同表示了JDK版本号,0000 0034 换算成十进制就是52,表示的是JDK1.8。
    在这里插入图片描述

  3. constant_pool_count:常量池计数器,U2(占两个字符),表示这个类文件中有多少个常数项,常量池计数器是从1开始的,1表示没有常量,2表示有一个常量,以此类推。这里0010换算成十进制是16,表示有15项常量。
    在这里插入图片描述
    我们可以用javap校验一下
    在这里插入图片描述

  4. constant_pool:常量池,主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。 字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。 而符号引用则属于编译原理方面的概念,包括了下面三类常量:
    类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。

  5. access_flags:访问标志,用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。

  6. this_class、super_class、interfaces_count、interfaces:类索引、父类索引与接口索引集合,这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于 Java 语言不允许多重继承, 所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。接口索引 集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序 从左到右排列在接口索引集合中。

  7. fields:字段表集合,描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量。 而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。 字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本 Java 代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问 性,会自动添加指向外部类实例的字段。

  8. methods:方法表集合
    描述了方法的定义,但是方法里的 Java 代码,经过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为“Code”的属性里面。 与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“”方法和实例构造器“”。

  9. attributes:属性表集合
    存储 Class 文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在 Code 属性表中。


Class类文件(字节码)小结

  • Class文件是一组以8位字节为基础单位的二进制流。
  • 类似于结构体的伪结构来存储数据。
  • 只有两种数据类型:无符号数和表。
  • 无符号数属于基本的数据类型,以u1、u2、u4、u8。
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型。


字节码指令

这一块各位也是有兴趣可以看一看,我这里挑几个出来讲一讲,想了解详细指令的话可以看这个链接:字节码助记码解释地址


异常处理


异常机制

如果你熟悉Java开发,一定对下图不陌生。其中Error和RuntimeException是非检查型异常,不需要catch去处理。但是其他异常就需要开发人员自己区处理了。
在这里插入图片描述


异常表

正常方法执行的时候,是封装成栈帧,压入虚拟机栈,在虚拟机栈进行一系列运算,最后根据完成出口出栈。那么异常的话呢?异常的话就需要依靠异常表了。
异常表,就是当程序出现异常的时候,告诉计算机该怎么处理的表。这里我举一个例子。
代码如下:

/**
 * @author: abfeathers
 * @date: 2021/3/13
 * @description: 在 synchronized 生成的字节码中,其实包含两条 monitorexit 指令,是为了保证所有的异常条件,都能够退出
 * 这就涉及到了 Java 字节码的异常处理机制
 */
public class SynchronizedDemo {
    synchronized void m1(){
        System.out.println("m1");
    }
    static synchronized void m2(){
        System.out.println("m2");
    }
    final Object lock=new Object();

    void doLock(){
        synchronized (lock){
            System.out.println("lock");
        }
    }
}

我们通过jclasslib来查看字节码信息,也可以通过javap校验一下(以javap的结果为准,毕竟javap才是官方的)。
在这里插入图片描述
在这里插入图片描述
这里介绍几个名词:

  • from :指定字节码索引开始的位置,如上图from 是7 表示的就是这一行。
    在这里插入图片描述
  • to:字节码索引的结束位置,如上图是17,则表示这一行。
    在这里插入图片描述
  • target:如果出现异常,跳转到这里,如上图的20。
    在这里插入图片描述
  • type:异常类型

再结合我上面的Java代码,就可以看出来了,被synchronized修饰的方法,正常执行就走自己的 monitorexit退出,如果异常就跳转到异常处理,最后走异常的 monitorexit退出。


Finally

在平时开发的过程中,我们有时候做try-catch处理的时候无论是否会出现异常都要做一些处理,比如说操作流数据的时候无论如何都要关闭流,这个时候就会使用Finally,那么Finally在字节码中是怎么样的呢?
我们先写一个小demo

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
/**
 * @author: abfeathers
 * @date: 2021/3/13
 * @description: finally字节码的处理
 */
public class StreamDemo {
    public void read(){
        InputStream in = null;
    try {
        in = new FileInputStream("Box.java");
    }catch(FileNotFoundException e){
        e.printStackTrace();
    } finally {
        if (null != in) {
            try {
                in.close();
            }catch(IOException e) {
                e.printStackTrace();
            }
        }
    }
  }
}

在代码中我们捕获了FileNotFoundException,在finally中关闭流的时候我们又捕获了IOException。在查看字节码的时候我发现一个很有意思的地方,IOException足足出现了三次。
在这里插入图片描述
我们再看看异常表
在这里插入图片描述
这里就可以看出来Java底层直接将异常处理复制了2份,分别塞到try和catch里,这就很硬核了。
我们也从代码来进行分析:

/** 
 * @author: abfeathers
 * @date: 2021/3/13
 * @description:  装箱拆箱字节码层面分析
 */
public class Box {
    public Integer cal() {
        Integer a = 1000;
        int b = a * 10;
        return b;
    }
}


拆箱装箱

拆箱装箱这一个很简单的概念了,装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
再看看jclasslib中的代码
在这里插入图片描述
注意看我红色方框,在对照代码,是不是就恍然大悟了?
原来在Java编译的时候,直接封装成了这种指令,1000先读入操作数栈中,再将1000直接调用Integer.valueOf方法进行装箱变成Integer,对照代码int b = a * 10,这里先将a调用Integer.intValue进行拆箱转换成int,在进行计算,最后要返回的时候又将b调用Integer.valueOf转换成Integer。


Integer拓展-IntegerCache

根据上文的拆箱装箱的讲解,我再提供一个代码示例,这个示例特别有意思,大家都知道Integer是一个对象,当int自动装箱成Integer的时候会new一个Integer对象,那么下面这段代码的执行结果是什么?

/**
 * @author: abfeathers
 * @date: 2021/3/13
 * @description:  IntegerCache及修改 -XX:AutoBoxCacheMax=256
 *
 */
public class BoxCache {
    public static void main(String[] args) {
        Integer n1 = 123;
        Integer n2 = 123;
        Integer n3 = 128;
        Integer n4 = 128;

        System.out.println(n1 == n2);
        System.out.println(n3 == n4);
    }
}

在这里插入图片描述
这个结果是不是特别意外?来我们看看字节码
在这里插入图片描述
根据字节码,发现可能问题处在Integer.valueOf中,我们来看看jdk的源码实现:
在这里插入图片描述
嗯?IntegerCache?Integer对象还有缓存?我们同上面的注释了解到,原来对于数值有一个缓存范围,下限(low)是定死的-128,上限(heigh)是可变的,如果没有设置的话默认是127,所以在这个范围内的数值都是会被缓存的。再看看上面的代码123明显在这个范围内,所以n1、n2实际上是引用的缓存里的同一个对象,而128明显不在这个范围内,所以n3、n4是引用的两个不同的对象。

既然是可以参数设置的,我们来通过参数-XX:AutoBoxCacheMax设置一下,再来看看运行结果。
在这里插入图片描述
在这里插入图片描述
是不是很有意思?


数组

数组实际上是JVM的一个内置对象,这个对象同样是继承的 Object 类。我们使用代码来理解一下:

/** 
 * @author: abfeathers
 * @date: 2021/3/13
 * @description: 数组的字节码解析
 */
public class ArrayDemo {
    int getValue() {
        int[] arr = new int[]{1111, 2222, 3333, 4444};
        return arr[2];
    }
    int getLength(int[] arr) {
        return arr.length;
    }
}

在这里插入图片描述
在这里插入图片描述
主要看一下这个getLength方法,这个方法实际上不是JDK的方法,它其实是JVM底层的方法arraylength
在这里插入图片描述


foreach

forech这个的话,我们来对照list和数组的实现来看,从Java代码上来看是一样的。

import java.util.List;

/** 
 * @author: abfeathers
 * @date: 2021/3/13
 * @description: foreach 底层实现的差异
 */
public class ForDemo {
    void loop(int[] arr) {
        for (int i : arr) {
            System.out.println(i);
        }
    }
    void loop(List<Integer> arr) {
        for (int i : arr) {
            System.out.println(i);
        }
    }
}

在这里插入图片描述
在这里插入图片描述
从Class文件来看的确差异很大,但是不太容易看明白,我们对Class文件来进行一次反编译看看。
这是我从idea中直接查看的,区别很明显吧。(当然也可以通过java指令进行反编译查看)
在这里插入图片描述


注解

注解我们直接看方法的注解和类上的注解的区别吧。
我这里先定义一个注解

public @interface AbfeathersAnnotation {
}

然后在写一个测试类

@AbfeathersAnnotation
public class AnnotationDemo {
    @AbfeathersAnnotation
    public void test(@AbfeathersAnnotation int a){
    }
}

这次我们通过javap指令来比较(在jclasslib看不太明显)。

 javap -v AnnotationDemo.class 

在这里插入图片描述

  1. 无论是类的注解,还是方法注解,都是由一个叫做 RuntimeInvisibleAnnotations 的结构来存储的。
  2. 参数的存储,是由 RuntimeInvisibleParameterAnotations 来保证的


上一篇:细谈JVM垃圾回收与部分底层实现

下一篇:玩转类加载和类加载器

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值