【Java】JVM知识梳理之一_JVM运行时内存区域与Java内存模型
本文就JVM运行时内存区域和Java内存模型进行一些简单的梳理。
一、JVM运行时内存区域
Java虚拟机在执行Java程序时,会将分配给JVM的内存划分为几个不同的区域。有些区域在JVM启动之后就存在,直到关闭JVM进程;有些区域则依赖于用户线程,随着用户线程的生命周期一同创建和销毁。
按照《Java虚拟机规范》的对应对JVM运行时内存区域的划分,以及Java8前后HotSpot虚拟机对该规范的具体实现,可以参考下图:
接下来我们对每个区域略作介绍。
1.1 程序计数器
程序计数器(Program Counter Register)是对当前线程执行的字节码的行号指示器。程序计数器占用的内存空间很小,它是线程私有的。当字节码(class)被执行时,线程通过自己的程序计数器来选取下一条字节码指令。程序控制流(分支,循环,异常处理等等)和线程切换时的线程上下文恢复都需要依赖这个计数器。
1.2 虚拟机栈
虚拟机栈就是我们平常讲JVM内存堆栈中的栈,它也是线程私有的,每个虚拟机栈的生命周期都与一个线程相同。虚拟机栈是线程用来执行方法的内存区域,后入先出(Last In First Out,LIFO)。如下图所示:
一个线程在执行方法时,每调用一个方法,就是将该方法作为栈帧
压入自己的虚拟机栈;方法里调用另一个方法,就是将另一个方法的栈帧再压入虚拟机栈;线程当前执行的方法就是栈顶帧。
每个栈帧对应一个方法,其内部包括以下内容:
- 局部变量表,对应方法参数与局部变量,其类型是Java的8种基本数据类型加上对象引用。注意是对象的引用,不是对象本身。
- 操作栈,线程执行方法内部字节码操作指令时使用的后入先出栈,各种指令会往操作栈中写入和提取信息。Java虚拟机的解释执行引擎被称为“基于栈的执行引擎”,里面的“栈”就是操作栈。
- 动态连接,每个栈帧都包含一个指向
运行时常量池
中该栈帧所属方法的引用,栈帧持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking),即,调用一个方法是通过该引用找到运行时常量池
中的方法信息的。 - 方法返回地址,方法执行结束,不管是正常退出还是异常退出,都需要返回到该方法被调用的位置。
1.3 本地方法栈
本地方法栈与虚拟机栈类似,不同的是,虚拟机栈是用来执行java方法(class字节码)的,而本地方法栈是用来执行本地方法(native)的。所谓本地方法即JVM进程所在机器的OS的本地函数库,例如linux的.so
或windows的.dll
这些可执行类库中的方法。Java语法上,使用过JNI
调用这些native接口的。
1.4 堆区
Java堆是JVM内存中最大的一块区域,JVM几乎所有的对象实例都在堆里分配内存并创建。堆内部区域的划分取决于JVM的垃圾回收策略,即GC策略。目前主流的GC策略大部分是基于分代收集算法的,如 parNew+CMS,或者G1等等。因此我们可以将Java堆再划分为新生代
,老年代
。如下图所示:
分代收集算法大致过程:
- JVM新创建的对象会放在
eden
区域。 - 当
eden
区域快满时,触发Minor GC
新生代GC,通过可达性分析将失去引用的对象销毁,剩下的对象移动到幸存者区S1
,并清空eden
区域,此时S2
是空的。 - 当
eden
区域又快满时,再次触发Minor GC
,对eden
和S1
的对象进行可达性分析,销毁失去引用的对象,同时将剩下的对象全部移动到另一个幸存者区S2
,并清空eden
和S1
。 - 每次
eden
快满时,重复上述第3步,触发Minor GC
,将幸存者在S1
与S2
之间来回倒腾。 - 在历次
Minor GC
中一直存活下来的幸存者,或者太大了会导致新生代频繁Minor GC
的对象,或者Minor GC
时幸存者对象太多导致S1
或S2
放不下了,那么这些对象就会被放到老年代。 - 老年代的对象越来越多,最终会触发
Full GC
,也叫Major GC
,对老年代的对象进行清理。通常Full GC
会或多或少导致STW
,暂停GC以外的所有线程,因此频繁的Full GC
会严重影响JVM性能。
1.5 方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。要注意的是,《Java虚拟机规范》中的方法区是一个逻辑上的区域,不同的JVM对它都有不同的实现。另外,它有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。
在Java8之前,HotSpot虚拟机将方法区实现为永久代
,能够通过分代收集的GC来管理其内存区域。但这种设计导致Java应用经常遇到内存溢出问题,很多JVM都需要在启动时添加参数-XX:MaxPermSize
来调整永久代
的大小。因此在Java7的时候,就先将方法区中的字符串常量池
,静态变量等转移到了Java堆中;而到了Java8,就直接移除了永久代
,将其中剩下的内容如类的元信息,方法元信息,class常量池,运行时常量池等移动到了一个新的区域Metaspace
元数据区,将JIT即时编译的代码缓存放到了CodeCache
区域。
不管是Java8之前的永久代
,还是Java8以后的元数据区
与CodeCache
,还是Java7以后堆中的字符串常量池
,它们在逻辑上都属于方法区
。只是不同JVM在不同版本中的具体实现不一样罢了。
1.6 内存区域异常类型
JVM内存的异常有两种,分别是内存溢出和栈溢出。
- 内存溢出是
OutOfMemoryError
,一般对应线程共享区域如堆和元数据区。当内存不足以分配对象空间,而堆或方法区又无法扩展时,就会抛出该异常。比如对应堆区的OutOfMemoryError: Java heap space
,对应元数据区的OutOfMemoryError: Metaspace
。如果Java虚拟机栈容量可以动态扩展 ,当栈扩展时无法申请到足够的内存也会抛出OutOfMemoryError
。 - 栈溢出是
StackOverflowError
,对应虚拟机栈和本地方法栈,当线程请求的栈深度大于虚拟机所允许的深度时就会抛出该异常。
二、Java内存模型
第一章讲的是JVM运行时的内存区域划分。而JVM还有一个重要的内存模型的概念,名字有点像,但其实讲的是并发环境下,JVM内存中的共享变量的访问规范。它叫JMM
,Java内存模型。
该模型仅针对并发环境下,多个线程之间共享变量的场景。例如class的成员变量,在多线程环境下,不同的线程改变该成员变量的值时,如何在线程之间控制和通信。JMM本质上是一个缓存一致性协议。它的目的,是为了在多CPU核环境下,在尽量利用硬件提高计算性能的同时,保证缓存一致性。
2.1 硬件的效率与一致性
现代计算机执行计算任务时总是尽量让多个cpu尽量并行计算,但计算任务并非只有cpu就行,它总是需要读写内存数据;但内存IO的速度和CPU计算的速度之间有几个数量级的差距,因此现代CPU设计了多层高速缓存,让数据尽量离CPU更近一点。但高速缓存引入了一个新的问题,那就是缓存一致性。多个CPU分别使用自己的高速缓存进行读写后,需要将数据写回内存,如果各自不一致怎么办?以谁为准?为了解决这个问题,就需要在高速缓存和内存之间使用统一的规则来进行数据同步,这就是缓存一致性协议。
2.2 Java内存模型
JMM本身的设计如下:
JMM规范将共享变量所在内存划分为主内存
与工作内存
。主内存为各线程共享,工作内存为各线程私有。当线程操作共享变量时,它需要将共享变量从主内存复制一份到工作内存中,在工作内存中修改之后再写回主内存。线程只能直接修改工作内存中的变量副本,变量副本与主存之间的读取和写入都是由实现了JMM规范的某种机制实现。JMM提供了4种操作来完成主存和工作内存之间的变量同步机制,分别是read
,write
,lock
和unlock
。这里不作细述。
2.3 JMM与JVM运行时内存区域
JMM和JVM运行时内存区域其实没啥关系,但主存和工作内存的划分,容易和JVM运行时的各种内存区域产生联想,导致概念上的混淆。
周志明先生的《深入理解Java虚拟机》一书中有下面的叙述:
主内存对应Java堆中的对象实例这一点,我没有什么疑义。但对于工作内存部分,我还有些疑惑:
- 如果工作内存对应虚拟机栈的部分区域的话,那是不是说明虚拟机栈有可能是被分配到CPU高速缓存里?也就是说JVM运行时内存区域不仅仅是RAM内存,还包括CPU高速缓存?
- 如果虚拟机栈只存在于RAM内存中而不会被分配到CPU高速缓存中的话,那么JMM中的工作内存在实际的JVM中到底是如何实现的?
以上是 【Java】JVM知识梳理之一_JVM运行时内存区域与Java内存模型 的全部内容, 来源链接: utcz.com/a/92314.html