JVM详解之:java class文件的密码本_java 密码本-程序员宅基地

技术标签: jvm  jdk  java  # JVM  编程语言  class  

简介

一切的一切都是从javac开始的。从那一刻开始,java文件就从我们肉眼可分辨的文本文件,变成了冷冰冰的二进制文件。

变成了二进制文件是不是意味着我们无法再深入的去了解java class文件了呢?答案是否定的。

机器可以读,人为什么不能读?只要我们掌握java class文件的密码表,我们可以把二进制转成十六进制,将十六进制和我们的密码表进行对比,就可以轻松的解密了。

下面,让我们开始这个激动人心的过程吧。

一个简单的class

为了深入理解java class的含义,我们首先需要定义一个class类:

public class JavaClassUsage {
    

    private int age=18;

    public void inc(int number){
    
        this.age=this.age+ number;
    }
}

很简单的类,我想不会有比它更简单的类了。

在上面的类中,我们定义了一个age字段和一个inc的方法。

接下来我们使用javac来进行编译。

IDEA有没有?直接打开编译后的class文件,你会看到什么?

没错,是反编译过来的java代码。但是这次我们需要深入了解的是class文件,于是我们可以选择 view->Show Bytecode:

当然,还是少不了最质朴的javap命令:

 javap -verbose JavaClassUsage

对比会发现,其实javap展示的更清晰一些,我们暂时选用javap的结果。

编译的class文件有点长,我一度有点不想都列出来,但是又一想只有对才能讲述得更清楚,还是贴在下面:

public class com.flydean.JavaClassUsage
  minor version: 0
  major version: 58
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // com/flydean/JavaClassUsage.age:I
   #8 = Class              #10            // com/flydean/JavaClassUsage
   #9 = NameAndType        #11:#12        // age:I
  #10 = Utf8               com/flydean/JavaClassUsage
  #11 = Utf8               age
  #12 = Utf8               I
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/flydean/JavaClassUsage;
  #18 = Utf8               inc
  #19 = Utf8               (I)V
  #20 = Utf8               number
  #21 = Utf8               SourceFile
  #22 = Utf8               JavaClassUsage.java
{
    
  public com.flydean.JavaClassUsage();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: bipush        18
         7: putfield      #7                  // Field age:I
        10: return
      LineNumberTable:
        line 7: 0
        line 9: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/flydean/JavaClassUsage;

  public void inc(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=2, args_size=2
         0: aload_0
         1: aload_0
         2: getfield      #7                  // Field age:I
         5: iload_1
         6: iadd
         7: putfield      #7                  // Field age:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/flydean/JavaClassUsage;
            0      11     1 number   I
}
SourceFile: "JavaClassUsage.java"

ClassFile的二进制文件

慢着,上面javap的结果好像并不是二进制文件!

对的,javap是对二进制文件进行了解析,方便程序员阅读。如果你真的想直面最最底层的机器代码,就直接用支持16进制的文本编译器把编译好的class文件打开吧。

你准备好了吗?

来吧,展示吧!

上图左边是16进制的class文件代码,右边是对16进制文件的适当解析。大家可以隐约的看到一点点熟悉的内容。

是的,没错,你会读机器语言了!

class文件的密码本

如果你要了解class文件的结构,你需要这个密码本。

如果你想解析class文件,你需要这个密码本。

学好这个密码本,走遍天下都…没啥用!

下面就是密码本,也就是classFile的结构。

ClassFile {
    
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

其中u2,u4表示的是无符号的两个字节,无符号的4个字节。

java class文件就是按照上面的格式排列下来的,按照这个格式,我们可以自己实现一个反编译器(大家有兴趣的话,可以自行研究)。

我们对比着上面的二进制文件一个一个的来理解。

magic

首先,class文件的前4个字节叫做magic word。

看一下十六进制的第一行的前4个字节:

CA FE BA BE 00 00 00 3A 00 17 0A 00 02 00 03 07 

0xCAFEBABE就是magic word。所有的java class文件都是以这4个字节开头的。

来一杯咖啡吧,baby!

多么有诗意的画面。

version

这两个version要连着讲,一个是主版本号,一个是次版本号。

00 00 00 3A

对比一下上面的表格,我们的主版本号是3A=58,也就是我们使用的是JDK14版本。

常量池

接下来是常量池。

首先是两个字节的constant_pool_count。对比一下,constant_pool_count的值是:

00 17

换算成十进制就是23。也就是说常量池的大小是23-1=22。

这里有两点要注意,第一点,常量池数组的index是从1开始到constant_pool_count-1结束。

第二点,常量池数组的第0位是作为一个保留位,表示“不引用任何常量池项目”,为某些特殊的情况下使用。

接下来是不定长度的cp_info:constant_pool[constant_pool_count-1]常量池数组。

常量池数组中存了些什么东西呢?

字符串常量,类和接口名字,字段名,和其他一些在class中引用的常量。

具体的constant_pool中存储的常量类型有下面几种:

每个常量都是以一个tag开头的。用来告诉JVM,这个到底是一个什么常量。

好了,我们对比着来看一下。在constant_pool_count之后,我们再取一部分16进制数据:

上面我们讲到了17是常量池的个数,接下来就是常量数组。

0A 00 02 00 03

首先第一个字节是常量的tag, 0A=10,对比一下上面的表格,10表示的是CONSTANT_Methodref方法引用。

CONSTANT_Methodref又是一个结构体,我们再看一下方法引用的定义:

CONSTANT_Methodref_info {
    
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

从上面的定义我们可以看出,CONSTANT_Methodref是由三部分组成的,第一部分是一个字节的tag,也就是上面的0A。

第二部分是2个字节的class_index,表示的是类在常量池中的index。

第三部分是2个字节的name_and_type_index,表示的是方法的名字和类型在常量池中的index。

先看class_index,0002=2。

常量池的第一个元素我们已经找到了就是CONSTANT_Methodref,第二个元素就是跟在CONSTANT_Methodref后面的部分,我们看下是什么:

07 00 04

一样的解析步骤,07=7,查表,表示的是CONSTANT_Class。

我们再看下CONSTANT_Class的定义:

CONSTANT_Class_info {
    
    u1 tag;
    u2 name_index;
}

可以看到CONSTANT_Class占用3个字节,第一个字节是tag,后面两个字节是name在常量池中的索引。

00 04 = 4, 表示name在常量池中的索引是4。

然后我们就这样一路找下去,就得到了所有常量池中常量的信息。

这样找起来,眼睛都花了,有没有什么简单的办法呢?

当然有,就是上面的javap -version, 我们再回顾一下输出结果中的常量池部分:

Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // com/flydean/JavaClassUsage.age:I
   #8 = Class              #10            // com/flydean/JavaClassUsage
   #9 = NameAndType        #11:#12        // age:I
  #10 = Utf8               com/flydean/JavaClassUsage
  #11 = Utf8               age
  #12 = Utf8               I
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/flydean/JavaClassUsage;
  #18 = Utf8               inc
  #19 = Utf8               (I)V
  #20 = Utf8               number
  #21 = Utf8               SourceFile
  #22 = Utf8               JavaClassUsage.java

以第一行为例,直接告诉你常量池中第一个index的类型是Methodref,它的classref是index=2,它的NameAndType是index=3。

并且直接在后面展示出了具体的值。

描述符

且慢,在常量池中我好像看到了一些不一样的东西,这些I,L是什么东西?

这些叫做字段描述符:

上图是他们的各项含义。除了8大基础类型,还有2个引用类型,分别是对象的实例,和数组。

access_flags

常量池后面就是access_flags:访问描述符,表示的是这个class或者接口的访问权限。

先上密码表:

再找一下我们16进制的access_flag:

没错,就是00 21。 参照上面的表格,好像没有21,但是别怕:

21是ACC_PUBLIC和ACC_SUPER的并集。表示它有两个access权限。

this_class和super_class

接下来是this class和super class的名字,他们都是对常量池的引用。

00 08 00 02

this class的常量池index=8, super class的常量池index=2。

看一下2和8都代表什么:

   #2 = Class              #4             // java/lang/Object
   #8 = Class              #10            // com/flydean/JavaClassUsage

没错,JavaClassUsage的父类是Object。

大家知道为什么java只能单继承了吗?因为class文件里面只有一个u2的位置,放不下了!

interfaces_count和interfaces[]

接下来就是接口的数目和接口的具体信息数组了。

00 00

我们没有实现任何接口,所以interfaces_count=0,这时候也就没有interfaces[]了。

fields_count和fields[]

然后是字段数目和字段具体的数组信息。

这里的字段包括类变量和实例变量。

每个字段信息也是一个结构体:

field_info {
    
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

字段的access_flag跟class的有点不一样:

这里我们就不具体对比解释了,感兴趣的小伙伴可以自行体验。

methods_count和methods[]

接下来是方法信息。

method结构体:

method_info {
    
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

method访问权限标记:

attributes_count和attributes[]

attributes被用在ClassFile, field_info, method_info和Code_attribute这些结构体中。

先看下attributes结构体的定义:

attribute_info {
    
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}

都有哪些attributes, 这些attributes都用在什么地方呢?

其中有六个属性对于Java虚拟机正确解释类文件至关重要,他们是:
ConstantValue,Code,StackMapTable,BootstrapMethods,NestHost和NestMembers。

九个属性对于Java虚拟机正确解释类文件不是至关重要的,但是对于通过Java SE Platform的类库正确解释类文件是至关重要的,他们是:

Exceptions,InnerClasses,EnclosingMethod,Synthetic,Signature,SourceFile,LineNumberTable,LocalVariableTable,LocalVariableTypeTable。

其他13个属性,不是那么重要,但是包含有关类文件的元数据。

总结

最后留给大家一个问题,java class中常量池的大小constant_pool_count是2个字节,两个字节可以表示2的16次方个常量。很明显已经够大了。

但是,万一我们写了超过2个字节大小的常量怎么办?欢迎大家留言给我讨论。

本文链接:http://www.flydean.com/jvm-class-file-structure/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/superfjj/article/details/107352521

智能推荐

使用nginx解决浏览器跨域问题_nginx不停的xhr-程序员宅基地

文章浏览阅读1k次。通过使用ajax方法跨域请求是浏览器所不允许的,浏览器出于安全考虑是禁止的。警告信息如下:不过jQuery对跨域问题也有解决方案,使用jsonp的方式解决,方法如下:$.ajax({ async:false, url: 'http://www.mysite.com/demo.do', // 跨域URL ty..._nginx不停的xhr

在 Oracle 中配置 extproc 以访问 ST_Geometry-程序员宅基地

文章浏览阅读2k次。关于在 Oracle 中配置 extproc 以访问 ST_Geometry,也就是我们所说的 使用空间SQL 的方法,官方文档链接如下。http://desktop.arcgis.com/zh-cn/arcmap/latest/manage-data/gdbs-in-oracle/configure-oracle-extproc.htm其实简单总结一下,主要就分为以下几个步骤。..._extproc

Linux C++ gbk转为utf-8_linux c++ gbk->utf8-程序员宅基地

文章浏览阅读1.5w次。linux下没有上面的两个函数,需要使用函数 mbstowcs和wcstombsmbstowcs将多字节编码转换为宽字节编码wcstombs将宽字节编码转换为多字节编码这两个函数,转换过程中受到系统编码类型的影响,需要通过设置来设定转换前和转换后的编码类型。通过函数setlocale进行系统编码的设置。linux下输入命名locale -a查看系统支持的编码_linux c++ gbk->utf8

IMP-00009: 导出文件异常结束-程序员宅基地

文章浏览阅读750次。今天准备从生产库向测试库进行数据导入,结果在imp导入的时候遇到“ IMP-00009:导出文件异常结束” 错误,google一下,发现可能有如下原因导致imp的数据太大,没有写buffer和commit两个数据库字符集不同从低版本exp的dmp文件,向高版本imp导出的dmp文件出错传输dmp文件时,文件损坏解决办法:imp时指定..._imp-00009导出文件异常结束

python程序员需要深入掌握的技能_Python用数据说明程序员需要掌握的技能-程序员宅基地

文章浏览阅读143次。当下是一个大数据的时代,各个行业都离不开数据的支持。因此,网络爬虫就应运而生。网络爬虫当下最为火热的是Python,Python开发爬虫相对简单,而且功能库相当完善,力压众多开发语言。本次教程我们爬取前程无忧的招聘信息来分析Python程序员需要掌握那些编程技术。首先在谷歌浏览器打开前程无忧的首页,按F12打开浏览器的开发者工具。浏览器开发者工具是用于捕捉网站的请求信息,通过分析请求信息可以了解请..._初级python程序员能力要求

Spring @Service生成bean名称的规则(当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致)_@service beanname-程序员宅基地

文章浏览阅读7.6k次,点赞2次,收藏6次。@Service标注的bean,类名:ABDemoService查看源码后发现,原来是经过一个特殊处理:当类的名字是以两个或以上的大写字母开头的话,bean的名字会与类名保持一致public class AnnotationBeanNameGenerator implements BeanNameGenerator { private static final String C..._@service beanname

随便推点

二叉树的各种创建方法_二叉树的建立-程序员宅基地

文章浏览阅读6.9w次,点赞73次,收藏463次。1.前序创建#include&lt;stdio.h&gt;#include&lt;string.h&gt;#include&lt;stdlib.h&gt;#include&lt;malloc.h&gt;#include&lt;iostream&gt;#include&lt;stack&gt;#include&lt;queue&gt;using namespace std;typed_二叉树的建立

解决asp.net导出excel时中文文件名乱码_asp.net utf8 导出中文字符乱码-程序员宅基地

文章浏览阅读7.1k次。在Asp.net上使用Excel导出功能,如果文件名出现中文,便会以乱码视之。 解决方法: fileName = HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8);_asp.net utf8 导出中文字符乱码

笔记-编译原理-实验一-词法分析器设计_对pl/0作以下修改扩充。增加单词-程序员宅基地

文章浏览阅读2.1k次,点赞4次,收藏23次。第一次实验 词法分析实验报告设计思想词法分析的主要任务是根据文法的词汇表以及对应约定的编码进行一定的识别,找出文件中所有的合法的单词,并给出一定的信息作为最后的结果,用于后续语法分析程序的使用;本实验针对 PL/0 语言 的文法、词汇表编写一个词法分析程序,对于每个单词根据词汇表输出: (单词种类, 单词的值) 二元对。词汇表:种别编码单词符号助记符0beginb..._对pl/0作以下修改扩充。增加单词

android adb shell 权限,android adb shell权限被拒绝-程序员宅基地

文章浏览阅读773次。我在使用adb.exe时遇到了麻烦.我想使用与bash相同的adb.exe shell提示符,所以我决定更改默认的bash二进制文件(当然二进制文件是交叉编译的,一切都很完美)更改bash二进制文件遵循以下顺序> adb remount> adb push bash / system / bin /> adb shell> cd / system / bin> chm..._adb shell mv 权限

投影仪-相机标定_相机-投影仪标定-程序员宅基地

文章浏览阅读6.8k次,点赞12次,收藏125次。1. 单目相机标定引言相机标定已经研究多年,标定的算法可以分为基于摄影测量的标定和自标定。其中,应用最为广泛的还是张正友标定法。这是一种简单灵活、高鲁棒性、低成本的相机标定算法。仅需要一台相机和一块平面标定板构建相机标定系统,在标定过程中,相机拍摄多个角度下(至少两个角度,推荐10~20个角度)的标定板图像(相机和标定板都可以移动),即可对相机的内外参数进行标定。下面介绍张氏标定法(以下也这么称呼)的原理。原理相机模型和单应矩阵相机标定,就是对相机的内外参数进行计算的过程,从而得到物体到图像的投影_相机-投影仪标定

Wayland架构、渲染、硬件支持-程序员宅基地

文章浏览阅读2.2k次。文章目录Wayland 架构Wayland 渲染Wayland的 硬件支持简 述: 翻译一篇关于和 wayland 有关的技术文章, 其英文标题为Wayland Architecture .Wayland 架构若是想要更好的理解 Wayland 架构及其与 X (X11 or X Window System) 结构;一种很好的方法是将事件从输入设备就开始跟踪, 查看期间所有的屏幕上出现的变化。这就是我们现在对 X 的理解。 内核是从一个输入设备中获取一个事件,并通过 evdev 输入_wayland

推荐文章

热门文章

相关标签