共计 6913 个字符,预计需要花费 18 分钟才能阅读完成。
Java 内存区域(运行时数据区域)和内存模型(JMM)
Java 内存区域和内存模型是不一样的货色,内存区域是指 Jvm 运行时将数据分区域存储,强调对内存空间的划分。
而内存模型(Java Memory Model,简称 JMM)是定义了线程和主内存之间的形象关系,即 JMM 定义了 JVM 在计算机内存 (RAM) 中的工作形式,如果咱们要想深刻理解 Java 并发编程,就要先了解好 Java 内存模型。
Java 运行时数据区域
家喻户晓,Java 虚拟机有主动内存管理机制,如果呈现内存透露和溢出方面的问题,排查谬误就必须要理解虚拟机是怎么应用内存的。
下图是 JDK8 之后的
JDK8 之前的内存区域图如下:
在 HotSpot JVM 中,永恒代中用于寄存类和办法的元数据以及常量池,比方 Class 和 Method。每当一个类首次被加载的时候,它的元数据都会放到永恒代中。
永恒代是有大小限度的,因而如果加载的类太多,很有可能导致永恒代内存溢出,即万恶的 java.lang.OutOfMemoryError: PermGen,为此咱们不得不对虚拟机做调优。
那么,Java 8 中 PermGen 为什么被移出 HotSpot JVM 了?我总结了两个次要起因:
因为 PermGen 内存常常会溢出,引发宜人的 java.lang.OutOfMemoryError: PermGen,因而 JVM 的开发者心愿这一块内存能够更灵便地被治理,不要再经常出现这样的 OOM
移除 PermGen 能够促成 HotSpot JVM 与 JRockit VM 的交融,因为 JRockit 没有永恒代。
依据下面的各种起因,PermGen 最终被移除,办法区移至 Metaspace,字符串常量移至 Java Heap。
援用自 https://www.sczyh30.com/posts…
程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它能够看作是以后线程所执行的字节码的行号指示器。
因为 Java 虚拟机的多线程是通过线程轮流切换并调配处理器执行工夫的形式来实现的,在任何一个确定的时刻,一个处理器内核都只会执行一条线程中的指令。
因而,为了线程切换后能复原到正确的执行地位,每条线程都须要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程公有”的内存。
如果线程正在执行的是一个 Java 办法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 办法,这个计数器值则为空(Undefined)。此内存区域是惟一一个在 Java 虚拟机标准中没有规定任何 OutOfMemoryError 状况的区域。
Java 虚拟机栈
与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程公有的,它的生命周期与线程雷同。
虚拟机栈形容的是 Java 办法执行的内存模型:每个办法在执行的同时都会创立一个栈帧(Stack Frame,是办法运行时的根底数据结构)用于存储局部变量表、操作数栈、动静链接、办法进口等信息。每一个办法从调用直至执行实现的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
在流动线程中,只有位千栈顶的帧才是无效的,称为以后栈帧。正在执行的办法称为以后办法,栈帧是办法运行的根本构造。在执行引擎运行时,所有指令都只能针对以后栈帧进行操作。
- 局部变量表
局部变量表是寄存办法参数和局部变量的区域。局部变量没有筹备阶段,必须显式初始化。如果是非静态方法,则在 index[0] 地位上存储的是办法所属对象的实例援用,一个援用变量占 4 个字节,随后存储的是参数和局部变量。字节码指令中的 STORE 指令就是将操作栈中计算实现的部分变呈写回局部变量表的存储空间内。
虚拟机栈规定了两种异样情况:如果线程申请的栈深度大于虚拟机所容许的深度,将抛出 StackOverflowError 异样;如果虚拟机栈能够动静扩大(以后大部分的 Java 虚拟机都可动静扩大),如果扩大时无奈申请到足够的内存,就会抛出 OutOfMemoryError 异样。
- 操作栈
操作栈是个初始状态为空的桶式构造栈。在办法执行过程中,会有各种指令往
栈中写入和提取信息。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 的值比预期的小。
- 动静链接
每个栈帧中蕴含一个在常量池中对以后办法的援用,目标是反对办法调用过程的动静连贯。
4. 办法返回地址
办法执行时有两种退出状况:
失常退出,即失常执行到任何办法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;
异样退出。
无论何种退出状况,都将返回至办法以后被调用的地位。办法退出的过程相当于弹出以后栈帧,退出可能有三种形式:
返回值压入下层调用栈帧。
异样信息抛给可能解决的栈帧。
PC 计数器指向办法调用后的下一条指令。
本地办法栈
本地办法栈(Native Method Stack)与虚拟机栈所施展的作用是十分类似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java 办法(也就是字节码)服务,而本地办法栈则为虚拟机应用到的 Native 办法服务。Sun HotSpot 虚拟机间接就把本地办法栈和虚拟机栈合二为一。与虚拟机栈一样,本地办法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异样。
线程开始调用本地办法时,会进入 个不再受 JVM 束缚的世界。本地办法能够通过 JNI(Java Native Interface)来拜访虚拟机运行时的数据区,甚至能够调用寄存器,具备和 JVM 雷同的能力和权限。当大量本地办法呈现时,势必会减弱 JVM 对系统的控制力,因为它的出错信息都比拟黑盒。对内存不足的状况,本地办法栈还是会抛出 nativeheapOutOfMemory。
JNI 类本地办法最驰名的应该是 System.currentTimeMillis(),JNI 使 Java 深度应用操作系统的个性性能,复用非 Java 代码。然而在我的项目过程中,如果大量应用其余语言来实现 JNI , 就会丢失跨平台个性。
Java 堆
对于大多数利用来说,Java 堆(Java Heap)是 Java 虚拟机所治理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创立。此内存区域的惟一目标就是寄存对象实例,简直所有的对象实例都在这里分配内存。
堆是垃圾收集器治理的次要区域,因而很多时候也被称做“GC 堆”(Garbage Collected Heap)。从内存回收的角度来看,因为当初收集器根本都采纳分代收集算法,所以 Java 堆中还能够细分为:新生代和老年代;再粗疏一点的有 Eden 空间、From Survivor 空间、To Survivor 空间等。从内存调配的角度来看,线程共享的 Java 堆中可能划分出多个线程公有的调配缓冲区(Thread Local Allocation Buffer,TLAB)。
Java 堆能够处于物理上不间断的内存空间中,只有逻辑上是间断的即可,以后支流的虚拟机都是依照可扩大来实现的(通过 -Xmx 和 -Xms 管制)。如果在堆中没有内存实现实例调配,并且堆也无奈再扩大时,将会抛出 OutOfMemoryError 异样。
办法区
办法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、动态变量、即时编译器编译后的代码等数据。尽管
Java 虚拟机标准把办法区形容为堆的一个逻辑局部,然而它却有一个别名叫做 Non-Heap(非堆),目标应该是与 Java 堆辨别开来。
Java 虚拟机标准对办法区的限度十分宽松,除了和 Java 堆一样不须要间断的内存和能够抉择固定大小或者可扩大外,还能够抉择不实现垃圾收集。垃圾收集行为在这个区域是比拟少呈现的,其内存回收指标次要是针对常量池的回收和对类型的卸载。当办法区无奈满足内存调配需要时,将抛出 OutOfMemoryError 异样。
JDK8 之前,Hotspot 中办法区的实现是永恒代(Perm),JDK8 开始应用元空间(Metaspace),以前永恒代所有内容的字符串常量移至堆内存,其余内容移至元空间,元空间间接在本地内存调配。
为什么要应用元空间取代永恒代的实现?
字符串存在永恒代中,容易呈现性能问题和内存溢出。
类及办法的信息等比拟难确定其大小,因而对于永恒代的大小指定比拟艰难,太小容易呈现永恒代溢出,太大则容易导致老年代溢出。
永恒代会为 GC 带来不必要的复杂度,并且回收效率偏低。
将 HotSpot 与 JRockit 合二为一。
运行时常量池
运行时常量池(Runtime Constant Pool)是办法区的一部分。Class 文件中除了有类的版本、字段、办法、接口等形容信息外,还有一项信息是常量池(Constant Pool Table),用于寄存编译期生成的各种字面量和符号援用,这部分内容将在类加载后进入办法区的运行时常量池中寄存。
一般来说,除了保留 Class 文件中形容的符号援用外,还会把翻译进去的间接援用也存储在运行时常量池中。
运行时常量池绝对于 Class 文件常量池的另外一个重要特色是具备动态性,Java 语言并不要求常量肯定只有编译期能力产生,也就是并非预置入 Class 文件中常量池的内容能力进入办法区运行时常量池,运行期间也可能将新的常量放入池中,这种个性被开发人员利用得比拟多的便是 String 类的 intern() 办法。
既然运行时常量池是办法区的一部分,天然受到办法区内存的限度,当常量池无奈再申请到内存时会抛出 OutOfMemoryError 异样。
间接内存
间接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机标准中定义的内存区域。
在 JDK 1.4 中新退出了 NIO,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 形式,它能够应用 Native 函数库间接调配堆外内存,而后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的援用进行操作。这样能在一些场景中显著进步性能,因为防止了在 Java 堆和 Native 堆中来回复制数据。
显然,本机间接内存的调配不会受到 Java 堆大小的限度,然而,既然是内存,必定还是会受到本机总内存(包含 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限度。服务器管理员在配置虚拟机参数时,会依据理论内存设置 -Xmx 等参数信息,但常常疏忽间接内存,使得各个内存区域总和大于物理内存限度(包含物理的和操作系统级的限度),从而导致动静扩大时呈现 OutOfMemoryError 异样。
Java 内存模型
Java 内存模型是共享内存的并发模型,线程之间次要通过读 - 写共享变量(堆内存中的实例域,动态域和数组元素)来实现隐式通信。
Java 内存模型(JMM)管制 Java 线程之间的通信,决定一个线程对共享变量的写入何时对另一个线程可见。
计算机高速缓存和缓存一致性
计算机在高速的 CPU 和绝对低速的存储设备之间应用高速缓存,作为内存和处理器之间的缓冲。将运算须要应用到的数据复制到缓存中,让运算能疾速运行,当运算完结后再从缓存同步回内存之中。
在多处理器的零碎中(或者单处理器多核的零碎),每个处理器内核都有本人的高速缓存,它们有共享同一主内存(Main Memory)。
当多个处理器的运算工作都波及同一块主内存区域时,将可能导致各自的缓存数据不统一。
为此,须要各个处理器拜访缓存时都遵循一些协定,在读写时要依据协定进行操作,来保护缓存的一致性。
JVM 主内存与工作内存
Java 内存模型的次要指标是定义程序中各个变量的拜访规定,即在虚拟机中将变量(线程共享的变量)存储到内存和从内存中取出变量这样底层细节。
Java 内存模型中规定了所有的变量都存储在主内存中,每条线程还有本人的工作内存,线程对变量的所有操作都必须在工作内存中进行,而不能间接读写主内存中的变量。
这里的工作内存是 JMM 的一个抽象概念,也叫本地内存,其存储了该线程以读 / 写共享变量的正本。
就像每个处理器内核领有公有的高速缓存,JMM 中每个线程领有公有的本地内存。
不同线程之间无奈间接拜访对方工作内存中的变量,线程间的通信个别有两种形式进行,一是通过消息传递,二是共享内存。Java 线程间的通信采纳的是共享内存形式,线程、主内存和工作内存的交互关系如下图所示:
这里所讲的主内存、工作内存与 Java 内存区域中的 Java 堆、栈、办法区等并不是同一个档次的内存划分,这两者基本上是没有关系的,如果两者肯定要勉强对应起来,那从变量、主内存、工作内存的定义来看,主内存次要对应于 Java 堆中的对象实例数据局部,而工作内存则对应于虚拟机栈中的局部区域。
重排序和 happens-before 规定
在执行程序时为了进步性能,编译器和处理器经常会对指令做重排序。重排序分三种类型:
编译器优化的重排序。编译器在不扭转单线程程序语义的前提下,能够重新安排语句的执行程序。
指令级并行的重排序。古代处理器采纳了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器能够扭转语句对应机器指令的执行程序。
内存零碎的重排序。因为处理器应用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从 java 源代码到最终理论执行的指令序列,会别离经验上面三种重排序:
JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供统一的内存可见性保障。
java 编译器禁止处理器重排序是通过在生成指令序列的适当地位会插入内存屏障(重排序时不能把前面的指令重排序到内存屏障之前的地位)指令来实现的。
happens-before
从 JDK5 开始,java 内存模型提出了 happens-before 的概念,通过这个概念来论述操作之间的内存可见性。
如果一个操作执行的后果须要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既能够是在一个线程之内,也能够是在不同线程之间。
这里的“可见性”是指当一条线程批改了这个变量的值,新值对于其余线程来说是能够立刻得悉的。
如果 A happens-before B,那么 Java 内存模型将向程序员保障—— A 操作的后果将对 B 可见,且 A 的执行程序排在 B 之前。
重要的 happens-before 规定如下:
程序程序规定:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
监视器锁规定:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
volatile 变量规定:对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。
传递性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C。
下图是 happens-before 与 JMM 的关系
volatile 关键字
volatile 能够说是 JVM 提供的最轻量级的同步机制,当一个变量定义为 volatile 之后,它将具备两种个性:
保障此变量对所有线程的可见性。而一般变量不能做到这一点,一般变量的值在线程间传递均须要通过主内存来实现。
留神,volatile 尽管保障了可见性,然而 Java 外面的运算并非原子操作,导致 volatile 变量的运算在并发下一样是不平安的。而 synchronized 关键字则是由“一个变量在同一个时刻只容许一条线程对其进行 lock 操作”这条规定取得线程平安的。
禁止指令重排序优化。一般的变量仅仅会保障在该办法的执行过程中所有依赖赋值后果的中央都能获取到正确的后果,而不能保障变量赋值操作的程序与程序代码中的执行程序统一。
最初,举荐与感激:
深刻了解 Java 虚拟机(第 2 版)
码出高效:Java 开发手册
Java 内存模型原理,你真的了解吗?
深刻了解 Java 内存模型