JVM 内存模型,百度笔试题百度校招面试经验
Java 内存模型,往往是指 Java 程序在运行时内存的模型,而 Java 代码是运行在 Java 虚拟机之上的,由 Java 虚拟机通过解释执行(解释器)或编译执行(即时编译器)来完成,故 Java 内存模型,也就是指 Java 虚拟机的运行时内存模型。
作为 Java 开发人员来说,并不需要像 C/C++开发人员,需要时刻注意内存的释放,而是全权交给虚拟机去管理,那么有就必要了解虚拟机的运行时内存是如何构成的。运行时内存模型,分为线程私有和共享数据区两大类,其中线程私有的数据区包含程序计数器、虚拟机栈、本地方法区,所有线程共享的数据区包含 Java 堆、方法区,在方法区内有一个常量池。
(1)线程私有区:
程序计数器,记录正在执行的虚拟机字节码的地址;
虚拟机栈:方法执行的内存区,每个方法执行时会在虚拟机栈中创建栈帧;
本地方法栈:虚拟机的 Native 方法执行的内存区;
(2)线程共享区:
Java 堆:对象分配内存的区域;
方法区:存放类信息、常量、静态变量、编译器编译后的代码等数据;
常量池:存放编译器生成的各种字面量和符号引用,是方法区的一部分。
对于大多数的程序员来说,Java 内存比较流行的说法便是堆和栈,这其实是非常粗略的一种划分,这种划分的”堆”对应内存模型的 Java 堆,”栈”是指虚拟机栈,然而 Java 内存模型远比这更复杂,想深入了解 Java 的内存,还是有必要明白整个内存模型。
运行时内存分为五大块区域(常量池属于方法区,算作一块区域),前面简要介绍了每个区域的功能,那接下来再详细说明每个区域的内容,Java 内存总体结构图如下:
2.1 程序计数器 PC
2.1.1. 什么是程序计数器?
程序计数器是一块较小的内存空间,可以把它看作当前线程正在执行的字节码的行号指示器。也就是说,程序计数器里面记录的是当前线程正在执行的那一条字节码指令的地址。?
注:但是,如果当前线程正在执行的是一个本地方法,那么此时程序计数器为空。?
2.1.2. 程序计数器的作用
程序计数器有两个作用:
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。?
2.1.3. 程序计数器的特点
是一块较小的存储空间
线程私有。每条线程都有一个程序计数器。
是唯一一个不会出现 OutOfMemoryError 的内存区域。
生命周期随着线程的创建而创建,随着线程的结束而死亡。?
2.2 虚拟机栈
虚拟机栈,生命周期与线程相同,是 Java 方法执行的内存模型。每个方法(不包含 native 方法)执行的同时都会创建一个栈帧结构,方法执行过程,对应着虚拟机栈的入栈到出栈的过程。
特点:
局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建。而且,局部变量表的大小在编译时期就确定下来了,在创建的时候只需分配事先规定好的大小即可。此外,在方法运行的过程中局部变量表的大小是不会发生改变的。
Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。?
a) StackOverFlowError:?
若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 异常。?
b) OutOfMemoryError:?
若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 异常。
Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
注:StackOverFlowError 和 OutOfMemoryError 的异同??
StackOverFlowError 表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。?
而 OutOfMemoryError 是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。?
栈帧(Stack Frame)结构
栈帧是用于支持虚拟机进行方法执行的数据结构,是属性运行时数据区的虚拟机站的栈元素。见上图, 栈帧包括:
局部变量表 (locals 大小,编译期确定),一组变量存储空间, 容量以 slot 为最小单位。
操作栈(stack 大小,编译期确定),操作栈元素的数据类型必须与字节码指令序列严格匹配
动态连接, 指向运行时常量池中该栈帧所属方法的引用,为了 动态连接使用。
前面的解析过程其实是静态解析;
对于运行期转化为直接引用,称为动态解析。
方法返回地址
正常退出,执行引擎遇到方法返回的字节码,将返回值传递给调用者
异常退出,遇到 Exception,并且方法未捕捉异常,那么不会有任何返回值。
额外附加信息,虚拟机规范没有明确规定,由具体虚拟机实现。
注意:人们常说,Java 的内存空间分为“栈”和“堆”,栈中存放局部变量,堆中存放对象。?
这句话不完全正确!这里的“堆”可以这么理解,但这里的“栈”只代表了 Java 虚拟机栈中的局部变量表部分。真正的 Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。?
异常(Exception)
Java 虚拟机规范规定该区域有两种异常:
StackOverFlowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出但内存空间可能还有很多。?
OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出内存也全都用光了
2.3 本地方法栈
本地方法栈和 Java 虚拟机栈实现的功能类似,只不过本地方法区是本地方法运行的内存模型。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间。
异常(Exception):Java 虚拟机规范规定该区域可抛出 StackOverFlowError 和 OutOfMemoryError。
2.4 Java 堆
Java 堆,是 Java 虚拟机管理的最大的一块内存,也是 GC 的主战场,里面存放的是几乎所有的对象实例和数组数据。整个 Java 虚拟机只有一个堆,所有的线程都访问同一个堆。而程序计数器、Java 虚拟机栈、本地方法栈都是一个线程对应一个的。
从内存回收角度,Java 堆被分为新生代和老年代;新生代又可被分为:Eden、From Survior、To Survior。?
不同的区域存放具有不同生命周期的对象。这样可以根据不同的区域使用不同的垃圾回收算法。这样划分的好处是为了更快的回收内存;
从内存分配角度,Java 堆可以划分出线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB);这样划分的好处是为了更快的分配内存;
堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的,因此当线程请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出 OutOfMemoryError。
对象创建的过程是在堆上分配着实例对象,那么对象实例的具体结构如下:
对于填充数据不是一定存在的,仅仅是为了字节对齐。HotSpot VM 的自动内存管理要求对象起始地址必须是 8 字节的整数倍。对象头本身是 8 的倍数,当对象的实例数据不是 8 的倍数,便需要填充数据来保证 8 字节的对齐。该功能类似于高速缓存行的对齐。
评论