JVM 内存区域介绍

咕咚 于 2017-04-18 发布

作者博客地址: http://gudong.site

本文博客地址: http://gudong.site/2017/04/18/jvm_memery_area.html

Java 内存模型

Java 内存模型定义了线程跟主内存之间的抽象关系:

线程之间的共享变量存储在主内存,每个线程都有一个私有的本地内存,本地内存中存储的是该线程读写共享变量的副本。

本地内存是一个抽象概念,并不真实存在。

from 全面理解 Java 内存模型_Java_Heaven Wang 的专栏 - CSDN 博客

汇编语言:了解寄存器与内存模型 - 地铁程序员 - 博客园

多线程内部运行机制

硬件模型

计算机内所有的计算都发生在 CPU 中,这是计算机专门用来做计算的地方,这里只做计算,不适合做存储,要计算的数据都存储在内存 RAM 这个地方,也就是 RAM 存储数据,CPU 计算数据,计算完毕然后同步到内存。

但是 CPU 直接操作 RAM 的效率并不高,这里如果有一个缓存,CPU 计算时直接使用缓存就会快很多,除此之外,这样的高速内存依旧不够快,所以还为 CPU 设计了寄存器,它的存取速度接近于 CPU 的操作速度。

这样的模型在单线程下很好的处理了 CPU 与 RAM 之间的存储效率之间的矛盾,是一种非常好的解决方案。

但是在多线程的情况下,就会存在问题。两个线程 A、B,A 线程更改了变量值后,其实是在自己的缓存中更改的,并不是直接更新到 RAM 区,所以 A 线程对共享变量的更改是对副本的更改,并不能及时的同步到线程 B,如果线程 B 也操作的同样的变量,就可能出现多线程同步问题。

它保证了被包含的代码在执行时是原子的,只有代码执行完成,工作内存跟主内存已经同步完成,下一个线程才能执行代码,这样就解决了同步的问题,与此同时,程序的效率下降了,因为失去了 CPU 高速内存的设计。

volatile

如果一个变量是 volatile 修饰的,JMM 会在写入这个字段之后插进一个 Write-Barrier 指令,并在读这个字段之前插入一个 Read-Barrier 指令。这意味着,如果写入一个 volatile 变量,就可以保证:

https://blog.csdn.net/suifeng3051/article/details/52611310


Java 虚拟机在执行 Java 程序的过程中,会把它所管理的内存划分为5个不同的数据区域。这些区域都有各自的用途,以及创建、销毁的时间,有的区域空间随着虚拟机进程的启动而分配,有些区域则是依赖用户线程,他们随着线程的启动和结束而建立和销毁。

Java 虚拟机管理的内存包括以下几个运行时数据区域,如图所示 {D431DF9A-9278-D045-49B5-F6AD12BC9FE9}.png

可以看到有五部分组成,下面分别介绍

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

Java 虚拟机栈

Java虚拟机栈 与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

以前经常说 Java 内存分为堆内存和栈内存,这其实是一种不严谨的说法,这里说的堆内存没问题,但是栈内存,其实就是这里的 Java 虚拟机栈,或者说是虚拟机栈中的局部变量表。

局部变量表

局部变量表应该是一个典型的栈,他存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。

怎么理解呢?比如现在正在执行一个方法,方法中定义了各种变量,

void test(){
  int age = 0;
  String name = "mao"
  Person person = new Person(age, name);
}

这时方法 test 在虚拟机中执行时,age name 这种基本数据类型会直接存放在局部变量表中,而 person 这种引用类型变量只会在局部变量表中存放一个引用指针,具体 person 的内存分配并不在局部变量表中分配,而是在后面讲到的 Java 堆中分配。

JVM 规范中规定了 Java 虚拟机栈 可能会发生两种异常:当线程请求的栈深度大于虚拟机所允许的深度,则抛出 StackOverflowError; 如果虚拟机可以动态扩展,扩展到到无法申请更多的内存会抛出 OOM 。

本地方法栈

关于本地方法栈,这里可以拿上面刚讲到的虚拟机栈做一下对比。 虚拟机栈为虚拟机执行 Java 方法而服务,本地方法栈则是为虚拟机使用到的 Native 方法服务。其余跟 Java 虚拟机栈并没有什么特别的区别。所以对应的异常抛出机制,跟虚拟机栈一致。

Java 堆

至此你会发现,上面说到的三个区域:程序计数器,Java 虚拟机栈、本地方法栈都是线程私有。

但是跟上面三种不一样,Java 堆是所有线程共享的一片区域,它随着一个 Java 虚拟机实例的创建而分配好。该区域存在的唯一目的就是存放对象,几乎应用中所有的对象实例都在这里分配内存(非绝对)。

因为上面三种内存空间为线程私有,他们随线程生而生,随线程死而死,所以这三种空间一般不需要执行垃圾回收,所以我们我们通常说的垃圾回收大多都发生在 Java 堆。

从内存回收角度讲,现在的垃圾收集器大都采用分代收集法。所以根据此可以把 Java 堆分为新生代和老年代

对新生代又可以细分为 Eden 空间、From Survivor 空间、To Survivor 空间。

根据 Java 虚拟机规范,Java 堆可以处与物理上不连续的内存空间中。只要逻辑连续即可。一般的 Java 堆都被设计成可扩展的。如果堆中没有内存分配实例,并且堆也无法扩展时,将会抛出 OutOfMemmoryError.

方法区

与Java堆一样,方法区(Method Area)是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

Java 虚拟机规范规定,该区域可以不实现垃圾回收。因为相对而言,这个区域垃圾回收行为比较少见,但是并非数据进入方法区就一直不回收了。

这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

运行时常量池

运行时常量池是方法区的一部分。

这里先说说一般的 class 文件,即一个 .java 文件编译后成为字节码文件 .class . Class 文件中除了有类的版本,字段,方法,接口,等描述信息,还有一项信息是常量池。

Class 文件中的常量池用于存放编译期生成的各种字面量和符号引用。这部分内容将在类加载后存放在方法区的运行时常量池。

这里对 Class 文件常量池和 JVM 运行时常量池 进行一下区别。

这俩是两个不同的概念,前者其实是一个物理上的概念,当我们的 .java 文件编译成 .class 文件后,该区域已经在 .class 文件中占据了固定的位置。这一点你可以通过查看具体的 .class 文件得到。而当这个 class 文件加载到 JVM 中后,Class 文件常量池中的信息将会全部存放在 JVM 方法区的运行时常量池。所以前者是实实在在存在的,而后者是一个 JVM 中的内存区域,两者存在一个简单的对应关系。

运行时常量池相对于 Class 文件常量池的一个重要特征就是前者具备动态性,运行时常量池可以在程序运行期间动态的把新的常量放入运行时常量池。

参考

JVM 垃圾回收