前言

java语言在其刚诞生之际喊出的口号--"Write Once,Run Anywhere",正是基于字节码(byte code)而存在的,java能够做到平台无关性,得力于这样一款优秀的中间语言,字节码的描述能力比java更强,所以它当然还不止为java服务,它同样为运行于JVM的其他语言服务,以作为一款通用的,与平台无关的,交付给JVM执行的媒介

广义的class文件就是字节码,但字节码不仅仅是class文件,它作为一段二进制流,还可以以其他各种形式存在,如压缩包(jar)、网络流等等

Tips:理论上任何编程语言,只要能被编译成字节码,就可以交付给JVM去跨平台的运行

class文件的前世今生

java发展已经有了二十多个年头了,但class文件的格式几乎没有任何改变,为了代码的向后兼容性,少数的几次class文件格式更新也是在原本结构上进行增加功能,并不更改已经定义的功能


class文件的特点

二进制流

由于class文件不需要可读性,为了节省空间,内部的数据按照既定的顺序紧凑的排列在一起,没有任何无用数据(空格、分隔符),全部都是重要数据。

正是因为不像XML\JSON等含分隔符,所以为了准确性,每个字节的含义、长度、顺序都是严格规定,不能改变

仅表示一个类

一个class文件表示一个类,如果一个java文件里写了多个类(包括内部类),会被编译成多个class文件

仅含两种数据类型

  1. 无符号数。以u1、u2、u4、u8来分别代表1字节、2字节、4字节、8字节的无符号数,并且是高位在前,即高位存储在地址最低位,和x86等处理器恰恰相反。
  2. 表。其实就是由多个无符号数组合而成的复合类型,也可以含有表,即嵌套结构,整个Class文件也可以视作一个表

魔数与版本号

如同计算机里的其他文件一样,class文件也被授予了一个标准文件头部,用于判断是否是一个class文件

class文件头被称为“魔数”,是个u4的数据,值为0xCAFEBABE(咖啡宝贝),呼应了java的logo图案。

Tips:来自于010Editor软件,可以很方便的查看class文件,并且会提示你每个位置二进制串的含义,如下:提示我括号里的4位是u4 magic

后面的四位分别是次版本号和主版本号,其中主版本号用的比较多,大家可以看书或上网查阅它与java版本的对应关系,如我是Java8,对应的大版本号就是十进制52,也就是0x34。高版本jdk能向下兼容老版本class文件,但会拒绝运行超过jdk版本的class文件

编译的时候可以通过javac参数自定义编译的版本,不过要符合版本的java特性,比如我写了个符合java6的代码,可以用jdk8来编译出java6的class文件

javac -source 1.6 -target 1.6 .\Test.java

查看生成的class文件,可以看到主版本号变成了0x32,对应着java6


常量池

魔数和版本号之后,也就是8位开始,就是常量池的入口了,通常是Class文件中最大的一部分。

存放内容

  1. 字面量

    • 文本字符串
    • final的常量值
  2. 符号引用
    • 类和接口的全限定名
    • 字段的名称和描述符(后面讲解名称和描述符有什么区别)
    • 方法的名称和描述符
    • 方法句柄和方法类型
    • 被模块导出或开放的包

开头存放u2类型的容量计数器,用于判断常量池的结束

值得一提的是它是从1开始计常量池的数据索引,0索引被用来表示不引用任何常量池数据,所以0x0F表示常量池有14条数据(1-14)

常量池中有十几种数据结构类型,这里就不给出所有的了,需要请查阅相关资料书籍

常量池中数据可以抽象成如下形式反复拼接

类型标志tag/u1 此种类型的特定结构/表

常量池中这十几种数据类型无缝拼接,所有的类型都是以tag来开头,通过它可以判断当前类型,进而得知类型的大小,进而读取全部的数据

手工分析实例

0x0A0003000C是第一个常量,因为tag是0x0A,对应的数据结构长度是5,从这之后就是下划线处第二个常量0x07000D,tag是0x07,数据结构如下

查表可知它表示的是Class_info

0x07是tag,0x000D是全限定名的索引,也就是常量池里的索引0x000D,十进制13,索引从1开始,我们可以数一数到第十三个常量,也就是下划线部分

tag是0x01

package org.b1ackc4t.jvm;
public class Test {
public static void main(String[] args) {
int a = 1;
}
}

对照可知这个常量的bytes部分确实是类的全限定名,一模一样。同理可以将常量表所有数据都推出来。

工具分析实例

幸运的是,我们不需要去每次手工的分析常量池数据,JDK已经内置了专门分析Class字节码的工具./bin/javap,正确配置了Java环境变量就可以直接使用了

javap -v Test.class

可以清晰的看到常量池的内容,比如刚刚分析的类全限定名,是第二个常量,指向第十三个常量的utf-8字符串


访问标志

紧随常量池之后的是访问标志

存放内容

各种各样的类修饰符,包括但不限于

  1. 是否是Public
  2. 是否是Final
  3. 是否是个接口
  4. 是否是Abstract
  5. 是否是个注解

类、父类、接口索引

紧随访问标志之后

存放内容

存储的是索引,指向常量池中的内容

  1. 自身全限定名
  2. 直接父类全限定名
  3. 所有接口全限定名集合

字段表集合

存放所有的字段,包括类级变量(static)、实例级变量。(一个字段表表示一个字段)

字段表结构内容

一个字段表结构包含以下内容:

  1. 访问修饰符
  2. 简单名称
  3. 描述符
  4. 其他额外属性(不像常量池的tag,额外属性没有tag,直接用属性名标识,属性名存在常量池中)
    • 比如会为final(包括非静态的final)的字段记录初始值

真实的详细结构请查阅相关资料,这里只描述大致存储了哪些内容

简单名称和描述符

相信如果初始简单名称和描述符,都会对这两个词迷惑,这两个词不仅描述字段也描述方法,意思似乎差不多,但其实大不一样,接下来开始详谈

  • 简单名称。就是平时给变量、方法定义的名字,嗯,就这么简单
  • 描述符。是一种特殊的约定俗成的结构字符串。用来表达字段的数据类型、方法的参数列表、返回值等等,具体如何书写这种描述字符串可查阅相关资料

其他额外属性用的较少,在此不过多介绍。只了解这些相信也能理解字节码是如何用这些简短的数据结构来记录各种复杂的字段了


方法表集合

存放所有的方法包括:

  1. 静态方法、普通方法
  2. 父类里被子类重写的方法
  3. <init>方法,由编译器自动根据构造方法添加的实例构造器方法
  4. <clinit>方法,如果有静态代码块或者给静态字段赋值操作,编译器自动根据这些代码生成<clinit>方法(类构造器)

方法表结构内容

方法表结构与字段表十分类似,仅仅在访问修饰和额外属性有区别

一个方法表结构包含以下内容:

  1. 访问修饰符(比字段表少几种修饰方法比如:volatile)
  2. 简单名称
  3. 描述符
  4. 其他额外属性(不像常量池的tag,额外属性没有tag,直接用属性名标识,属性名存在常量池中)
    • Code (方法代码,最重要的额外属性)

只要两个方法简单名称和描述符不完全相同,那么就可以在类中共存。Java方法重载就是简单名称相同,描述符不同。但是描述符还包括了返回值类型,所以字节码的描述其实是比Java范围更大的,理论上只有返回值不同的方法也能够进行重载,但这在Java上是不成立的,也印证了字节码并非只为Java而创造,未来可能有其他语言编译成字节码,能够用上字节码的其他特性,运行在JVM上

额外属性

这里只列举两个最常见的,详细信息请查阅相关资料

Code属性

作为方法表中最重要的额外属性,能够描述诸多信息,并非单纯存放代码

存放内容:

  1. 操作栈深度最大值

    • 编译期可知的,编译时由编译器计算出方法执行时,任意时刻都不会超过的最大深度
  2. 局部变量表长度(单位:变量槽Slot)
    • 编译期可知的,编译时由编译器计算出方法任意时刻所使用的局部变量(包括形参)最大长度,可能会根据变量生命周期进行相应的优化。
  3. 方法字节码内容(我们的主角!)
    • 《Java虚拟机规范》规定字节码长度不能超过65535(也就是一个方法不能超过65535个字节码,不然编译不通过),但长度实际上是用u4来存储的,有四个字节长,可能是为了未来发展设定的。
  4. 异常表(用于方法体中异常处理(try-catch)的代码跳转)

Exceptions属性

方法throws抛出的异常信息,比如下面的Exception

public static void main(String[] args) throws Exception {
System.out.println("hello world");
}

实例-分析一个简单程序的字节码

目标class文件的源码如下:

package org.b1ackc4t.jvm;

public class Test extends SuperClass {

    public static int staticVal = 1;
public static final byte finalStaticVal = 100;
public int val1 = 2;
public final int finalVal = 3;
public int val3;
public int[] val4;
public int val5; static {
System.out.println("static code block!");
} public Test() {
this.val3 = 10;
System.out.println("constructor 1");
} public Test(int arg) {
this.val3 = 20;
System.out.println("constructor 2");
} public boolean fun1(int a, float b) {
int c = 10;
int d = 20;
System.out.println("fun1");
return true;
} public static void main(String[] args) throws Exception {
try {
System.out.println("main!");
Test t1 = new Test();
System.out.println(t1.val5);
} catch (Exception e) {
e.printStackTrace();
}
}
}

我们还是用javap来分析

javap -v Test.class

版本号

主版本号52,对应java8版本

常量池

访问标志以及父类、接口

访问权限为public

直接父类是SuperTest

没有接口

字段表集合

分析一个比较特别的字段

方法表集合

这里只分析两个方法

惊讶的是结尾还记录了编译的文件名Test.java

"Test.java"也确实出现在了常量池中,这并不是javap加的,而是字节码中确实存在的。

总结

字节码(class文件)的描述能力真的比Java语言要更强一些,默认情况下可以把Java语言表述的所有内容全部装进简短的字节码中,这也是为何class文件被反编译后还原成java代码的还原度如此之高,甚至文件名、局部变量名都能够一一还原。但真实环境中,无论是为了运行效率还是代码安全性,我们都应当适当更改编译选项,不要将过多信息编译进字节码之中,以免隐患产生。

一文理解Java-class字节码文件的更多相关文章

  1. 玩命学JVM(一)—认识JVM和字节码文件

    本篇文章的思维导图 一.JVM的简单介绍 1.1 JVM是什么? JVM (java virtual machine),java虚拟机,是一个虚构出来的计算机,但是有自己完善的硬件结构:处理器.堆栈. ...

  2. 【java虚拟机系列】从java虚拟机字节码执行引擎的执行过程来彻底理解java的多态性

    我们知道面向对象语言的三大特点之一就是多态性,而java作为一种面向对象的语言,自然也满足多态性,我们也知道java中的多态包括重载与重写,我们也知道在C++中动态多态是通过虚函数来实现的,而虚函数是 ...

  3. OpenJDK源码研究笔记(八)-详细解析如何读取Java字节码文件(.class)

    在上一篇OpenJDK源码研究笔记(七)–Java字节码文件(.class)的结构中,我们大致了解了Java字节码文件的结构. 本篇详细地介绍了如何读取.class文件的大部分细节. 1.构造文件  ...

  4. OpenJDK源码研究笔记(七)–Java字节码文件(.class)的结构

    最近在看OpenJDK源码的过程中,顺便看了Java编译器(javac)的源码. 为了理解javac的源码,需要先搞懂Java字节码文件(.class)的结构. 于是,我就认真看了下OpenJDK中J ...

  5. Java Eclipse编译后产生的字节码文件,用DOS命令符怎么打开

    在很多初学者刚刚接触eclipse的时候,写完一个代码文件.例如 Demo.java 通过run as a java application生成之后,会产生一个Demo.class. Demo.cla ...

  6. java字节码文件 helloworld

    Java代码 \\A.java public class A{} 1 2 1 2 javac A.java \\得到 A.class javap -v A.class 下面是javap工具帮我们生成的 ...

  7. @使用javap反编译Java字节码文件

    在Sun公司提供的JDK中,就已经内置了Java字节码文件反编译工具javap.exe(位于JDK安装目录的bin文件夹下). 我们可以在dos窗口中使用javap来反汇编指定的Java字节码文件.在 ...

  8. 浅谈Java反射机制 之 获取类的字节码文件 Class.forName(&quot;全路径名&quot;) 、getClass()、class

    另一个篇:获取 类 的 方法 和 属性(包括构造函数) 先贴上Java反射机制的概念: AVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法: 对于任意一个对象,都能够调用它 ...

  9. Java跨平台原理(字节码文件,虚拟机)

    介绍 C/C++语言都直接编译成针对特定平台机器码.如果要跨平台,需要使用相应的编译器重新编译. Java源程序(.java)要先编译成与平台无关的字节码文件(.class),然后字节码文件再解释成机 ...

  10. java 命令查字节码文件, 查.class文件内容

    1. 需要用javac,javap命令,所以先配下环境变量 2.配置环境变量 单击“计算机-属性-高级系统设置”,单击“环境变量”.在“系统变量”栏下单击“新建”,创建新的系统环境变量. 3.写需要用 ...

随机推荐

  1. mysql innodb 性能优化

    建议参数: max_connections=800 key_buffer_size=512M query_cache_size=128M sort_buffer_size=64M table_open ...

  2. 转C#窗体无法接受Keydown事件

    问题一描述:当新建一个窗体时,添加KeyDown事件后,会正常处理,但是当添加有控件时,比如Button,TextBox,不会触发窗体的KeyDown事件,也没有调用KeyDown事件的处理程序. 原 ...

  3. [转载]ASP.NET中TextBox控件设立ReadOnly=&quot;true&quot;后台取不到值

    原文地址:http://www.cnblogs.com/yxyht/archive/2013/03/02/2939883.html ASP.NET中TextBox控件设置ReadOnly=" ...

  4. OpenCms 集成外部Solr Server

    OpenCms默认是以内嵌的Solr作为全文搜索服务的,不过我们也可以配置一个独立的Solr搜索服务器 设置外部Solr Server 1. 从Solr 官方站点http://lucene.apach ...

  5. Play on Words 欧拉通路(回路)判断

    Play on Words note:  判断一下连通性. #include <iostream> #include <cstdio> #include <cstring ...

  6. Json对象序列化与反序列化

    如果后台的参数数对象,需要在前台传入: JS代码: //创建JS对象 var CUTTING_TABLET_MO = new Object(); CUTTING_TABLET_MO.CUTTING_T ...

  7. windows下 安装 rabbitMQ 及操作常用命令(操作创建用户密码 角色等)

    rabbitMQ是一个在AMQP协议标准基础上完整的,可服用的企业消息系统.它遵循Mozilla Public License开源协议,采用 Erlang 实现的工业级的消息队列(MQ)服务器,Rab ...

  8. bzoj 1814 Ural 1519 Formula 1 插头DP

    1814: Ural 1519 Formula 1 Time Limit: 1 Sec  Memory Limit: 64 MBSubmit: 942  Solved: 356[Submit][Sta ...

  9. windows 多网卡路由设置

    1.问题 windows操作系统,双网卡连接内外网,配置路由使内网请求走内网网卡,外网请求走外网网卡 2.网络参数 内网网卡 IP:172.22.25.152 mask: 255.255.255.0 ...

  10. 做为一个.net码农,打开公司的一个项目,大叔我哭了

    先说下背景,楼主在上海,之前一直是做BS互联网开发的,今年进入这家公司,是做软件产品的小外企. 然后,啥也不说了,直接上图吧: 因为一个屏幕没有办法显示出来,所以我截了3张图,然后拼成一张,这还是我花 ...