java内存模型

    科技2024-03-26  102

    Java内存模型

    《Java虚拟机规范》中规定了Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。Java虚拟机所管理的内存包括以下几个运行时数据区域:程序计数器(Program Counter Register)、Java虚拟机栈(Java Virtual Machine Stack)、本地方法栈(Native Method Stacks)、堆(Java Heap)、方法区(Method Area),如下图所示。

     

    程序计数器(Program Counter Register)

    程序计数器可以看作是当前线程所执行的字节码的行号指示器。

    线程私有记录正在执行的虚拟机字节码指令地址如果正在执行的本地(native)方法,这个计数器的值为空(undefined)唯一没有OOM(OutOfMemorryError)情况的区域

    Java虚拟机栈(Java Virtual Machine Stack)

    Java虚拟机栈描述的是Java方法执行的线程内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

    如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常如果虚拟机栈可以动态扩展(HotSpot虚拟机的栈容量不可以动态扩展),当扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常

         1.局部变量表

    局部变量表是存放方法参数和局部变量的区域。 局部变量没有准备阶段, 必须显式初始化。如果是非静态方法,则在 index[0] 位置上存储的是方法所属对象的实例引用,一个引用变量占 4 个字节,随后存储的是参数和局部变量。字节码指令中的 STORE 指令就是将操作栈中计算完成的局部变呈写回局部变量表的存储空间内。

    虚拟机栈规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常;如果虚拟机栈可以动态扩展(当前大部分的 Java 虚拟机都可动态扩展),如果扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常。

         2. 操作栈

    操作栈是个初始状态为空的桶式结构栈。在方法执行过程中, 会有各种指令往栈中写入和提取信息。JVM 的执行引擎是基于栈的执行引擎, 其中的栈指的就是操作栈。字节码指令集的定义都是基于栈类型的,栈的深度在方法元信息的 stack 属性中。

    i++ 和 ++i 的区别:

    i++:从局部变量表取出 i 并压入操作栈(load memory),然后对局部变量表中的 i 自增 1(add&store memory),将操作栈栈顶值取出使用,如此线程从操作栈读到的是自增之前的值。++i:先对局部变量表的 i 自增 1(load memory&add&store memory),然后取出并压入操作栈(load memory),再将操作栈栈顶值取出使用,线程从操作栈读到的是自增之后的值。

    之所以说 i++ 不是原子操作,即使使用 volatile 修饰也不是线程安全,就是因为,可能 i 被从局部变量表(内存)取出,压入操作栈(寄存器),操作栈中自增,使用栈顶值更新局部变量表(寄存器更新写入内存),其中分为 3 步,volatile 保证可见性,保证每次从局部变量表读取的都是最新的值,但可能这 3 步可能被另一个线程的 3 步打断,产生数据互相覆盖问题,从而导致 i 的值比预期的小。

         3. 动态链接

    每个栈帧中包含一个在常量池中对当前方法的引用, 目的是支持方法调用过程的动态连接。

         4.方法返回地址

    方法执行时有两种退出情况:

    正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;异常退出。

    无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:

    返回值压入上层调用栈帧。异常信息抛给能够处理的栈帧。PC计数器指向方法调用后的下一条指令。

    本地方法栈(Native Method Stacks)

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

    与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

    堆(Java Heap)

    Java堆是虚拟机管理的内存中最大的一块。被所有线程共享的一块区域,在虚拟机启动是创建。此区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

    Java堆是垃圾收集器管理的内存区域,因此也被称作“GC堆”(Garbage Collected Heap)。从回收内存的角度看,由于现代垃圾收集器大部分是基于分代收集理论设计的,所以Java堆中经常出现“新生代”、“老年代”、“永久代”、“Eden空间”、“From Survivor空间”、“To Survivor空间”等,这些区域划分仅仅是一部分垃圾收集器的共同特性或者设计风格而已,并不是某个Java虚拟机具体实现的固有内存布局,更不是《Java虚拟机规范》中堆Java堆的细致划分。

    从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有分配的缓冲区(Thread Loacl Allocation Buffer,TLAB),以提升对象分配时的效率。

    Java堆既可以被实现成固定大小的,也可以是可扩展的。当前主流的Java虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,将对抛出OutOfMemorryError异常。

    方法区(Method Area)

    方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

    垃圾收集行为在这个区域是比较少出现的,其内存回收目标主要是针对常量池的回收和对类型的卸载。当方法区无法满足内存分配需求时,将抛出 OutOfMemoryError 异常。

    JDK8 之前,Hotspot 中方法区的实现是永久代(Perm),JDK8 开始使用元空间(Metaspace),以前永久代所有内容的字符串常量移至堆内存,其他内容移至元空间,元空间直接在本地内存分配。

    为什么要使用元空间取代永久代的实现?

    字符串存在永久代中,容易出现性能问题和内存溢出。类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。将 HotSpot 与 JRockit 合二为一。

    运行时常量池(Running Constant Pool)

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

    一般来说,除了保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

    运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是 String 类的 intern() 方法。

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

     

    -- 深度理解Java虚拟机(第3版)

    -- https://www.cnblogs.com/czwbig/p/11127124.html

    Processed: 0.035, SQL: 8