浅谈JVM之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次方个常量。很明显已经够大了。

以上就是浅谈JVM之java class文件的密码本的详细内容,更多关于JVM之java class文件的密码本的资料请关注我们其它相关文章!

(0)

相关推荐

  • 解析Java Class 文件过程

    前言: 身为一个java程序员,怎么能不了解JVM呢,倘若想学习JVM,那就又必须要了解Class文件,Class之于虚拟机,就如鱼之于水,虚拟机因为Class而有了生命.<深入理解java虚拟机>中花了一整个章节来讲解Class文件,可是看完后,一直都还是迷迷糊糊,似懂非懂.正好前段时间看见一本书很不错:<自己动手写Java虚拟机>,作者利用go语言实现了一个简单的JVM,虽然没有完整实现JVM的所有功能,但是对于一些对JVM稍感兴趣的人来说,可读性还是很高的.作者讲解的很详细,

  • JVM中ClassLoader类加载器的深入理解

    JVM的体系结构图 先来看一下JVM的体系结构,如下图: JVM的位置 JVM的位置,如下图: JVM是运行在操作系统之上的,与硬件没有直接的交互,但是可以调用底层的硬件,用JIN(Java本地接口调用底层硬件) JVM结构图中的class files文件 class files文件,是保存在我们电脑本地的字节码文件,.java文件经过编译之后,就会生成一个.class文件,这个文件就是class files所对应的字节码文件,如下图: JVM结构图中的类加载器ClassLoader的解释 类加

  • java虚拟机之JVM调优详解

    JVM常用命令行参数 1. 查看参数列表 虚拟机参数分为基本和扩展两类,在命令行中输入 JAVA_HOME\bin\java就可得到基本参数列表. 在命令行输入 JAVA_HOME\bin\java –X就可得到扩展参数列表. 2. 基本参数说明: -client,-server: 两种Java虚拟机启动方式,client模式启动比较快,但是性能和内存管理相对较差,server模式启动比较慢,但是运行性能比较高,windos上采用的是client模式,Linux采用server模式 -class

  • jvm运行原理以及类加载器实例详解

    JVM运行原理 首先从".java"代码文件,编译成".class"字节码文件,然后类加载器将".class"字节码文件中的类给加载带JVM中,最后就是JVM执行写好的代码.执行过程如下图 类加载器 类加载过程 加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载 加载 一旦JVM进程启动之后,一定会先把类加载到内存中,然后从main()方法的入口代码开始执行 public class

  • 通过实例解析Java class文件编译加载过程

    一.Java从编码到执行 首先我们来看一下Java是如何从编码到执行的呢? 我们有一个x.java文件通过执行javac命令可以变成x.class文件,当我们调用Java命令的时候class文件会被装载到内存中,这个过程叫做classloader.一般情况下我们自己写代码的时候会用到Java的类库,所以在加载的时候也会把Java类库相关的类也加载到内存中.装载完成之后会调用字节码解释器和JIT即时编译器来进行解释和编译,编译完之后由执行引擎开始执行,执行引擎下面对应的就是操作系统硬件了.下图是大

  • jvm之java类加载机制和类加载器(ClassLoader)的用法

    当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载.连接.初始化3个步骤来对该类进行初始化.如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化. 一.类加载过程 1.加载 加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象. 类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础

  • JVM常量池的深入讲解

    提示:这里咱们要说的常量池,常量池就是咱们面试中所说的常量池,谈谈你对常量池的认识?面试官一问咱们就懵逼了,你要记得你脑子中有一张图!!! 剩下的就好办了 提示:请各位大佬批评指正!! 前言 提示:学习的时候会有点头疼哦 一.Class常量池与运行时常量池 Class常量池可以理解为是Class文件中的资源仓库. Class文件中除了包含类的版本.字段.方法.接口等描述信息外,还有一项信息就是 常量池(constant pool table) ,用于存放编译期生成的各种 字面量(Literal)

  • 详解JVM 中的StringTable

    是什么 字符串常量池是 JVM中的一个重要结构,用于存储JVM运行时产生的字符串.在JDK7之前在方法区中,存储的是字符串常量.而字符串常量池在 JDK7开始移入堆中,随之而来的是除了存储字符串常量外,还可以存储字符串引用(因为在堆中,引用堆中的字符串常量很方便,所以可以存储引用).这使得很多字符串的操作在 JDK7中和在之前的版本中执行是不同的结果.这也是为什么字符串相关的问题是如此具有迷惑性的原因之一. 底层 String:在 JDK9之前,String底层是使用 char数组来存储字符串数

  • 浅谈JVM之java class文件的密码本

    简介 机器可以读,人为什么不能读?只要我们掌握java class文件的密码表,我们可以把二进制转成十六进制,将十六进制和我们的密码表进行对比,就可以轻松的解密了. 下面,让我们开始这个激动人心的过程吧. 一个简单的class 为了深入理解java class的含义,我们首先需要定义一个class类: public class JavaClassUsage { private int age=18; public void inc(int number){ this.age=this.age+

  • 浅谈一段java代码是如何执行的

    本文分享自华为云社区<一段java代码是如何执行的>,原文作者:技术火炬手 . 当你学会了java语言之后,你写了一些代码,然后你想要执行你的代码,来达成某些功能.那么,你都知道这段代码都是如何执行的吗? 1. 编译成class 众所周知,java代码是不能直接在jvm上执行的,执行的是class文件,将java代码编程class文件,需要编译 常用的编译方法是:javac xxx.java 但目前常见的java编辑工具,如eclipse和ideal都自带自动编译动能 2. jvm的构成 让我

  • 浅谈JVM之类的加载链接和初始化

    加载 JVM可以分为三大部分,五大空间和三大引擎,要讲起来也不是特别复杂,先看下面的总体的JVM架构图. 从上面的图中,我们可以看到JVM中有三大部分,分别是类加载系统,运行时数据区域和Execution Engine. 加载就是根据特定名称查找类或者接口的二进制表示,并根据此二进制表示来创建类和接口的过程. 运行时常量池 我们知道JVM中有一个方法区的区域,在JDK8中,方法区的实现叫做元空间.这个元空间是存放在本地内存中的. 方法区中存放着每个class对应的运行时常量池. 当类或者接口创建

  • 浅谈JVM垃圾回收有哪些常用算法

    一.前言: 垃圾回收: 在未来的JDK中可能G1会为ZGC所取代 先问自己几个问题: 什么是垃圾? 垃圾就是堆内存中(范指)没有任何指针指向的对象实体.不具有可达性. 为什么要回收垃圾? 因为我们的内存是有限的,内存长时间不清理就会导致内存溢出,OOM: 只要是程序正在跑,那么就不断生成新的对象,我们需要GC开辟新的空间分配给新的对象. 我们怎么回收垃圾? 依靠Java的自动内存回收机制,机制的优劣由算法决定: 或者说是机制的适配度由算法和应用场景共同决定. 什么时候回收垃圾? 当堆中的实体对象

  • 浅谈jvm中的垃圾回收策略

    java和C#中的内存的分配和释放都是由虚拟机自动管理的,此前我已经介绍了CLR中GC的对象回收方式,是基于代的内存回收策略,其实在java中,JVM的对象回收策略也是基于分代的思想.这样做的目的就是为了提高垃圾 回收的性能,避免对堆中的所有对象进行检查时所带来的程序的响应的延迟,因为jvm执行GC时,会stop the word,即终止其它线程的运行,等回收完毕,才恢复其它线程的操作.基于分代的思想是:jvm在每一次执行垃圾收集器时,只是对一小部分内存 对象引用进行检查,这一小部分对象的生命周

  • 浅谈JVM中的JOL

    JOL简介 JOL的全称是Java Object Layout.是一个用来分析JVM中Object布局的小工具.包括Object在内存中的占用情况,实例对象的引用情况等等. JOL可以在代码中使用,也可以独立的以命令行中运行.命令行的我这里就不具体介绍了,今天主要讲解怎么在代码中使用JOL. 使用JOL需要添加maven依赖: <dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-co

  • 浅谈JVM之使用JFR解决内存泄露

    简介 虽然java有自动化的GC,但是还会有内存泄露的情况.当然java中的内存泄露跟C++中的泄露不同. 在C++中所有被分配的内存对象都需要要程序员手动释放.但是在java中并不需要这个过程,一切都是由GC来自动完成的.那么是不是java中就没有内存泄露了呢? 要回答这个问题我们首先需要界定一下什么是内存泄露.如果说有时候我们不再使用的对象却不能被GC释放的话,那么就可以说发生了内存泄露. 一个内存泄露的例子 我们举一个内存泄露的例子,先定义一个大对象: public class KeyOb

  • 浅谈JVM系列之JIT中的Virtual Call

    Virtual Call和它的本质 有用过PrintAssembly的朋友,可能会在反编译的汇编代码中发现有些方法调用的说明是invokevirtual,实际上这个invokevirtual就是Virtual Call. Virtual Call是什么呢? 面向对象的编程语言基本上都支持方法的重写,我们考虑下面的情况: private static class CustObj { public void methodCall() { if(System.currentTimeMillis()==

  • 浅谈JVM 底层解析 i++和 ++i 区别

    目录 一.前言 二.代码实现 三.字节码指令 四.字节码解析 1. 第一类问题 2. 第二类问题 3. 第三类问题 4. 第四类问题 一.前言 如果只用普通的知识解释i++和++i的话 i++ 先将i赋值再++ ++i 先++再赋值 但是这简单的回答并不能入吸引面试官的眼球,如果用java字节码指令分析则效果完全不同 二.代码实现 public class OperandStackTest { /** 程序员面试过程中, 常见的i++和++i 的区别 */ public static void

  • 浅谈JVM内存溢出原因和解决思路

    目录 栈溢出(虚拟机栈和本地方法栈) 产生原因 解决思路 堆溢出 产生原因 解决思路 方法区和运行时常量池溢出 产生原因 解决思路 本机直接内存溢出 产生原因 解决思路 栈溢出(虚拟机栈和本地方法栈) 产生原因 在HotSpot中,只能由-Xss参数来设定.因为在HotSpot中不区分虚拟机栈和本地方法栈的. 栈溢出时会出现两种异常:StackOverflowError异常和OutOfMemoryError异常. StackOverflowError异常因为线程请求的栈深度大于虚拟机允许的最大深

随机推荐