转载

JVM内存模型(JMM)和内存区域,别再傻傻分不清楚

在面试中,经常会遇到JVM内存相关的问题。但是实际上很多情况下,大家没有把内存模型和内存区域认真区分,实际上这是两个层面的东西。

JVM内存模型(JMM)

JMM的出现是为了解决在并发编程中的两个经典问题:线程之间如何通信及线程之间如何同步。

  • 这里的线程是指并发执行的活动实体
  • 通信是指线程之间以何种机制来交换信息

通信:在命令式编程中,线程之间的通信方式有两种

  • 共享内存(共享内存模型:线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来 隐式 进行通信)
  • 消息传递(消息传递模型:线程之间没有公共状态,线程之间必须通过明确的发送消息来 显式 进行通信)

同步:同步是指程序用于控制不同线程之间操作发生 相对顺序 的机制

  • 在共享内存并发模型里,同步是显式进行的,程序员必须显式指定某个方法或某段代码需要在线程之间 互斥 执行
    • 典型的共享内存通信方式就是通过共享对象进行通信
  • 在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的
    • 在java中典型的消息传递方式就是wait()和notify()

小结一下:

通信方式 通信隐显 同步隐显 程序员角度
共享内存 隐式通信 显示同步 显示指定
消息传递 显示通信 隐式同步 透明
  • Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

Java内存模型的抽象结构

JVM内存模型(JMM)和内存区域,别再傻傻分不清楚
  • 从抽象的角度看,JMM定义了线程和主内存之间的抽象关系:
    • 线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
    • 本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
  • Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
    • JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。
  • 在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。局部变量,方法定义参数(Formal Method Parameter)和异常处理器参数(Exception Handler Parameter)不会再线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java内存区域(运行时数据区)

Java内存区域一般指的值运行时数据区。Java虚拟机在执行Java程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK 1.8对运行时数据区有调整,所以和之前的版本略有不同。

在JDK 1.8版本之前,如果你不知道JVM规范定义的运行时数据区和HotSpot JVM对应的实现的区别还问题不大,当然,大部分博客也是这么做的。但是在MetaSpace出现之后,就必须要搞清楚这两者的区别,不然很难理解区分方法区、永久带、元空间。

JVM规范定义的运行时数据区

参考: JDK8的JVM规范文档

针对文档的运行时数据区我自己翻译了一版: The Java Virtual Machine Specification, Java SE 8 Edition(Java虚拟机规范)-运行时数据区(翻译)

规范中定义了6个运行时数据区,The pc Register(程序计数器)、Java Virtual Machine Stacks(Java虚拟机栈)、Heap(堆)、Method Area(方法区)、Run-Time Constant Pool(运行时常量池)、Native Method Stacks(本地方法栈)。

  • 原文也说了 Each run-time constant pool is allocated from the Java Virtual Machine's method area. 运行时常量池是在JVM的方法区中分配的。

所以,这就是我们常看到的图:

JVM内存模型(JMM)和内存区域,别再傻傻分不清楚
  • 值得注意的是:规范中定义的这些运行时数据区都是 抽象 的概念,并没有限制具体的实现。所以具体的虚拟机实现的这几个运行时数据区不一定是相互隔离的。例如: 栈帧可能是分配在堆中的 (如:CLDC HI)。

具体的Java虚拟机中的运行时数据区(以HotSpot VM为例)

HotSpot VM简介

HotSpot VM是Sun JDK和OpenJDK中所带的虚拟机,是目前使用范围最广的Java虚拟机。

在2006年的JavaOne大会上,Sun公司宣布最终会把Java开源,并在随后的一年,陆续将JDK的各个部分(其中当然也包括了HotSpot VM)在GPL协议下公开了源码, 并在此基础上建立了OpenJDK。这样,HotSpot VM便成为了Sun JDK和OpenJDK两个实现极度接近的JDK项目的共同虚拟机。

在2008年和2009年,Oracle公司分别收购了BEA公司和Sun公司,这样Oracle就同时拥有了两款优秀的Java虚拟机:JRockit VM和HotSpot VM。 Oracle公司宣布在不久的将来会完成这两款虚拟机的整合工作,使之优势互补。(这也是JDK 1.8中对HotSpot VM运行时数据区进行大调整的一个背景之一)

HotSpot VM 结构

HotSpot VM将虚拟机栈和本地方法栈合二为一:

JVM内存模型(JMM)和内存区域,别再傻傻分不清楚
在JDK 1.8之前的版本,图上的方法区在HotSpot VM中的实现就是 PermGen (永久代),在其他JVM上不存在永久代。在JDK 1.8的时候,HotSpot VM将方法区的实现替换成了 Meatspace

(元空间)。所以在本质上,永久代和元空间都是方法区的实现,只是它们的实现细节不一样。

  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
  • 移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代
  • 移除永久代的工作从JDK1.7就开始了。JDK1.7中,存储在永久代的部分数据就已经转移到了 Java Heap 或者是 Native Heap 。譬如符号引用(Symbols)转移到了 native heap ;字面量(interned strings)转移到了 java heap ;类的静态变量(class statics)转移到了 java heap

永久代和元空间的一些区别

在JVM规范中定义: Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. 方法区逻辑上属于堆的一部分。(JDK1.7之前HotSpot VM的方法区物理上也属于了堆的一部分)

  • 永久代分配在堆中(在JVM中),元空间使用直接内存(在JVM之外)
  • 永久代溢出: java.lang.OutOfMemoryError: PermGen space ,元空间溢出: java.lang.OutOfMemoryError: Metaspace
  • 永久代GC和老年代GC是捆绑到,其中一个满了,都会触发两个区域的GC;元空间中类和其元数据的生命周期与其对应的类加载器相同,当某个类加载器不再存活,GC会把对应的空间整个回收(每个类加载器有单独的存储空间),不需要扫描压缩耗时的操作。
原文  https://juejin.im/post/5ef064fee51d4573f45c4119
正文到此结束
Loading...