运行时数据区是指对 JVM 运行过程中涉及到的内存根据功能、目的进行的划分,而内存模型可以理解为对内存进行存取操作的过程定义。总是有人望文生义的将前者描述为 “Java 内存模型”,最近在阅读《深入理解 Java 虚拟机》之后对二者加深了部分理解,于是写一篇相关内容的学习总结。

运行时数据区

《Java 虚拟机规范》定义中,由 JVM 管理的内存区域分为以下几个运行时数据区域:

flowchart LR
subgraph 运行时数据区
subgraph 线程私有
虚拟机栈
本地方法栈
程序计数器
end
subgraph 线程共享
方法区
javaHeap[Java 堆]
end
end

程序计数器

程序计数器(Program Counter Register)的生命周期和 Java 线程一致,仅能被相关线程访问。用于记录当前线程所执行的字节码的行号,代码中的分支、循环、跳转、异常处理和多线程发生切换时的线程恢复等基础功能都需要依赖程序计数器完成。当执行 Java 代码时,程序计数器存储正在执行的虚拟机字节码指令地址,而执行本地方法时,程序计数器应该为空。

《Java 虚拟机规范》中规定程序计数器不会发生 OutOfMemoryError 异常情况。

虚拟机栈和本地方法栈

虚拟机栈(Java Virtual Machine Stack)的生命周期和 Java 线程一致,仅能被相关线程访问。用于描述 Java 方法执行的线程内存模型:每次进入一个新的方法时,JVM 都会同步创建一个栈帧,栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每次执行结束一个方法时,对应栈帧就会出栈。

虚拟机栈可能发生两类异常情况:

  • StackOverflowError:线程请求的栈深度大于虚拟机允许的深度
  • OutOfMemoryError:如果 JVM 栈容量可以动态拓展,当需要拓展时 JVM 无法申请到足够的内存就会抛出该异常。而在 HotSpot 这种不允许动态拓展的虚拟机中,如果创建时就失败依然也会抛出该异常。

本地方法栈(Native Method Stack)与虚拟机栈基本类似,当执行本地方法时使用本地方法栈,同样会抛出 StackOverflowErrorOutOfMemoryError

《Java 虚拟机规范》对本地方法栈的实现方式没有任何强制规范,故 HotSpot 虚拟机中虚拟机栈和本地方法栈直接合二为一。

虚拟机栈的大小通过 java 命令的参数 -Xss 设定。

Java 堆

Java 堆(Java Heap)在虚拟机启动时就创建,虚拟机关闭时销毁,被所有 Java 线程共享。用于存放所有的对象实例。

Java 堆受垃圾回收器管理,由于现代主流的垃圾回收器都是基于分代收集理论设计,Java 堆中经常会出现“新生代”、“老年代”、“永久代”、“Eden空间”、“From Survivor空间”、“To Survivor空间” 等名词。这些名词对于 Java 堆的划分,是指一部分垃圾回收器的设计风格,而不是 JVM 具体实现的固有内存布局,更不是 《Java 虚拟机规范》里对 Java 堆的进一步细致划分。并且近年来新出现的垃圾回收器也有不采用分代设计的,再用这些名词划分 Java 堆空间也已经不正确了。

Java 堆可能发生 OutOfMemoryError异常:分配内存给新的对象实例失败、且堆无法再拓展时抛出该异常。

Java 堆的大小通过 java 命令的参数 -Xmx-Xms 设定。

方法区

方法区(Method Area)和 Java 堆一样在虚拟机启动时就创建,虚拟机关闭时销毁,被所有 Java 线程共享。用于存储已被虚拟机加载的类型信息、常量、静态变量和即时编译器编译后的代码缓存。

在 HotSpot 虚拟机中,方法区的实现经历过两次大规模改动:

  1. JDK 6 及以前:为了方便使用分代垃圾收集器管理方法区内存,使用永久代(Permanent Generation)实现方法区。
  2. JDK 7 时期:字符串常量池和静态变量转移到本地内存中(Native Memory),其他数据依然存放在由永久代实现的方法区中。
  3. JDK 8 及此后:方法区中的全部数据都存放在被称为元空间(Meta-space)的本地内存中。

如果方法区无法满足新的分配内存需求时会抛出 OutOfMemoryError 异常。

内存模型

内存模型:在特定的缓存一致性协议约束下,对特定的内存高速缓存进行读写访问的过程抽象。内存模型主要用于在共享内存的多核系统中解决缓存一致性的问题。不同架构的物理机通常具有不一样的内存模型,而Java虚拟机也有自己的内存模型。

在硬件层面上,内存模型针对高速缓存。如图所示,处理器运算速度和主内存的存取速度天差地别,必须加入高速缓存来作为内存和处理器之间的缓冲。如果两个处理器执行的指令都涉及到主内存同一区域,就会发生各自的缓存数据不一致。这种情况下需要通过缓存一致性协议规定处理器的读写行为。

flowchart LR

CpuOne(处理器一) <--> CacheOne(高速缓存一)
CacheOne <--> Protocol[缓存一致性协议]

CpuTwo(处理器二) <--> CacheTwo(高速缓存二)
CacheTwo <--> Protocol

Protocol <--> Memory((主内存))

在 JVM 中,内存模型针对各线程的工作内存。如图所示,其中关键名词解释如下:

  • 主内存:与上方硬件层面的主内存概念一致,实际是操作系统为 JVM 分配的物理内存空间。
  • 工作内存:每个线程各自的工作内存,可与上方硬件层面的高速缓存类比。

工作内存中保存了该线程使用到的变量的主内存副本(并不是百分之百的完全副本,例如两条线程同时访问一个 10MB 的对象,并不会在各自的工作内存中都创建一个完全相同的对象),对每个变量的读、写操作都必须在工作内存中进行,不能直接访问主内存(volatile 变量通过读写屏障实现,也受这条规则约束,并不是直接访问主内存)。

主内存、工作内存的划分方式与前一章中阐述的堆、栈、方法区等划分方式是截然不同的概念,不能类比或对应。

flowchart LR

ThreadOne(线程一) <--> WorkingMemoryOne(工作内存一)
WorkingMemoryOne <--> SaveAndLoad[Save和Load操作]

ThreadTwo(线程二) <--> WorkingMemoryTwo(工作内存二)
WorkingMemoryTwo <--> SaveAndLoad

SaveAndLoad <--> Memory((主内存))

volatile 变量

volatile 变量有可见性和禁止指令重排序优化两个特点。

可见性

一个线程修改了 volatile 变量,其他线程立即可知。在 JVM 中的实现方式为,线程对 volatile 变量的读取时,需要先将主内存中的当前值拷贝到工作内存中;线程对 volatile 变量写入时,写入工作内存后立即同步到主内存中。注意可见性和原子性是不同的概念,volatile 关键字无法保证对变量操作的原子性。

禁止指令重排序优化

指令重排序优化是指处理器为了提高运算单元的利用率,会对指令进行乱序执行优化,处理器能够保证经过乱序的指令和原始顺序的指令执行结果一致。禁止指令重排序的方式是添加内存屏障,重排序时不能把后面的指令重排序到内存屏障之前的位置。

如果在多线程环境中,指令重排序优化可能导致访问共享资源出错,一个常见的例子就是单例模式的双锁检查式实现方式需要将实例引用添加 volatile 修饰。

public class Singleton {
//如果不添加 volatile 修饰可能发生异常
private volatile static Singleton instance; public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
} private Singleton() {};
}

其中 instance = new Singleton(); 一句对应的字节码指令为:

NEW Singleton
DUP
INVOKESPECIAL Singleton.<init> ()V
PUTSTATIC Singleton.instance : LSingleton;

总共分为四个步骤:

  1. NEW 指令创建对象(此时还没有执行构造函数,对象的所有成员变量都是默认的“零值”),将对象引用压入操作数栈。
  2. DUP 指令将当前操作数栈顶的值拷贝一份(此时操作数栈顶的两个元素是两个相同的、值为前一步创建的对象的引用)。
  3. INVOKESPECIAL 指令会调用当前操作数栈顶的对象的 <init> 方法,该方法是根据空参的构造函数生成的。
  4. PUTSTATIC 指令将操作数栈顶的对象引用赋值给 Singleton 类的 instance 引用。

根据指令重排序的原则,三、四两步之间乱序执行不会影响结果,那么如果发生了指令重排、且两个线程恰好按照如下步骤执行就会发生异常情况(篇幅所限不给出能发生指令重排序的代码,其实就是上方代码删去 volatile ):

sequenceDiagram
threadOne ->> threadOne : NEW Singleton
threadOne ->> threadOne : DUP
threadOne ->> Singleton.class : PUTSTATIC Singleton.instance : LSingleton;

threadTwo ->> Singleton.class : 调用 getInstance() 函数
Singleton.class ->> threadTwo : Singleton.instance 引用不为 null,返回引用值

threadTwo ->> threadTwo : 使用单例对象
Note over threadTwo : 此时单例对象还未完全初始化,发生异常

threadOne ->> threadOne : INVOKESPECIAL

而如果 instance 使用 volatile 修饰,由于内存屏障的存在指令 INVOKESPECIALPUTSTATIC 之间就不会发生指令重排,确保了上述问题不会发生。另外请注意,Java 中双锁检查式方式实现的单例在 Java 5 之后才能完全保证可用,此前版本依然会出现问题。

Java 运行时数据区和内存模型的更多相关文章

  1. 002-JVM运行时数据区【内存模型】

    一.概述 JVM定义了不同运行时数据区,他们是用来执行应用程序的.某些区域随着JVM启动及销毁,另外一些区域的数据是线程性独立的,随着线程创建和销毁. 1.1.jvm自身物理结构 1.2.java内存 ...

  2. 读书笔记-浅析Java运行时数据区

    作为一个 Java 为主语言的程序员,我偶尔也需要 用 C/C++ 写程序,在使用时让我很烦恼的一件事情就是需要对 new 出来的对象进行 delete/free 操作,我老是担心忘了这件事情,从而导 ...

  3. Jvm基础(1)-Java运行时数据区

    最近在看<深入理解Java虚拟机>,里面讲到了Java运行时数据区,这是Jvm基本知识,把读书笔记记录在此.这些知识属于常识,都能查到的,如果我有理解不对的地方,还请指出. 首先把图贴上来 ...

  4. Java JVM运行时数据区,内存管理和GC垃圾回收

    一 . 运行时数据区 程序计数器是线程私有的,是一块很小的内存空间,是当前线程执行到字节码行号的计数指示器.每个CPU处理器核心 在任何一个时刻,都只可能运行着唯一的一个线程,执行着一条指令.所以在多 ...

  5. Java运行时数据区

    目录 1. 概述 2. Java内存结构 3. 程序计数器 4. Java虚拟机栈 5. 本地方法栈 6. 堆 7. 方法区 8. 运行时常量池 9. 直接内存 10. 总结 1. 概述 作为日常的J ...

  6. Java 运行时数据区

    写在前面 本文描述的有关于 JVM 的运行时数据区是基于 HotSpot 虚拟机. 概述 JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以 ...

  7. 【转】Java运行时数据区简介及堆与栈的区别

    理解JVM运行时的数据区是Java编程中的进阶部分.我们在开发中都遇到过一个很头疼的问题就是OutOfMemoryError(内存溢出错误),但是如果我们了解JVM的内部实现和其运行时的数据区的工作机 ...

  8. JVM内存布局(又叫Java运行时数据区)

    JVM 堆中的数据是共享的,是占用内存最大的一块区域. 可以执行字节码的模块叫作执行引擎. 执行引擎在线程切换时怎么恢复?依靠的就是程序计数器. JVM 的内存划分与多线程是息息相关的.像我们程序中运 ...

  9. Java运行时数据区概述

    Java 虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,如图所示: 程序计数器 程序计数器是一块比较小的内存空间,可以看作是当前线程所执行的字节 ...

随机推荐

  1. [Golang]-8 工作池、速率限制、原子计数器、互斥锁

    目录 工作池 速率限制 原子计数器 互斥锁 工作池 在这个例子中,我们将看到如何使用 Go 协程和通道实现一个工作池 . func worker(id int, jobs <-chan int, ...

  2. tensorflow报错:Attempting to fetch value instead of handling error Internal: failed to get device attribute 13 for device 0: CUDA_ERROR_UNKNOWN:

    就是在spyder跑上一篇文章的代码然后就报错: Attempting to fetch value instead of handling error Internal: failed to get ...

  3. vue中获取元素并控制相应的dom

    1 在标签中使用ref="xxx" 2 在methods中调用this.$refs.xxx this.$refs.xxx.$el获取dom 注意1:大多数情况下为了复用方法,将xx ...

  4. element-ui UI 组件库剖析

    element-ui UI 组件库剖析 /* Automatically generated by './build/bin/build-entry.js' */ https://github.com ...

  5. 如何在 Apple Watch S6上离线播放音乐

    如何在 Apple Watch S6上离线播放音乐 Apple Watch 离线播放音乐 营销策略,捆绑销售 Apple Watch + AirPods + Apple Music Apple Wat ...

  6. URL parser All In One

    URL parser All In One const url = new URL(`https://admin:1234567890@cdn.xgqfrms.xyz:8080/logo/icon.p ...

  7. koa-router all in one

    koa-router all in one holy shit , WTF, which is the true koa-router! MMP, 哪一个是正确的呀,fuck 找半天都晕了! koa- ...

  8. how to measure function performance in javascript

    how to measure function performance in javascript Performance API Performance Timeline API Navigatio ...

  9. CSS Modules in depth

    CSS Modules in depth https://github.com/css-modules/css-modules https://webpack.js.org/loaders/css-l ...

  10. Baccarat流动性挖矿是如何改进自动化做市商的痛点的?

    Baccarat自上线至今已经有两个多月的时间,尤其代币BGV引来了无数投资者的注意.同时也有越来越多的投资者开始关注到Baccarat本身,Baccarat采取的AMM机制,与其他的DeFi项目所采 ...