深入解析Java的运行时数据区

上传人:Baige****0346 文档编号:266122470 上传时间:2022-03-14 格式:DOCX 页数:17 大小:2.98MB
返回 下载 相关 举报
深入解析Java的运行时数据区_第1页
第1页 / 共17页
深入解析Java的运行时数据区_第2页
第2页 / 共17页
深入解析Java的运行时数据区_第3页
第3页 / 共17页
深入解析Java的运行时数据区_第4页
第4页 / 共17页
深入解析Java的运行时数据区_第5页
第5页 / 共17页
点击查看更多>>
资源描述

《深入解析Java的运行时数据区》由会员分享,可在线阅读,更多相关《深入解析Java的运行时数据区(17页珍藏版)》请在金锄头文库上搜索。

1、深入解析Java的运行时数据区 【摘要】 Java程序运行的过程中,JVM会将其所管理的内存划分成若干个区域,统称为是运行时数据区。其中,一些线程间共享的区域,随着JVM的启动而创建,JVM的退出而销毁;另一些线程私有的区域,则随着线程的开始而创建,线程的结束而销毁。如图所示,运行时数据区由以下几个区域所组成:程序计数器、Java虚拟机栈、本地方法栈、方法区、堆。Java程序运行的过程中,JVM会将其所管理的内存划分成若干个区域,统称为是运行时数据区。其中,一些线程间共享的区域,随着JVM的启动而创建,JVM的退出而销毁;另一些线程私有的区域,则随着线程的开始而创建,线程的结束而销毁。如图所示

2、,运行时数据区由以下几个区域所组成:程序计数器、Java虚拟机栈、本地方法栈、方法区、堆。运行时数据区根据JVM的运行模型,程序运行前,JVM会将程序编译后的字节码加载到内存中;程序运行时,字节码解析器会读取内存中的字节码,按照顺序将字节码的指令解析成固定的操作。在这过程中,程序计数器(Program Counter Register)保存当前线程正在执行的字节码地址。从字节码运行的原理来看,单线程模型下的程序计数器貌似可有可无,字节码解析器会按照顺序将字节码翻译成固定操作,即使遇到分支跳转,也无碍程序正确运行下去。然而,现实中的程序往往是通过多线程协作来完成一个任务的,CPU会为每个线程分配

3、一定的时间片,一个线程在其时间片耗尽之后会挂起,直到它再次获得时间片后才会重新运行。为了确保程序正确运行,线程必须从挂起的地方重新执行。有了程序计数器,就可以保证在涉及线程上下文切换的情景下,程序依然能够正确无误地运行下去。因此,程序计数器是线程私有的,避免了线程之间的相互影响。JVM会为每个线程都分配一块非常小的内存空间用作程序计数器,这也是唯一一个Java虚拟机规范没有规定OutOfMemoryError的运行时区域。JVM会给每个线程都分配一个私有的内存空间,称为Java虚拟机栈(Java VM Stack)。Java虚拟机栈随着线程的创建而创建,它与传统语言(如C语言)的栈有着类似的作

4、用,JVM只会对其执行两种操作:栈帧(Stack Frame)的入栈和出栈。也就是说,Java虚拟机栈是存储栈帧的后进先出队列(LIFO)。每个方法的执行过程,都会伴随着栈帧的创建、入栈和出栈。栈帧是用来存储局部数据和部分过程结果的数据结构,主要包含局部变量表(Local Variable Table)、操作数栈(Operand Stack)、指向当前方法所属类的运行时常量池的引用(Runtime Constant Pool Reference),如图所示为Java虚拟机栈的模型。Java 虚拟栈模型局部变量表(LVT)局部变量表(LVT) 是一个索引以0开始的字节数组,存储了一个方法的所有入

5、参和局部变量。LVT所存储的类型都是编译期可知的,包括各基础类型(byte、char、short、int、long、float、double、boolean)、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。LVT有如下几个特点:1、第0个Slot(槽位)固定存储指向方法所属对象的this指针。2、除了long和double占用了连续2个Slot之外,其他类型都只占用了1个Slot。3、LVT按照变量的声明顺序进行存储。考虑以下代码来验证LVT的这些特点:先用javac命令将其编译成class文件:javac-gJvmStackLvt.java#

6、javac编译.java文件,输出.class文件再使用javap命令解析class文件,就可以看到showLvt函数的LVT。从javap的输出结果可以看出,LVT的第0个Slot的名字为this,签名为Lcom/yrunz/jdk/chapter1/JvmStackLvt表示是指向JvmStackLvt类型的this指针,验证了特点1;变量l和d所在Slot的索引与其相邻的变量所在Slot 的索引相差2,表示l和d占用了2个Slot,而其他的变量都只占用了1个Slot,验证了特点2;LVT中变量的存储顺序也是与其声明顺序相同,验证了特点3。操作数栈(OS)操作数栈(OS) 用于在方法运算过

7、程存储其中间的运算结果、方法入参和返回结果,它是一个后进先出(Last-In-First-Out,LIFO)的队列。JVM提供了对OS出栈和入栈的指令,如load指令属于入栈指令、store指令属于出栈指令。考虑以下代码:使用javac命令将其编译成class文件后,用javap解析得到:因为javap的输出结果中并不涉及操作数栈的内容,我们可以根据指令码和LVT来推断出OS的入栈和出栈过程,如图所示。运行时常量池引用(Runtime Constant Pool Reference)每个栈帧内都包含一个指向当前方法所属类的运行时常量池引用,也称为符号引用(Symbolic Reference)

8、,用于在类加载阶段对代码进行动态链接。动态链接所做的就是根据符号引用所表示名字,转换成对方法或变量的实际引用,从而实现运行时绑定(Late Binding)。考虑以下代码:使用javac命令将其编译成class文件后,用javap解析得到:可以看出,f2()的指令码中,有两处使用了符号引用。getfield指令引用的符号为#2,最终解析成com/yrunz/jdk/chapter1/JvmStackCpr.a:I;invokevirtual指令引用的符号为#3,最终解析成com/yrunz/jdk/chapter1/JvmStackCpr.f1:()V。Java虚拟机规范规定,Java虚拟机栈

9、可以被实现成固定大小,也可以实现成可动态地扩展和收缩。JVM通常会提供设定Java虚拟机栈容量大小或范围的参数,比如-Xss参数用于设定栈的大小。当线程请求分配的栈空间超过JVM指定的最大容量时,程序就会抛出StackOverflowError异常;而对于栈空间可动态扩展的情形,当尝试扩展却无法分配到足够内存时,程序就会抛出OutOfMemoryError异常。本地方法栈(Native Method Stack) 的作用与Java虚拟机栈类似,区别在于后者是为Java方法服务,而本地方法栈则为native方法服务。Java虚拟机规范没有对native方法机制及其实现语言做强制规定,如果JVM不

10、提供native方法,则无需实现本地方法栈。本地方法栈既可以被实现成固定大小,也可以实现成可动态地扩展和收缩,因此在特定的场景下也会抛出StackOverflowError异常和OutOfMemoryError异常。方法区(Method Area)是线程间共享的区域,在JVM启动时创建,用于存储类的元信息、静态变量、常量、普通方法的字节码等内容。方法区可以被实现成大小固定或可动态扩展和收缩,如果内存空间不满足内存分配要求就会抛出OutOfMemoryError异常。对于HotSpot虚拟机而言,在JDK 1.8以前,方法区被实现为“永久代”(Permanent Generation),属于堆的

11、逻辑组成部分,并提供了两个参数调节其大小,-XX:PermSize用于设定初始容量,-XX:MaxPermSize用于设定最大容量。JDK 1.8之后,HotSpot不再有“永久代”的概念,类的元信息数据迁移到被称为“元空间”(Metaspace)的新区域,而静态变量、常量等则存储于堆中。元空间没有使用堆内存,而是分配在本地内存中,默认情况下其容量只受可用的本地内存大小限制。类似地,HotShot虚拟机也提供了两个参数来调节其大小,-XX:MetaspaceSize用于设定初始容量,-XX:MaxMetaspaceSize用于设定最大容量。运行时常量池(Runtime Constant Poo

12、l)运行时常量池(Runtime Constant Pool)属于方法区的一部分,class文件被加载到内存后,其中的常量池信息(包括符号引用和编译期可知的字面值常量)就被存储于此。 这些信息可通过javap解析class文件查看,考虑如下代码:使用javac命令将其编译成class文件后,用javap解析得到:由结果可知,class常量池信息中包含了MethodAreaRtcp类所有的符号引用和编译期可知的字面值常量。什么样的字面值才算是字面值常量?1、字符串字面值。编译器在编译 java 代码时,会将字符串字面值添加到常量池中,比如例子中的hello和 world在常量池中的位置分别为#6

13、7和#83。2、用final修饰的基础类型成员变量的字面值。注意,必须是用final修饰的,而且必须是类的成员变量的字面值才会被编译器添加到常量池中,如i2的值2在常量池中的位置为#32。i5虽然是用final修饰的基础类型成员变量,但是它的值5是运行时函数调用的结果,因此并未出现在常量池中;i7虽然是被final修饰,但是它本身是一个局部变量,因此它的值7并未出现在常量池中。另外,short、byte和char类型的字面值常量会被编译器转换为int类型后存入常量池中,如例子中s1、b1和c1的字面值常量对应在常量池中的位置分别为#41、#44和#47,它们的类型都是Integer。3、由字面

14、值常量相加得到的结果。因为编译器做了优化,多个字面值常量相加后得到的结果,也会被添加到常量池中。如例子中i6和str3,在常量池中对应的位置为#38和#85。程序执行阶段也会有新的常量加入运行时常量池中。在Java程序执行阶段,也会有新的常量加入到运行时常量池中,这部分并不属于class常量池,因此我们无法从javap的结果中找到这些常量。1、String.intern()方法的返回值会加入运行时常量池中。String.intern()方法调用时,如果常量池中已经存在与其相等的String对象(使用equals比较时返回true),则返回该字符串;否则将该字符串添加到常量池中,然后将其返回。如

15、下例子验证了这一特点:2、基础类型的包装类也用到了常量池技术。Java的部分基础类型的包装类(Character、Byte、Short、Integer、Long、Boolean)也用到了常量池技术,当使用数值字面值给它们赋值时,它们就会被存储到运行时常量池中。此外,位于运行时常量池中的包装类相加得到的结果,也会被存储在常量池中。值得注意的是,只有在 -127, 127 的范围内,包装类才会使用到常量池技术,超过该范围的还是会在堆中存储。以下例子验证了这些特点:堆(Heap)是运行时数据区中最大的一块区域,绝大部分的对象(包括类实例和数组)都在上面存储。堆是所有线程共享的,随着JVM的启动而创建

16、。我们通过new创建出来的对象都分配于此,而且无需主动释放对象内存,统一由垃圾收集器(Garbage Collector,GC)来进行管理和销毁 这也是Java跟C+相比区别最大的特点之一。当堆中没有足够的内存来创建对象时,就会抛出OutOfMemoryError异常。堆的分代管理JVM对堆进行了分代管理,分成新生代(Young Generation)和老年代(Old Generation),其中新生代中又分为Eden Space、From Survivor Space、To Survivor Space三个区域,如图所示。JVM将堆划分成这么多的区域,主要是为了方便垃圾收集器对对象进行管理,现代的垃圾收集器一般都采用了分代收集

展开阅读全文
相关资源
相关搜索

当前位置:首页 > IT计算机/网络 > 架构

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号