本文就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性能。
Java8之前有一个永恒代
,也会被分代收集算法的GC治理。但永恒代
严格来说是不属于Java堆区域的,它实际上是对办法区
的一种实现。上面的章节会对该概念进行阐明。
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在不同版本中的具体实现不一样罢了。
这里提到的各种常量池,如字符串常量池
,class常量池
,运行时常量池
将在当前的文章中进一步梳理。
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
。这里不作细述。
一开始是8种,起初出于升高了解难度和严谨性的思考,降为4种。利用这四种操作,JMM可能实现多个线程间对共享变量操作的原子性,可见性和有序性。
2.3 JMM与JVM运行时内存区域
JMM和JVM运行时内存区域其实没啥关系,但主存和工作内存的划分,容易和JVM运行时的各种内存区域产生联想,导致概念上的混同。
周志明学生的《深刻了解Java虚拟机》一书中有上面的叙述:
如果两者肯定要勉强对应起来,那么从变量、主内存、工作内存的定义来看,主内存次要对应于Java堆中的对象实例数据局部 ,而工作内存则对应于虚拟机栈中的局部区域。
主内存对应Java堆中的对象实例这一点,我没有什么疑义。但对于工作内存局部,我还有些纳闷:
- 如果工作内存对应虚拟机栈的局部区域的话,那是不是阐明虚拟机栈有可能是被调配到CPU高速缓存里?也就是说JVM运行时内存区域不仅仅是RAM内存,还包含CPU高速缓存?
- 如果虚拟机栈只存在于RAM内存中而不会被调配到CPU高速缓存中的话,那么JMM中的工作内存在理论的JVM中到底是如何实现的?