32个问题,学习Java虚拟机的运行时数据区
学习JVM虚拟机是一个比较枯燥无味的过程,刚开始基本是看不懂学不懂,然后就是似懂非懂,最后觉得好像懂了一些,到后来又觉得还是没懂,反正就是懵懵懂懂,过目就忘,一问就卡住,说也说不清,其实说的就是我自己。
我觉得在学习了相关理论知识之后,除了进行实操之外,通过提问和回答的方式,也能更好的理解所学知识,并检验自己是否真的理解了。
今天我们要学习的是Java虚拟机的运行时数据区,包括程序计数器(Program Counter Register)、Java虚拟机栈(Java Virtual Machine Stack)、本地方法栈(Native Method Stack)、堆(Heap)、方法区(Method Area)、运行时常量池(Run-Time Constant Pool)。
主要是基于Java SE 8的规范(The Java Virtual Machine Specification, Java SE 8 Edition)。
公共问题
哪些区域是线程共享的,什么时候创建与销毁?
哪些区域是线程私有的,什么时候创建与销毁?
线程私有的内存区域,包括程序计数器、Java虚拟机栈和本地方法栈 。
Java虚拟机栈、堆和方法区使用的内存要保证是连续的吗?
都不需要。
(The memory for … does not need to be contiguous.)
所有区域都会抛出OutOfMemoryError?
不是。
程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域 。
程序计数器
什么是程序计数器?有什么用?
程序计数器,也叫PC寄存器(Program Counter Register)。
在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令 , 分支、循环、跳转、异常处理、线程恢复等基础功能 都需要依赖这个计数器来完成。
(具体的虚拟机实现可以采用更高效的方式)
程序计数器对于Java方法和本地方法有区别吗?
如果线程正在执行的是一个Java方法,那么这个计数器记录的是正在执行的虚拟机字节码指令的地址 。
如果线程正在执行的是一个Native方法,那么这个计数器值是undefined 。
为什么每个线程要有一个程序计数器?
为了线程切换后能恢复到正确的执行位置 。
线程间进行上下文切换时,需要先保存程序计数器,在恢复线程时继续从上次保存的程序计数器位置继续执行。
多个线程同时执行时,需要有多个程序计数器来分别指示下一条待执行的指令。
Java虚拟机栈和本地方法栈
两者有什么区别?
区别是虚拟机栈为虚拟机执行Java方法服务 ,而本地方法栈则为虚拟机使用到的Native方法服务 。
Java虚拟机栈的作用是什么?
Java虚拟机栈的作用与传统语言(例如C语言)中的栈非常类似,用于存储局部变量与一些尚未计算好的结果。另外,它在方法调用和返回中也扮演了很重要的角色 。
Java虚拟机栈在何时抛出何种异常?
如果线程请求分配的栈容量超过Java虚拟机栈所允许的最大容量 ,将抛出StackOverflowError异常。
如果Java虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存 ,或者创建新线程时没有足够的内存去创建对应的虚拟机栈 ,将抛出OutOfMemoryError异常。
Java虚拟机栈默认大小是多少?如何修改?
虚拟机栈默认大小为1M(以前是256K),可以通过-Xss参数进行设置。
栈帧是什么,什么时候创建与销毁?
栈帧是用来存储数据和部分过程结果的数据结构(操作数栈、局部变量表) ,同时也用来处理动态链接(dynamic linking)、方法返回值和异常分派(dispatch exception) 。
栈帧随着方法的调用而创建,随着方法的结束而销毁。
抛出了在方法内未被捕获的异常,也算方法结束。
栈帧里面都装了什么内容?
主要是局部变量表、操作数栈和指向当前方法所属的类的运行时常量池的引用 。
不同线程的栈帧之间可以互相引用吗?
不可以。
Java虚拟机栈是线程私有的,因而栈帧也是线程本地私有的数据。
如何理解当前栈帧?方法相互调用如何影响栈帧?
栈帧随着方法的调用而创建,当前正在执行的方法称为当前方法,对应的栈帧就是当前栈帧 。
当前方法调用了新的方法时 ,新的栈帧也会随之创建。
随着程序的控制权移交到新的方法,新的栈帧也会成为新的当前栈帧。
当方法返回时 ,当前栈帧会传回此方法的执行结果给前一个栈帧。
然后,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
局部变量表
什么是局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量 。
Java虚拟机使用局部变量表来完成方法调用时的参数传递 。
局部变量表存储在哪里?
每一个栈帧有一个自己的局部变量表,栈帧中局部变量表的长度由编译期决定。
局部变量表存储于类或接口的二进制表示之中(如class文件),通过方法的code属性保存及提供给栈帧使用。
本地方法栈是必须的吗?
如果Java虚拟机不支持native方法,或是本身不依赖传统栈,那么可以不提供本地方法栈。
如果支持本地方法栈,那这个栈一般会在线程创建的时候按线程分配。
本地方法栈在何时抛出何种异常?
Java堆
堆的作用
存放对象实例 ,几乎所有的对象实例都是在这里分配内存的。
所有的对象实例真的都要在堆上分配吗?
通常情况下,所有的对象实例,包括数组都是在堆上分配的。
但随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙变化发生,所有的对象都分配在堆上也渐渐变得不是那么绝对了。
堆一般会怎么划分使用?
现在的垃圾收集器基本都采用分代收集算法,所以Java堆一般划分为新生代和老年代。新生代又可以再划分为Eden区、From Survivor区和To Survivor区。
为什么堆内存要进行划分?
由于绝大多数对象的生命周期通常比较短,因此根据对象的生命周期划分为新生代和老年代。
这样设计,有利于分别采用更合适的垃圾收集算法,提高回收效率。
新生代的大部分对象生命周期较短,使用复制算法只需要移动少量对象,剩下的直接清理掉。
老年代的对象生命周期相对较长,使用标记清理或标记整理算法更加高效。
本地线程分配缓冲区TLAB可以更安全更快地分配内存。
TLAB是什么?
TLAB是指本地线程分配缓冲区(Thread Local Allocatoin Buffer),即给对象分配内存之前,每个线程在Java堆中预先申请的一小块内存 。
TLAB有什么作用?
一方面是为了解决多线程并发分配内存的线程安全问题 ,另一方面可以更快的分配内存 。
TLAB如何工作?
给对象分配内存之前,每个线程在Java堆中预先申请一小块内存(本地线程分配缓冲区)。
哪个线程要分配内存,就在哪个线程的TLAB上分配。
当TLAB使用完了,需要重新申请一块新的内存。
方法区
什么是方法区,以及其作用?
方法区与传统语言中的编译代码存储区 (storage area for compiled code)或者操作系统进程的正文段 (text segment)的作用非常类似。
它存储了每一个类的结构信息,例如,运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容 ,还包括一些在类、实例、接口初始化时用到的特殊方法 (实例初始化方法
、类或接口的初始化方法
)。
(存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
)
当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
方法区进行垃圾回收的目的是什么?
主要是针对常量池的回收和对类型的卸载 。
简单的虚拟机实现可以选择在这个区域不实现垃圾收集与压缩。
方法区就是永久代吗?
方法区是Java虚拟机运行时数据区的一个组成部分,而永久代属于具体虚拟机的一个实现。
只是因为HotSpot虚拟机的设计者选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。
使用永久代来实现方法区有缺点吗?
使用永久代来实现方法区,现在看来并不是一个好主意,因为这样更容易遇到内存溢出问题 。
在目前已经发布的JDK 1.7的HotSpot中,已经把原本放在永久代的字符串常量池移出。
JDK 1.8使用元空间取代了永久代,主要用于存储类的元数据。
运行时常量池
运行时常量池是什么?
是class文件中每一个类或接口的常量池表的运行时表示形式 。
它包括了若干种不同的常量,从编译期可知的数值字面量到必须在运行期解析后才能获得的方法或字段引用。
运行时常量池类似于传统语言中的符号表(symbol table),不过它存储的数据范围更为广泛。
创建类或接口时,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
通过javap命令,可以查看class文件的常量池信息。
何时创建?
在虚拟机启动的时候,具体在加载类和接口到虚拟机后,就创建对应的运行时常量池。
在哪里分配?
每一个运行时常量池都在Java虚拟机的方法区中分配。
闲来无聊,也开了个免费知识星球。
谈技术,也聊人生。
参考
《The Java Virtual Machine Specification, Java SE 8 Edition》
《Java虚拟机规范》(Java SE 8版)爱飞翔 周志明 等译
Java Class文件结构实例分析
JVM指令分析实例
标签: