乐趣区

关于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)。如下图所示:

一个线程在执行办法时,每调用一个办法,就是将该办法作为 栈帧 压入本人的虚拟机栈;办法里调用另一个办法,就是将另一个办法的栈帧再压入虚拟机栈;线程以后执行的办法就是栈顶帧。

每个栈帧对应一个办法,其外部包含以下内容:

  1. 局部变量表,对应办法参数与局部变量,其类型是 Java 的 8 种根本数据类型加上对象援用。留神是对象的援用,不是对象自身。
  2. 操作栈,线程执行办法外部字节码操作指令时应用的后入先出栈,各种指令会往操作栈中写入和提取信息。Java 虚拟机的解释执行引擎被称为“基于栈的执行引擎”, 外面的“栈”就是操作栈。
  3. 动静连贯,每个栈帧都蕴含一个指向 运行时常量池 中该栈帧所属办法的援用,栈帧持有这个援用是为了反对办法调用过程中的动静连贯 (Dynamic Linking),即,调用一个办法是通过该援用找到 运行时常量池 中的办法信息的。
  4. 办法返回地址,办法执行完结,不论是失常退出还是异样退出,都须要返回到该办法被调用的地位。

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 堆再划分为 新生代 老年代。如下图所示:

分代收集算法大抵过程:

  1. JVM 新创建的对象会放在 eden 区域。
  2. eden 区域快满时,触发 Minor GC 新生代 GC,通过可达性剖析将失去援用的对象销毁,剩下的对象挪动到幸存者区 S1,并清空eden 区域,此时 S2 是空的。
  3. eden 区域又快满时,再次触发 Minor GC,对edenS1的对象进行可达性剖析,销毁失去援用的对象,同时将剩下的对象全副挪动到另一个幸存者区 S2,并清空edenS1
  4. 每次 eden 快满时,反复上述第 3 步,触发 Minor GC,将幸存者在S1S2之间来回倒腾。
  5. 在历次 Minor GC 中始终存活下来的幸存者,或者太大了会导致新生代频繁 Minor GC 的对象,或者 Minor GC 时幸存者对象太多导致 S1S2放不下了,那么这些对象就会被放到老年代。
  6. 老年代的对象越来越多,最终会触发 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 种操作来实现主存和工作内存之间的变量同步机制,别离是readwritelockunlock。这里不作细述。

一开始是 8 种,起初出于升高了解难度和严谨性的思考,降为 4 种。利用这四种操作,JMM 可能实现多个线程间对共享变量操作的原子性,可见性和有序性。

2.3 JMM 与 JVM 运行时内存区域

JMM 和 JVM 运行时内存区域其实没啥关系,但主存和工作内存的划分,容易和 JVM 运行时的各种内存区域产生联想,导致概念上的混同。

周志明学生的《深刻了解 Java 虚拟机》一书中有上面的叙述:

如果两者肯定要勉强对应起来, 那么从变量、主内存、工作内存的定义来看, 主内存次要对应于 Java 堆中的对象实例数据局部 , 而工作内存则对应于虚拟机栈中的局部区域。

主内存对应 Java 堆中的对象实例这一点,我没有什么疑义。但对于工作内存局部,我还有些纳闷:

  1. 如果工作内存对应虚拟机栈的局部区域的话,那是不是阐明虚拟机栈有可能是被调配到 CPU 高速缓存里?也就是说 JVM 运行时内存区域不仅仅是 RAM 内存,还包含 CPU 高速缓存?
  2. 如果虚拟机栈只存在于 RAM 内存中而不会被调配到 CPU 高速缓存中的话,那么 JMM 中的工作内存在理论的 JVM 中到底是如何实现的?
退出移动版