一. 概述
java内存区域共分为5块,其中方法区和堆区是所有线程共享的;虚拟机栈、本地方法栈、PC程序计数器是每个线程私有的。
image.png
二. 堆内存
整个堆内存由新生代和老年代组成,不包括元空间;一般新生代和老年代的大小比例为1:2。
其中新生代由Eden区和两个Survivor区构成,一般三者之间大小比例为8:1:1。
下图分别是1.8及以后版本、1.7及以前版本的内存区域图。
image.png
image.png
Eden区
1、Eden区位于新生代,是新对象分配内存的地方,由于堆是所有线程共享的所以在堆上分配内存需要加锁。
2、而Sun JDK为提升效率,会为每个新建的线程在Eden上分配一块独立的空间由该线程独享,这块空间称为TLAB(Thread Local Allocation Buffer)。
3、在TLAB上分配内存不需要加锁,因此JVM在给线程中的对象分配内存时会尽量在TLAB上分配。
4、如果对象过大或TLAB用完,则仍然在堆上进行分配。如果Eden区内存也用完了,则会进行一次Minor GC
堆内存分配异常
1、Java虚拟机规范规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。
2、Java虚拟机规范规定,如果在堆上没有内存完成实例分配,并且堆上也无法再扩展时,将会抛出OutOfMemoryError异常。
三. 方法区
方法区也是被所有的线程共享的一块内存区域。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样 不需要连续的内存和可以选择固定大小或者可扩展之外,还可以选择不实现垃圾回收。(这区域的内存回收目标主要是针对常量池的回收和类型的卸载,一般而言这个区域的内存回收比较难以令人满意,尤其是类型的回收条件相当苛刻,但是这部分区域的内存回收确实是必要的)
Java虚拟机规范规定,当方法区无法满足内存分配的需求时,将抛出OutOfMemoryError异常。
运行时常量池是方法区的一部分。CLass文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
方法区、永久代、元空间
1、方法区是逻辑上的东西,是JVM 的规范,所有虚拟机必须遵守的。
2、PermGen永久代是JDK7及之前方法区的实现(-XX:PermSize 设置永生代大小)。
3、Metaspace元空间是JDK8及之后方法区的实现。
4、元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存
四. PC程序计数器
程序计数器可以看做是当前线程所执行的字节码的行号指示器。
如果线程执行的是一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址。
如果线程执行的是一个Native方法,计数器的值为空。
Java虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域。
五. 虚拟机栈
每个方法执行的同时会创建一个栈帧,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
Java虚拟机栈是线程私有的,它的生命周期与线程相同。
局部变量表存放了编译时期可知的各种基本数据类型和对象引用。局部变量表所需的内存空间在编译时期完成分配。当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,方法运行期间不会改变局部变量表的大小。
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError 异常。
如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
image.png
5.1 局部变量表
局部标量表是一组变量值的存储空间,用于存放方法参数和局部变量。在Class文件的方法表的Code属性的max_locals指定了该方法所需局部变量表的最大容量。
局部变量表的基本单位为变量槽(slot,下文有单独的介绍);局部变量表存放的是方法参数和局部变量;虚拟机是通过索引定位的方式使用局部变量表。
当调用方法是非static方法时,局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用(reference),即 “this” 关键字指向的对象。分配完方法参数后,便会依次分配方法内部定义的局部变量。
为了节省栈帧空间,局部变量表中的 Slot 是可以重用的。因为即使是一个方法内,也是存在作用域的,当离开了某些变量的作用域之后,这些变量对应的 Slot 空间就可以交给其他变量使用。但是这种机制有时候会影响垃圾回收行为,原因很简单,当离开某个作用域时,如果没有新的变量值覆盖之前作用域内的变量(指reference)空间,那么当垃圾回收时,则该引用对应的java堆中的内存则不允许被回收,因为局部变量表中还存在该引用。所以问题在于虚拟机并没有主动清理局部变量表中离开作用域的变量值,而是采用新盖旧的方法被动清理。
slot槽的大小:一般跟虚拟机有关,32位内存的占4字节,64位的占8字节; long和double占2个槽,其他占一个槽。
5.2 操作数栈
操作数栈是方法执行算术运算或者是调用其他的方法进行参数传递的时候时的媒介,这就是“基于栈的执行引擎”。
编译代码后就确定栈帧需要多深的操作数栈,在Class 文件的Code 属性的 max_stacks 指定了执行过程中最大的栈深。
int a=1;
int b=2;
int c=a+b;
iload_0 // 将局部变量表0号索引的值入操作数栈
iload_1 // 将局部变量表1号索引的值入操作数栈
iadd // 操作数栈去除前两位相加,放入栈顶
istore_2 // 操作数栈顶元素出栈,放入局部变量表2号索引
5.3 动态链接
动态链接主要就是指向运行时常量池的方法引用。
每一个栈帧内存都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference )保存在class文件的常量池里。比如,描述一个方法调用其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
image.png
5.4 方法返回地址
方法只有两种退出方式:
1、当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口,一般来说,调用者的PC计数器可以作为返回地址。
2、当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口,返回地址要通过异常处理器表来确定。
方法返回时能进行3个操作:
1、恢复上层方法的局部变量表和操作数栈
2、把返回值压入调用者调用者栈帧的操作数栈
3、调整 PC 计数器的值以指向方法调用指令后面的一条指令
六. 本地方法栈
本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法服务。