文档章节

JVM与字节码——类的方法区模型

随风溜达的向日葵
 随风溜达的向日葵
发布于 2017/11/28 10:42
字数 3270
阅读 2766
收藏 118
点赞 9
评论 11

从一个类开始

我们从一个简单类开始说起:

package example.classLifecicle;
public class SimpleClass {
	public static void main(String[] args) {
		SimpleClass ins = new SimpleClass();
	}
}

这是一段平凡得不能再平凡的Java代码,稍微有点编程语言入门知识的人都能理解它表达的意思:

  1. 创建一个名为SimpleClass的类;
  2. 定义一个入口main方法;
  3. 在main方法中创建一个SimpleClass类实例;
  4. 退出。

什么是Java bytecode

那么这一段代码是怎么在机器(JVM)里运行的呢?在向下介绍之前先说清几个概念。

首先,Java语言和JVM完全可以看成2个完全不相干的体系。虽然JVM全称叫Java Virtual Machine,最开始也是为了能够实现Java的设计思想而制定开发的。但是时至今日他完全独立于Java语言成为一套生命力更为强悍的体系工具。他有整套规范,根据这个规范它有上百个应用实现,其中包括我们最熟悉的hotspot、jrockit等。还有一些知名的变种版本——harmony和android dalvik,严格意义上变种版本并不能叫java虚拟机,因为其并未按照jvm规范开发,但是从设计思想、API上看又有大量的相似之处。

其次,JVM并不能理解Java语言,他所理解的是称之为Java bytecode的"语言"。Java bytecode从形式上来说是面向过程的,目前包含130多个指令,他更像可以直接用于CPU计算的一组指令集。所以无论什么语言,最后只要按照规范编译成java bytecode(以下简称为"字节码")都可以在JVM上运行。这也是scala、groovy、kotlin等各具特色的语言虽然在语法规则上不一致,但是最终都可以在JVM上平稳运行的原因。

Java bytecode的规范和存储形式

前面代码保存成 .java 文件然后用下面的命令编译过后就可以生成.class字节码了:

$ javac SimpleClass.java #SimpleClass.class

字节码是直接使用2进制的方式存储的,每一段数据都定义了具体的作用。下面是SimpleClass.class 的16进制数据(使用vim + xxd打开):

一个 .class 文件的字节码分为10个部分:

0~4字节:文件头,用于表示这是一个Java bytecode文件,值固定为0xCAFEBABE。

2+2字节:编译器的版本信息。

2+n字节:常量池信息。

2字节:入口权限标记。

2字节:类符号名称。

2字节:父类符号名称。

2+n字节:接口。

2+n字节:域(成员变量)。

2+n字节:方法。

2+n字节:属性。

每个部分的前2个字节都是该部分的标识位。

本篇的目的是说明字节码的作用以及JVM如何使用字节码运转的,想要详细了解2进制意义的请看这里:http://www.jianshu.com/p/252f381a6bc4。

反汇编及字节码解析

我们可以使用 javap 命令将字节码反汇编成我们容易阅读的格式化了的指令集编码:

$ javap -p SimpleClass.class #查看类和成员
$ javap -s SimpleClass.class #查看方法签名
$ javap -c SimpleClass.class #反汇编字节码
$ javap -v SimpleClass.class #返汇编查看所有信息

javap 还有很多的参数,可以使用 javap --help 来了解。下面是使用javap -v 命令输出的内容,输出了常量池信息、方法签名、方法描述、堆栈数量、本地内存等信息:

public class example.classLifecicle.SimpleClass
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V
   #2 = Class              #14            // example/classLifecicle/SimpleClass
   #3 = Methodref          #2.#13         // example/classLifecicle/SimpleClass."<init>":()V
   #4 = Class              #15            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               SourceFile
  #12 = Utf8               SimpleClass.java
  #13 = NameAndType        #5:#6          // "<init>":()V
  #14 = Utf8               example/classLifecicle/SimpleClass
  #15 = Utf8               java/lang/Object
{
  public example.classLifecicle.SimpleClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class example/classLifecicle/SimpleClass
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
}

下面是关于字节码格式的描述:

public class example.classLifecicle.SimpleClass

这一段表示这个类的符号。

flags: ACC_PUBLIC, ACC_SUPER

该类的标记。例如是否是public类等等,实际上就是将一些Java关键字转译成对应的Java bytecode。

Constant pool:

constant pool: 之后的内容一直到 { 符号,都是我们所说的"常量池"。在对java类进行编译之后就会产生这个常量池。通常我们所说的类加载,就是加载器将字节码描述的常量信息转换成实际存储在运行时常量池中的一些内存数据(当然每个方法中的指令集也会随之加载到方法指向的某个内存空间中)。

"#1"可以理解为常量的ID。可以把常量池看作一个Table,每一个ID都指向一个常量,而在使用时都直接用"#1"这样的ID来引用常量。

常量池中的包含了运行这个类中方法所有需要用到的所有常量信息,Methodref、Class、Utf8、NameAndType等表示常量的类型,后面跟随的参数表示这个常量的引用位置或者数值。

{}:

常量池之后的{}之间是方法。每一个方法分为符号(名称)、标记、描述以及指令集。descriptor:描述。flags:入口权限标记。Code:指令集。

Code中,stack表示这一段指令集堆栈的最大深度, locals表示本地存储的最大个数, args_size表述传入参数的个数。

字节码如何驱动机器运行

在往下说之前,先说下JVM方法区的内容。方法区顾名思义就是存储各种方法的地方。但是从实际应用来看,以Hotspot为例——方法区在实现时通常分为class常量池、运行常量池。在大部分书籍中,运行时常量池被描述为包括类、方法的所有描述信息以及常量数据(详情请看这里的介绍)。

对于机器来说并不存在什么类的感念的。到了硬件层面,他所能了解的内容就是:1)我要计算什么(cpu),2)我要存储什么(缓存、主存、磁盘等,我们统称内存)?

按照分层模型来说JVM只是一个应用进程,是不可能直接和机器打交道的(这话也不是绝对的,有些虚拟机还真直接当作操作系统在特有硬件设备上用)。在JVM到硬件之间还隔着一层操作系统,在本地运行时是直接调用操作系统接口的(windows和linux都是C/C++)。不过为了JVM虚拟机更高效,字节码设计为更接近机器逻辑行为的方式来运行。不然也没必要弄一个字节码来转译Java语言,像nodejs用的V8引擎那样实时编译Javascript不是更直接?这也是过去C/C++唾弃Java效率低下,到了如今Java反而去吐槽其他解释型编译环境跑得慢的原因(不过这也不见得100%正确。比如某些情况下Java在JVM上处理JSON不见得比JavaScript在nodejs上快,而且写起代码来也挺费劲的)。

我们回到硬件计算和存储的问题。CPU的计算过程实质上就是操作系统的线程不断给CPU传递指令集。线程就像传送带一样,把一系列指令排好队然后一个一个交给CPU去处理。每一个指令告诉CPU干一件事,而干事的前后总得有个依据(输入)和结果(输出),这就是各种缓存、内存、磁盘的作用——提供依据、保存结果。JVM线程和操作系统线程是映射关系(mapping),而JVM的堆(heap)和非堆(Non-heap)就是一个内存管理的模型。所以我们跳出分层的概念,将字节码理解为直接在驱动cpu和内存运行的汇编码更容易理解。

最后,我们回到方法区(Method Area)这个规范概念。CPU只关心一堆指令,而JVM中所有的指令都是放置在方法区中的。JVM的首要任务是把这些指令有序的组织起来,按照编程好的逻辑将指令一个一个交给CPU去运行。而CPU都是靠线程来组织指令运算的,所以JVM中每个线程都有一个线程栈,通过他将指令组织起来一个一个的交给CPU去运算——这就是计数器(Counter Register,用以指示当前应该执行什么字节码指令)、线程栈(Stacks,线程的运算模型——先进后出) 和 栈帧(Stacks Frame,方法执行的本地变量) 的概念。所以无论多复杂的设计,方法区可以简单的理解为:有序的将指令集组织起来,并在使用的时候可以通过某些方法找到对应的指令集合

解析常量池

先看 SimpleClass 字节码中常量池中的一些数据,上图中每一个方框表示一个常量。方框中第一行的 #1 表示当前常量的ID,第二行 Methodref 表示这个这个常量的类型,第三行 #4,#13 表示常量的值。

我们从 #1 开始跟着每个常量的值向下延伸可以展开一根以 Utf8 类型作为叶节点的树,每一个叶节点都是一个值。所有的方法我们都可以通过树的方式展开得到下面的查询字段:

class = java/lang/Object //属于哪个类
method = "<init>" //方法名称
params = NaN //参数
return = V //返回类型

所有的方法都会以 package.class.name:(params)return 的形式存储在方法区中,通过上面的参数很快可以定位到方法,例如  java.lang.Object."<init>":()V,这里"<init>"是构造方法专用的名称。

解析方法中的指令集

方法除了用于定位的标识符外就是指令集,下面解析main方法的指令集:

0: new           #2                  // class example/classLifecicle/SimpleClass
3: dup
4: invokespecial #3                  // Method "<init>":()V
7: astore_1
8: return

1))new 表示新建一个ID为#2的对象即SimpleClass(#2->#15="example/classLifecicle/SimpleClass")。此时JVM会在堆上创建一个能放置SimpleClass类的空间并将引用地址返回写到栈顶。这里仅仅完成在堆中分配空间,没执行初始化。

2)dup表示复制栈顶数据。此时栈中有2个指向同一内存区域的SimpleClass引用。

3)invokespecial #3表示执行#3的方法。通过解析常量池#3就是SimpleClass的构造方法。此后会将SimpleClass构造方法中的指令压入栈中执行。

4)接下来来是SimpleClass的构造方法部分: a)aload_0 表示将本地内存的第一个数据压入栈顶,本地内存的第一个数据就是this。b)invokespecial #1 表示执行 Object 的构造方法。c)退出方法。这样就完成了实例的构造过程。

5)完成上述步骤后,线程栈上还剩下一个指向SimpleClass实例的引用,astore_1 表示将引用存入本地缓存第二个位置。

6)return -> 退出 main 方法。

方法区结构

那么在方法区中所有的类是如何组织存放的呢?

我们用一个关系型数据库常的结构就可以解释他。在数据库中我们常用的对象有3个——表、字段、数据。每一个类对应的字节码我们都可以看成会生成2张数据库表——常量池表、方法表。通过字节码的解析,在内存中产生了如下结构的表:

常量池表:example.classLifecicle.SimpleClass_Constant

id type value
#1 Methodref #4,#13
…… ……
#4 Class #15
#15 Utf8 java/lang/Object

方法表:example.classLifecicle.SimpleClass_Method

name params return flag code
<init>     NaN V static,public ……
…  …… …… …… ……

然后在运行过程中当计数器遇到 invokespecial #3 这样的指令时就会根据指令后面的ID去本类的常量表中查询并组装数据。当组装出 class = java/lang/Object、method = "<init>"、params = NaN、return = V这样的数据后,就会去名为java.lang.Object的表中根据 method、params、return 字段的数据查询对应的code,找到后为该code创建一个本地内存,随后线程计数器逐个执行code中的指令。

这里仅仅用关系型数据库表的概念来解释方法区中如何将指令执行和字节码对应起来,真正的JVM运行方式比这复杂得多。不过这样很容易理解方法区到底是怎么一回事。

© 著作权归作者所有

共有 人打赏支持
随风溜达的向日葵
粉丝 165
博文 50
码字总数 114909
作品 0
广州
其他
加载中

评论(11)

随风溜达的向日葵
随风溜达的向日葵

引用来自“zhouinfo”的评论

new 表示新建一个ID为#2的对象即SimpleClass(#2->#15="example/classLifecicle/SimpleClass") 应该是 #2->#14
谢谢指正。
开源X
开源X
棒棒的
zhouinfo
zhouinfo
new 表示新建一个ID为#2的对象即SimpleClass(#2->#15="example/classLifecicle/SimpleClass") 应该是 #2->#14
蓝天bluesky
蓝天bluesky

引用来自“蓝天bluesky”的评论

用java语言写了简单的jvm,大家可以看下:relaxed:
https://gitee.com/skyz/vm-j

引用来自“紫电清霜”的评论

为何辣么叼
模仿别人的的,可以看下《自己动手写java虚拟机》这本书
caomini89
caomini89
凤飞飞
紫电清霜
紫电清霜

引用来自“蓝天bluesky”的评论

用java语言写了简单的jvm,大家可以看下:relaxed:
https://gitee.com/skyz/vm-j
为何辣么叼
cc_z
cc_z
不错:+1:
山雨欲来
山雨欲来
:+1:竟然可以这么深入
山雨欲来
山雨欲来

引用来自“蓝天bluesky”的评论

用java语言写了简单的jvm,大家可以看下:relaxed:
https://gitee.com/skyz/vm-j
你想做一个java版的 pypy?
蓝天bluesky
蓝天bluesky
用java语言写了简单的jvm,大家可以看下:relaxed:
https://gitee.com/skyz/vm-j
Java 面试知识点解析(三)——JVM篇

前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大部...

我没有三颗心脏 ⋅ 05/16 ⋅ 0

深入理解java虚拟机阅读笔记(一)————java内存区域

第二章:Java内存区域与内存溢出 2.2 运行时数据区域 2.2.1 程序计数器: (1)、一块较小的内存空间 (2)、可看做当前线程执行的字节码的行号指示器 (3)、字节码解释器工作时通过改变这个...

qq_37468185 ⋅ 05/10 ⋅ 0

作为一个java程序员这些技能你都知道吗?

一、Java特点 1、 面向对象 尽管受到其前辈的影响,但Java没被设计成兼容其他语言源代码的程序。这允许Java开发组自由地从零开始。这样做的一个结果是,Java语言可以更直接、更易用、更实际的...

java高级架构牛人 ⋅ 05/23 ⋅ 0

JVM学习之——Java内存区域

为了加深对Java语言的理解,加深对Java虚拟机工作机制、底层特性的了解和掌握,准备在闲暇时间,抽空对《深入理解Java虚拟机 JVM高级特性与最佳实践》一书进行学习。本文是学习此书第2章时的...

你想要怎样的未来 ⋅ 05/27 ⋅ 0

如何计算Java对象所占内存的大小

摘要 本文以如何计算Java对象占用内存大小为切入点,在讨论计算Java对象占用堆内存大小的方法的基础上,详细讨论了Java对象头格式并结合JDK源码对对象头中的协议字段做了介绍,涉及内存模型、...

阿里云云栖社区 ⋅ 05/24 ⋅ 0

JAVA虚拟机 JVM 详细分析 原理和优化(个人经验+网络搜集整理学习)

JVM是java实现跨平台的主要依赖就不具体解释它是什么了 ,简单说就是把java的代码转化为操作系统能识别的命令去执行,下面直接讲一下它的组成 1.ClassLoader(类加载器) 加载Class 文件到内...

小海bug ⋅ 06/14 ⋅ 0

JVM学习总结(一)运行时数据区

《深入Java虚拟机》这本书买了有一段时间了,当时看的时候就只是看,并没有边看边总结啥的,最后发现到脑子里面的根本所剩无几了。现在开始要好好归纳总结地再学习一遍。 运行时数据区域 JV...

hensemlee ⋅ 04/22 ⋅ 0

热修复与插件化基础——Java与Android虚拟机

一、Java虚拟机(JVM) 1、JVM整体结构 使用javac将java文件编译成class文件。 类加载器(ClassLoader)将class字节码加载进JVM对应的内存中。 JVM将内存分配给方法区、堆区、栈区、本地方式...

CSDN_LQR ⋅ 05/13 ⋅ 0

有一到五年开发经验的JAVA程序员需要掌握的知识与技能!

JAVA是一种平台,也是一种程序设计语言,如何学好程序设计不仅仅适用于JAVA,对C++等其他程序设计语言也一样管用。有编程高手认为,JAVA也好C也好没什么分别,拿来就用。为什么他们能达到如此...

java高级架构牛人 ⋅ 06/02 ⋅ 0

HotSwap和JRebel原理

HotSwap和JRebel原理 HotSwap和Instrumentation 在2002年的时候,Sun在Java 1.4的JVM中引入了一种新的被称作的实验性技术,这一技术被合成到了内部,其允许调试者使用同一个类标识来更新类的...

千里明月 ⋅ 05/09 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

聊聊spring cloud的RequestRateLimiterGatewayFilter

序 本文主要研究一下spring cloud的RequestRateLimiterGatewayFilter GatewayAutoConfiguration @Configuration@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMi......

go4it ⋅ 38分钟前 ⋅ 0

Spring JavaConfig 注解

JavaConfig注解允许开发者将Bean的定义和配置放在Java类中。它是除使用XML文件定义和配置Bean外的另一种方案。 配置: 如一个Bean如果在XML文件可以这样配置: <bean id="helloBean" class="...

霍淇滨 ⋅ 46分钟前 ⋅ 0

Spring clound 组件

Spring Cloud技术应用从场景上可以分为两大类:润物无声类和独挑大梁类。 润物无声,融合在每个微服务中、依赖其它组件并为其提供服务。 Ribbon,客户端负载均衡,特性有区域亲和、重试机制。...

英雄有梦没死就别停 ⋅ 47分钟前 ⋅ 0

Confluence 6 重新获得站点备份文件

Confluence 将会创建备份,同时压缩 XML 文件后存储熬你的 <home-directory>/backups> 目录中。你需要自己访问你安装的 Confluence 服务器,并且从服务器上获得这个文件。 运行从 Confluence...

honeymose ⋅ 52分钟前 ⋅ 0

informix的常用SQL语句

1、创建数据库 eg1. 创建不记录日志的库testdb,参考语句如下: CREATE DATABASE testdb; eg2. 创建带缓冲式的记录日志的数据库testdb(SQL语句不一定在事务之中,拥有者名字不被用于对象的解...

wangxuwei ⋅ 今天 ⋅ 0

matplotlib画图

最简单的入门是从类 MATLAB API 开始,它被设计成兼容 MATLAB 绘图函数。 from pylab import *from numpy import *x = linspace(0, 5, 10)y = x ** 2figure()plot(x, y, 'r')...

Dr_hu ⋅ 今天 ⋅ 0

RabbitMQ学习以及与Spring的集成(三)

本文介绍RabbitMQ与Spring的简单集成以及消息的发送和接收。 在RabbitMQ的Spring配置文件中,首先需要增加命名空间。 xmlns:rabbit="http://www.springframework.org/schema/rabbit" 其次是模...

onedotdot ⋅ 今天 ⋅ 0

JAVA实现仿微信红包分配规则

最近过年发红包拜年成为一种新的潮流,作为程序猿对算法的好奇远远要大于对红包的好奇,这里介绍一种自己想到的一种随机红包分配策略,还请大家多多指教。 算法介绍 一、红包金额限制 对于微...

小致dad ⋅ 今天 ⋅ 0

Python 数电表格格式化 xlutils xlwt xlrd的使用

需要安装 xlutils xlwt xlrd 格式化前 格式化后 代码 先copy读取的表格,然后按照一定的规则修改,将昵称中的学号提取出来替换昵称即可 from xlrd import open_workbookfrom xlutils.copy ...

阿豪boy ⋅ 今天 ⋅ 0

面试题:使用rand5()生成rand7()

前言 读研究生这3 年,思维与本科相比变化挺大的,这几年除了看论文、设计方案,更重要的是学会注重先思考、再实现,感觉更加成熟吧,不再像个小P孩,人年轻时总会心高气傲。有1 道面试题:给...

初雪之音 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部