JVM详解之:java class文件的密码本

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: JVM详解之:java 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:


image.png


当然,还是少不了最质朴的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文件打开吧。


你准备好了吗?


来吧,展示吧!


image.png


上图左边是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


image.png


对比一下上面的表格,我们的主版本号是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中存储的常量类型有下面几种:


image.png


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


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


image.png


上面我们讲到了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是什么东西?


这些叫做字段描述符:


image.png


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


access_flags



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

先上密码表:


image.png


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


image.png


没错,就是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的有点不一样:


image.png


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


methods_count和methods[]



接下来是方法信息。


method结构体:


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


method访问权限标记:


image.png


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都用在什么地方呢?


image.png


其中有六个属性对于Java虚拟机正确解释类文件至关重要,他们是:


ConstantValue,Code,StackMapTable,BootstrapMethods,NestHost和NestMembers。


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


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


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


相关文章
|
1月前
|
监控 算法 Java
Java虚拟机(JVM)的垃圾回收机制深度解析####
本文深入探讨了Java虚拟机(JVM)的垃圾回收机制,旨在揭示其背后的工作原理与优化策略。我们将从垃圾回收的基本概念入手,逐步剖析标记-清除、复制算法、标记-整理等主流垃圾回收算法的原理与实现细节。通过对比不同算法的优缺点及适用场景,为开发者提供优化Java应用性能与内存管理的实践指南。 ####
|
24天前
|
监控 算法 Java
Java虚拟机(JVM)垃圾回收机制深度剖析与优化策略####
本文作为一篇技术性文章,深入探讨了Java虚拟机(JVM)中垃圾回收的工作原理,详细分析了标记-清除、复制算法、标记-压缩及分代收集等主流垃圾回收算法的特点和适用场景。通过实际案例,展示了不同GC(Garbage Collector)算法在应用中的表现差异,并针对大型应用提出了一系列优化策略,包括选择合适的GC算法、调整堆内存大小、并行与并发GC调优等,旨在帮助开发者更好地理解和优化Java应用的性能。 ####
30 0
|
16天前
|
Java
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
80 34
|
21天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
23天前
|
存储 监控 算法
Java虚拟机(JVM)垃圾回收机制深度解析与优化策略####
本文旨在深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法及参数调优方法。通过剖析垃圾回收的生命周期、内存区域划分以及GC日志分析,为开发者提供一套实用的JVM垃圾回收优化指南,助力提升Java应用的性能与稳定性。 ####
|
27天前
|
机器学习/深度学习 监控 算法
Java虚拟机(JVM)的垃圾回收机制深度剖析####
本文深入探讨Java虚拟机(JVM)的垃圾回收机制,揭示其工作原理、常见算法、性能调优策略及未来趋势。通过实例解析,为开发者提供优化Java应用性能的思路与方法。 ####
39 1
|
27天前
|
Oracle 安全 Java
深入理解Java生态:JDK与JVM的区分与协作
Java作为一种广泛使用的编程语言,其生态中有两个核心组件:JDK(Java Development Kit)和JVM(Java Virtual Machine)。本文将深入探讨这两个组件的区别、联系以及它们在Java开发和运行中的作用。
68 1
|
1月前
|
消息中间件 存储 Java
RocketMQ文件刷盘机制深度解析与Java模拟实现
【11月更文挑战第22天】在现代分布式系统中,消息队列(Message Queue, MQ)作为一种重要的中间件,扮演着连接不同服务、实现异步通信和消息解耦的关键角色。Apache RocketMQ作为一款高性能的分布式消息中间件,广泛应用于实时数据流处理、日志流处理等场景。为了保证消息的可靠性,RocketMQ引入了一种称为“刷盘”的机制,将消息从内存写入到磁盘中,确保消息持久化。本文将从底层原理、业务场景、概念、功能点等方面深入解析RocketMQ的文件刷盘机制,并使用Java模拟实现类似的功能。
42 3
|
1月前
|
监控 算法 Java
深入理解Java虚拟机(JVM)的垃圾回收机制
【10月更文挑战第21天】 本文将带你深入了解Java虚拟机(JVM)的垃圾回收机制,包括它的工作原理、常见的垃圾收集算法以及如何优化JVM垃圾回收性能。通过本文,你将对JVM垃圾回收有一个全新的认识,并学会如何在实际开发中进行有效的调优。
47 0
|
5月前
|
存储 Java 程序员
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
84 10