JVM研习之一:Java虚拟机的内存结构

注1:以下所提及线程,无特定说明的均默认指代“Java虚拟机线程”。

注2:注意避免混淆Stack、Heap和Java(VM) Stack、Java Heap的概念。Java虚拟机是在操作系统之上的更高层抽象,可以看作是一台虚拟的计算机。Java虚拟机的内存划分与操作系统的内存分区无法一一对应。Java虚拟机的实现本身是由其他语言编写的应用程序,在Java语言程序的角度上看分配在Java Stack中的数据,而在实现虚拟机的程序角度上看则可以是分配在Heap之中。

Java虚拟机的内存结构,区别于侧重于多线程的Java内存模型(Java Memory Model)。

我们当然要精确地定义每个区域的概念和用途,但在此之前(或之后),是不是应该思考一下:JVM的内存结构为什么要这样划分?

私以为主要是依据不同数据的更新频率、访问速度要求、垃圾收集管理来划分的。线程的工作内存区,要求速度快,也受限于高速读写设备资源的稀缺性,JVM据此划分了PC寄存器、Stack(栈),用来存储当前方法操作计数和局部变量(基本类型及引用)。对于更新频率较低的类结构信息、常量、静态方法等,划分了“方法区”。对于占用容量较大的对象实例,划分了“Java Heap(堆)”。为了区分Java方法和Native方法的处理,又将 Stack拆分为JVM Stack(Java虚拟机栈区)、Native Method Stack(本地方法栈)。
这就是JVM的五大内存区了。

1、PC寄存器,线程私有,小,快;
2、JVM Stack,线程私有,小,快;
3、Native Method Stack,线程私有,小,快;
4、Java Heap,共享区,大,容量动态分配,慢;
5、方法区,共享区,中;

名称中英对照

* 运行时数据区 - (Run-Time Data Areas)
    - PC寄存器 - (Program Counter Register)
    - Java虚拟机栈 - (JVM Stack)
      * 栈帧 - (Frame)
        * 局部变量表 - (Local Variables)
        * 操作数栈 - (Operand Stack)
        * 动态链接 - (Dynamic Linking)
    - Java堆 - (Java Heap)
    - 方法区  - (Method Area)
      * 运行时常量池  - (Runtime Constant Pool)
    - 本地方法栈  - (Native Method Stack)


各区的定义(JVM规范)

JVM定义了几种程序运行期间会使用到的运行时数据区,分别对应JVM或线程的生命周期。

PC寄存器:每一条线程都有自己的PC寄存器。正在被线程执行的current method,如其不是native的,PC寄存器就保存JVM正在执行的字节码指令的地址,如其是native的,则值为undefined。

JVM Stack:每一条线程都有自己私有的JVM Stack。其与线程同时创建,用于存储Frame。与传统语言(C)中的栈类似,JVM Stack用于存储局部变量与一些过程结果。它在方法调用和返回中也扮演了很重要的角色。因为除了Frame的出栈和入栈外,JVM Stack不会再受其他因素影响,所以Frame可以在堆中分配。

本地方法栈:JVM实现可能会使用到传统的栈(C Stack)来支持native方法的执行,这个栈就是本地方法栈。当JVM使用其他语言来实现指令集解释器时,也会使用本地方法栈。这个栈一般在线程创建时按线程分配。

Java Heap :在JVM中,Heap是可供各条线程共享的运行时内存区,也是供所有类实例和数据对象分配内存的区域。Heap在JVM启动的时候被创建,它存储了被ASMS/GC所管理的各种对象,这些对象无需也无法显式地被销毁。

方法区:在JVM中,方法区是可供各条线程共享的运行时内存区域。它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容。方法区在虚拟机启动的时候被创建。

运行时常量池:该池是每一个类或接口的常量池的运行时表示形式,它包括了若干种不同的常量:从编译期可知的数值字面值到必须运行期解析才能获得的方法或字段引用。运行时常量池扮演了类似传统语言中符号表(Symbol Table)的角色,不过它存储数据范围比通常意义上的符号表要更为广泛。每一个运行时常量池都分配在Java虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的的运行时常量池就被创建出来。

栈帧:Frame是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接、方法返回值和异常分派。Frame随方法调用而创建,随方法结束而销毁——无论是正常完成或是异常完成(抛出了在方法内无未被捕获的异常)都算作方法结束。Frame在存储空间分配在JVM Stack之中,每一个Frame都有自己的局部变量表、操作数栈和指向当前方法所属的类的运行时常量池的引用。

局部变量表和操作数栈的容量是在编译期确定,并通过方法的Code属性保存和提供给Frame使用。因此,Frame容量的大小仅仅取决于JVM的实现和方法调用时可被分配的内存。

在一条线程中,只有当前正在执行的那个方法的Frame是活动的,称为Current Frame(当前栈帧,简称CF),其对应的方法称为Current Method(简称CM),定义该方法的类就称作Current Class。对局部变量表和操作数栈的各种操作,通常都指的是对当前栈帧的对局部变量表和操作数栈进行的操作。

如果CM调用了其他方法,或者CM执行结束,那这个方法的Frame就不再是CF了。当一个新的方法被调用,一个新的Frame也会随之而创建,并且随着程序控制权移交到新的方法而成为新的CF。当方法返回之际,CF会传回方法的执行结果给前一个Frame,在方法返回之后,CF随之被丢弃,前一个Frame就重新成为CF了。

需要特别注意的是,Frame是线程本地私有的数据,不同线程的Frame不能被互相访问或引用。

局部变量表:每个Frame内部都包含一组称为“局部变量表”的变量列表。它的长度由编译期决定,并且存储于类和接口的二进制表示之中,即通过方法的Code属性保存及提供给Frame使用。本区保存8大基本类型以及Reference、returnAddress类型的数据,其中double和long类型的数据占两个变量位,其余的占一个。JVM使用局部变量表来完成方法调用时的参数传递,当一个方法被调用的时候,它的参数将传递至从0开始的连续的局部变量表位置上。特别地,当一个实例方法被调用的时候,第0个局部变量一定是用来存储被调用的实例方法所在的对象的引用(即this关键字)。后续的其他参数将会传递至从1开始的连续的局部变量表位置上。

操作数栈:每个Frame内部都包含一个称为“操作数栈”的后进先出栈(LIFO)。它的长度由编译期决定,并且存储于类和接口的二进制表示中,即通过方法的Code属性保存及提供给Frame使用。在上下文明确,不会产生误解的前提下,我们经常把“当前栈帧的操作数栈”直接简称为“操作数栈”。

操作数栈所属的Frame在创建初时,操作数栈是空的。JVM提供一些字节码指令来从局部变量表或对象实例的字段中复制常量或变量值到操作数栈中,也提供了一些指令用于从操作数栈中取走数据、操作数据和把操作结果重新入栈。在方法调用时,操作数栈也用来准备调用方法的参数以及接收方法返回结果。(过程细节见$2.6.2)

动态链接:每个Frame内部都包含一个指向运行时常量池的引用来支持当前方法的代码实现动态链接。

各区的深入理解

1.PC寄存器

PC寄存器(Program Counter Register,程序计数器),是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于JVM的多线程是通过轮流切换分配CPU执行时间的方式来实现的,在某个特定时刻,一个CPU/内核只会执行一条线程的指令。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个私有、独立的PC寄存器,各线程间互不影响,我们称这类内存区域为“线程私有”的内存。

如果线程当前执行的是一个Java方法,PC寄存器记录的是正在执行的虚拟机字节码指令的地址;

如果线程正在执行的是native方法,它的值则为空(undefined)。

2.JVM Stack

和PC寄存器一样,JVM Stack也是线程私有的。它的生命周期与线程相同。JVM Stack描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个Frame用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用到执行完成的过程,就对应着一个Frame在JVM Stack中入栈到出栈的过程。

经常有人把Java内存分为堆内存(Heap)和栈内存(Stack),这种分法比较粗糙,Java内存区域的划分远比这复杂。其中所指“栈”就是现在讲的JVM Stack,或者说是JVM Stack中局部变量表部分(直接越过了Stack和Frame)。

局部变量表,存放了编译期可知的8大基本数据类型、对象引用(reference类型,并不是对象本身,可能只是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。局部变量表所需的空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

当线程请求分配的栈容量超过JVM允许的最大容量时,将抛出StackOverflowError;

如果JVM可动态扩展,并且扩展的动作已经尝试过,但是目前无法申请到足够的内存去完成扩展,或在建立新的线程时没有足够的内存去创建对应的JVM Stack,将抛出OutOfMemoryError。

StackOverflowError 表示 请求 > Stack.Max;

OutOfMemoryError 表示 请求 > 可分配内存;

3.本地方法栈

本地方法栈与JVM Stack所发挥的作用是非常相似的,区别只是JVM Stack为虚拟机执行Java方法服务,而本地方法栈是为虚拟机使用到的Native方法服务。

在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构没有强制规定,由实现自由选择。甚至有的虚拟机(如HotSpot)直接将本地方法栈和JVM Stack合二为一。与JVM Stack一样,也会抛出StackOverflowError和OutOfMemoryError异常。

4.Java Heap

对大多数应用来说,Java堆(Java Heap)是JVM所管理的内存中最大的一块,它是被所有线程共享的一块内存区域,在虚拟机启动时创建。该区的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java Heap是GC管理的主要区域。由于现在收集器基本都采用了分代收集算法,所以Java Heap还细分为:新生代和老年代;再细一点的 Eden空间、From Survivor空间、To Survivor空间等。不过无论怎么划分,都与存放内容无关,无论哪个区域存储的都是对象实例。

在实现时,既可以是固定大小的,也可以是扩展的,不过主流的虚拟机都是按照可扩展来实现的(-Xmx和-Xms)。如果堆 中没有内存完成实例分配,并且堆无法再扩展时,就会抛出OutOfMemoryError。

5.方法区

与Java Heap一样,方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然JVM规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫Non-Heap(非堆),目的应该是与Java Heap分区来的。

Java虚拟机规范对方法区的限制非常宽松,可以选择不实现垃圾收集。垃圾收集行为在本区是比较少出现的,但非数据进入方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回收“成绩”很难令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。

运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

Java虚拟机对Class文件每一部分(自然也包括常量池)的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行,但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的需要来实现这个内存区域。

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时抛出OutOfMemoryError异常。

各区容量的规范要求

PC寄存器:容量至少应当能保存一个returnAddress类型的数据或一个与平台相关的本地指针的值。(不可调节)
JVM Stack:可调节初始容量,对于可动态扩展和收缩的,可调节最大、最小容量。
Java Heap:可调节初始容量,对于可动态扩展和收缩的,可调节最大、最小容量。
方法区:可调节初始容量,对于可动态扩展和收缩的,可调节最大、最小容量。
本地方法栈:可调节初始容量,对于可动态扩展和收缩的,可调节最大、最小容量。

参考

1、《Java虚拟机规范SE7》第2章 Java虚拟机结构
2、《深入理解Java虚拟机》第2章 Java内存区域

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271