文档章节

JVM 之 运行时数据区(更新)

麦壳原野
 麦壳原野
发布于 2014/12/19 09:50
字数 3065
阅读 3647
收藏 207
JVM

第一篇 JVM 之 Class文件结构

JVM定义了一系列程序运行期间使用的运行时数据区(run-time data area)。这些数据区域中的一些随着JVM的启动而创建直到JVM的停止而销毁,而另一些则随着某个线程的创建而创建,随着线程的销毁而销毁。

为了能更直观的了解JVM的运行时数据区,我们先上张图来瞅瞅整个JVM内存的逻辑布局:

以上仅是一个JVM运行时内存布局的概念模型,我们可以看出JVM主要定义5大类运行时数据区:

1)虚拟机栈,2)方法区,3)本地方法栈,4)堆,5)程序计数器。

当然除了这几个数据区还有1)运行时常量池,2)帧,3)本地变量表,4)操作数栈等数据区,下面我们都会一一分析。

对于上图个人觉得除了看到这几块区域,也没什么深入的细节上的信息了,而且很多情况下还会误导初学者,比如很多人认为虚拟机栈就那么一块区域,其实不然,而且虚拟机栈可以是不连续的。

因此作为一名程序员,个人一直认为代码是最好的注释和文档,一行代码胜过千言万语。因此为了更好的理解JVM的内存模型,我们下面用JAVA代码的形式来深入分析下。

零,JVM

像上一篇文章一样,我们还是从整体着手,然后到具体的细节逐个分析。下面就是一个可能的JVM内存的Java实现的类结构图:

下面我们逐个列出每个数据区域的类实现来(注:该实现只是一个用来帮助理解的模型,会忽略很多细节,并且可能有不正确的地方,欢迎讨论)

//JVM.java
public class JVM {
	private Heap heap;
	private MethodArea methodArea;
	private Map<String, NativeMethodStack> nativeMethodStacks;
	private Map<String, VMStack> vmStacks;//假设线程名为键
	private Map<String, PCRegister> pcRegisters;//假设线程名为键
	//....getter, setter
}

上述代码很简单,清晰明了,不多说了。接下来我们就逐个深入分析。


一,Heap(堆)

堆是虚拟机中线程共享的一块数据区域,也就说所有的线程都可以访问这块区域的数据。同时堆是虚拟机中用来对象和数组分配内存的地方。堆的生命周期跟虚拟机一样,在虚拟机启动时创建,在虚拟机关闭时销毁。另外虚拟机中的对象无需显示的进行内存回收,JVM垃圾回收器会自动回收那些‘不用的’对象和数组。为更好的实现来及回收机制,通常JVM的实现会将堆内存划分为新生代(New Generation)和老年代(Tenured Generation),而新生代中又分为Eden Area和Survivor Area。下面我们看下堆内存的结构:

//堆
public class Heap {
	
	private long xms;//min heap size
	private long xmx;//max heap size
	private NewGeneration newGenration;//新生代
	private TenuredGeneration tenuredGeneration;//老年代
}
//新生代
public class NewGeneration {

	private int survivorRatio;// = (eden size / survivor size)
	private long xmn;//new generation
	private EdenArea eden;
	private SurvivorArea fromSurvivor;
	private SurvivorArea toSurvivor;
}

//老年代
public class TenuredGeneration {

	private byte[] memory;
}

//Eden
public class EdenArea {

	private byte[] memory;
}

//Survivor
public class SurvivorArea {

	private byte[] memory;
}

相信看到代码后你会感觉更加直观了。


二,VM Stack(虚拟机栈)

JVM虚拟机栈是线程私有数据区域,也就是每个线程都有一个自己的虚拟机栈内存,该内存随着线程的创建而创建,随着线程的销毁而销毁。虚拟机栈用来存储栈帧(frame),而栈帧会在下文详解。虚拟机栈类似于传统语言(如C)中的栈。它主要用来完成方法的调用和返回。由于虚拟机栈除了push和pop栈帧没有其他操作,所以虚拟机栈的内存可以是不连续的。下面是虚拟机栈的Java代码结构

import java.util.Stack;
public class VMStack {

	private Thread owner;
	private long stackDeep;//最大栈容量
	private Stack<Frame> frames;
}

废话不多说,继续往下说,既然提到栈帧,我们就看看什么是栈帧。


三,Frame(栈帧)

栈帧用来存储方法执行期间的数据和部分结果,同时还会执行动态链接,返回方法返回值,以及分派异常等动作。

每当有方法被调用时,就会创建一个新的栈帧,并压入执行该方法的线程的虚拟机栈中。当方法执行结束后,该栈帧就会被弹出并销毁,无论该方法是正常结束还是异常退出。每个栈帧内部都有一个本地变量表和操作数栈,以及一个指向当前方法所属类的运行时常量池的引用。(本地变量表,操作数栈,运行时常量池将在下文分析)

本地变量表和操作数栈的大小在编译期就会被确定,并且其大小由与该栈帧关联的方法的代码决定,另外他们的内存可以在方法被调用时再分配。

对每个线程,任意时刻都只会有一个栈帧(当前执行方法的栈帧)处于活动状态。这个栈帧被称为当前栈帧(current frame),相关联的方法叫做当前方法(current method),当前方法所定义的类叫做当前类(current class)。

如果当前方法调用另一个方法,那么就会创建一个新的栈帧,并成为当前栈帧。当当前方法返回时,当前栈帧就会将返回值传递回前一帧,该栈帧销毁,前一帧成为当前栈帧。

注意:某个线程创建的栈帧是该线程私有的,其他线程无法访问到。至于详细的方法调用和执行的过程我们在后续文章会进行更为详细的分析。

import java.lang.reflect.Method;
public class Frame {

	private LocalVariable[] localVariableTable;//本地变量表
	private OperandStack operandStack;//操作数栈
	private RuntimeConstantPool constantpool;//当前方法所属类的运行时常量池的引用
	private VMStack ownerStack;//所属虚拟机数栈
}



四,LocalVariable (局部变量表)

上面已经提到,每个栈帧都会包含一个局部变量表(局部变量数组),用来存储方法参数,局部变量等数据。而且局部变量表的大小由所属栈帧的关联方法的代码决定,并在编译器就确定了。

一个局部变量可以保存一个boolean,byte,char,short,int,float,reference或returnAddress的值,一对局部变量可以保存一个long或double的值。局部变量表由下标索引,索引从0开始,最大值不超过变量表大小。

long和double的值占用两个相邻的局部变量,而且不许用两个局部变量中较小的那个下表来索引该long或double值。

虚拟机使用局部变量表来进行方法调用过程中的参数传递。在静态方法调用时,所有的参数会按照顺序保存到局部变量表中从第0个位置开始的连续的局部变量。而调用实例方法时,局部变量表的第0个位置始终保存调用该方法的对象的引用(this),然后从第1个位置开始保存方法的参数。

public class LocalVariable {

	private Type type;
	private Slot slot;
	
	public enum Type{
		_boolean,
		_byte,
		_char,
		_short,
		_int,
		_float,
		_reference,
		_returnAddress,
		_long,
		_double
	}
	public static class Slot{
	    private byte[] values;
	}
}



五,OperandStack(操作数栈)

第三部分已经提到,每个栈帧都包含一个后进先出的操作数栈,栈的深度在编译器便已确定,其大小由与该栈帧关联的方法体代码决定。

JVM提供了一些将常量或局部变量表中的变量加载到操作数栈中的指令,同样也提供了一些用来从操作数栈中获取数据,并操作他们,然后重新放回栈中的指令。操作数栈也会被用来准备传递给方法的参数以及接受方法的返回值。举个例子,iadd指令要求操作数栈顶预先有两个int值(其他指令压入),并将两个值弹出栈相加,让后将结果重新压回栈中。

操作数栈中的每个值都可以用来存储所有类型的数据,包括long,double。

操作数栈中的数据必须按照其类型进行适当的操作,比如我们不能将一个int值压入栈顶后按float类型弹出并操作。

public class OperandStack {

	private Slot[] values;
	
	public Slot pop(){return new Slot();}
	public void push(Slot slot){}
	
	public static class Slot{
		private byte[] v;
	}
}



六,Method Area(方法区)

同堆内存一样,方法区也是一个线程共享的数据区域。方法区有点类似传统编程语言(如C)中的用来存放编译代码的内存区域,或者类似于操作系统进程中的文本段。它主要保存着每个类的常量池,字段,方法,以及方法或构造器中的代码等数据(简单理解就是,每个类的Class文件加载,解析后就被存放在方法区中了)。

方法区的生命周期与虚拟机相同。尽管虚拟机中指出逻辑上方法区是堆内存的一部分,只是垃圾回收没有那么频繁,但是我们习惯上都会分开来讲。

更新:在HotSpot的实现中,方法区包含在了永久代中,同样在永久代中的还有一块区域我们可以称之为String literal pool(字符串常量池),该区域用于存放代码中的字符串字面量,以减少相同字符串对象创建带来的开销。最终内存布局参看下图。

public class PermGen{
       private MethodArea methodArea;
       private StringLiteralPool literalPool;
}

public class StringLiteralPool{
       private byte[] values;
}

public class MethodArea {

	private ClassInfo[] classes;
	
	public static class ClassInfo{
		private RuntimeConstantPool constantPool;
		private Field[] fields;
		private Method[] methods;
	}
}

这里我们没有用java.lang.Class是因为我们下面要讲到RuntimeConstantPool。其实方法区中存放的主要就是java.lang.Class实例集合。


七,Runtime Constant Pool(运行时常量池)

每个运行时常量池都是某个对应类或者接口的class文件中的常量池的运行时映射。一个运行时常量池就像是传统编程语言里面的符号表,不过它所包含的数据类型比符号表丰富。

所有的运行时常量池都分配在方法区中,某个类或者接口的运行时常量池会在该类或者接口被加载时创建。

public class RuntimeConstantPool {

	private ClassInfo clazz;
	private byte[] values;
}


八,PC Register(程序计数器)

或许这个该放在最前面分析的。Java的多线程机制离不开程序计数器,每个线程都有一个自己的程序计数器,以便完成不同线程上下文环境的切换。

任意时刻,如果当前方法不是native的,那么程序计数器都会保存当前被执行的指令的地址。如果当前方法是native的,那么程序计数器的值为undefined。程序计数器应该足够大以至于可以容纳returnAddress和特定平台的指针。

public class PCRegister {

	private Thread ownerThread;
	private byte[] values;
}


九,Native Method Stack (本地方法栈)

JVM的实现可以使用本地方法区来作为传统语言的栈来支持本地方法的调用(native方法)。本地方法栈同样可以用于其他语言(如C)写的虚拟机指令集的解释器实现。通常本地方法栈也是线程私有的数据区,生命周期同线程相同。

更新:引用http://blog.jamesdbloom.com/JVMInternals.html 文章中的图(很详细)


© 著作权归作者所有

麦壳原野
粉丝 312
博文 40
码字总数 76402
作品 0
程序员
私信 提问
加载中

评论(9)

foodon
foodon
用代码这种形式确实比纯文字清楚,把类列表那加个UML类图效果我看会更好。
唐家V
唐家V
读起来很清晰,读完并做了笔记加深理解
eechen
eechen
Linux虚拟地址空间布局
http://www.cnblogs.com/clover-toeic/p/3754433.html
用户进程部分分段存储内容如下所示(从内存高地址到低地址):
栈(stack): 函数参数、返回地址、局部变量等
堆(heap): 动态分配的内存
BSS段(bss): 未初始化或初值为0的全局变量和静态局部变量
数据段(data): 已初始化且初值非0的全局变量和静态局部变量
代码段(text): 可执行代码、字符串字面值、只读变量
fabself
fabself
点赞
静风流云
静风流云
谢谢分享!
麦壳原野
麦壳原野 博主

引用来自“石头哥哥”的评论

good jod

Thanks
石头哥哥
石头哥哥
good jod
麦壳原野
麦壳原野 博主

引用来自“hongframe”的评论

只是些概念而已。。。
So...?!
__啊
__啊
只是些概念而已。。。
JVM 运行时数据区简介及堆与栈的区别

理解JVM运行时的数据区是Java编程中的进阶部分。我们在开发中都遇到过一个很头疼的问题就是OutOfMemoryError(内存溢出错误),但是如果我们了解JVM的内部实现和其运行时的数据区的工作机制,...

大数据之路
2015/08/02
4K
1
用Java实现JVM第四章《运行时数据区》

案例介绍 本案例初步实现运行时数据区里;线程、Java虚拟机栈、帧、操作数栈、局部变量表。 在运行Java程序时,Java虚拟机需要使用内存来存放各种各样的数据。Java虚拟机规范把这些内存区域叫...

付政委
04/27
0
0
JVM(二)Java虚拟机组成详解

导读:详细而深入的总结,是对知识“豁然开朗”之后的“刻骨铭心”,想忘记都难。 Java虚拟机(Java Virtual Machine)下文简称jvm,上一篇我们对jvm有了大体的认识,进入本文之后我们将具体...

王磊的博客
01/14
182
0
JVM的方法区和永久带是什么关系?

群里面有小伙伴问到这个问题,说在网上看了很多文章,但是还是没弄明白这俩是啥关系,下面我们就来详细的解释一下: 什么是方法区? 方法区(Method Area)是jvm规范里面的运行时数据区的一个...

若鱼1919
2018/07/26
0
0
经典面试题|讲一讲JVM的组成

JVM(Java 虚拟机)算是面试必问的问题的了,而但凡问 JVM 一定会问的第一个问题就是:讲一讲 JVM 的组成?那本文就注重讲一下 JVM 的组成。 首先来说 JVM 的组成分为,整体组成部分和运行时...

程序猿院长
04/15
31
0

没有更多内容

加载失败,请刷新页面

加载更多

快速排序与冒泡排序

快速排序与冒泡排序 比较基础,特准备写博客记录和思考一下

T型人才追梦者
23分钟前
2
0
OSChina 周三乱弹 —— 调查人员问狗 那你在做什么啊?

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 小小编辑推荐:《Let It Be》- John Denver 《Let It Be》- John Denver 手机党少年们想听歌,请使劲儿戳(这里) @FalconChen :每天看一遍,...

小小编辑
今天
6
0
高效程序员的45个习惯总结版-文末脑图

1 做事 一个重大的错误应该被当做一次学习而不是指责他人的机会,团队成员一起工作,应该互相帮助,而不是互相指责 2 欲速则不达 不要为了修复问题而去修复,要投入时间和精力保持代码整洁 ...

阿提说说
今天
18
0
带南海九段线分位数地图可视化(R语言版)

今天带来一篇承诺虾神的可视化博客。内容是使用R语言进行带南海九段线分位数地图可视化。虾神的原博文地址如下(Python版)。 Python实现带南海九段线分位数地图完整可视化版本(附代码及数据...

胖胖雕
今天
12
0
Nginx 的进程结构,你明白吗?

Nginx 进程结构 这篇文章我们来看下 Nginx 的进程结构,Nginx 其实有两种进程结构: 单进程结构 多进程结构 单进程结构实际上不适用于生产环境,只适合我们做开发调试使用。因为在生产环境中...

武培轩
今天
20
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部