深入理解Java虚拟机(一)-Java内存区域

Java内存模型

由于计算机上的内存模型涉及到物理的主内存、高速缓存和寄存器等。这些不同的计算机不同的操场系统可能会存在差异,Java虚拟机规范中试图定义一种Java内存模型,来屏蔽掉各种硬件和操作系统的内存访问差异,让Java程序在各个平台下都能达到一致的访问效果。

主内存与工作内存

Java内存模型规定了所有变量都存储在主内存内(主内存包括方法区和堆),此处主内存隶属于Java虚拟机内存的一部分,而虚拟机内存是操作系统分配的。每条Java线程还有自己的工作内存,工作内存中保存了被该线程使用到的变量的主内存的副本,线程对变量的所有操作都在工作内存中进行,Java线程之间的变量值传递都通过主内存来完成。

1.png

内存间的交互

关于主内存和工作内存间的交互协议,即一个变量如何从主内存拷贝到工作内存、又是如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了8种操作,这8种操作实现时必须保证每一种操作都是原子的、不可再分的,其中前4条是作用于主内存,后4条作用于工作内存:

  • lock锁定:将一个变量标识为线程独占状态
  • unlock解锁:将锁定状态的变量解除锁定,释放后的变量才可以被其他变量锁定
  • read读取:将变量从主内存传输到线程的工作内存中,待之后的load加载
  • write写入:把store操作从工作内存中得到的变量值写入主内存的变量中
  • load加载:将read后从主内存得到的变量值加载到工作内存的变量副本中
  • use使用:把工作内存中的一个变量值传递给字节码执行引擎,等待字节码指令使用
  • assign赋值:把一个从执行引擎接收到的值赋值给工作内存的变量
  • store存储:把工作内存中一个变量的值传送到主内存中,以便随后的write使用

Java内存区域

运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分位若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。其中程序计数器、JVM栈、本地方法栈是线程私有的,而方法区和堆是所有线程共享的。

程序计数器

它是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器:如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行Native方法,这个计数器值为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机栈

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

  • 局部变量表:局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表所需的内存空间在编译期间完成分配。

  • 动态链接:动态链接是在运行时将符号引用解析为直接引用的过程。

  • 操作数:参与运算的常量或者变量称为操作数。

在Java 虚拟机规范中,对这个区域规定了两种一场状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StarkOverFlowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:

1
java -Xss512M HackTheJava

本地方法栈

本地方法栈与Java虚拟机栈类似,它们的区别只不过是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为本地方法服务。该区域可能抛出的异常与Java虚拟机栈一样。

Java堆

Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。Java 堆还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。

1
2
3
一般情况下,新创建的对象都会存放到新生代中。

在新生代每进行一次垃圾收集后,就会给存活的对象“加1岁”,当年龄达到一定数量的时候就会进入老年代,另外,比较大的对象也会进入老年代。

Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,如果堆中没有内存完成实例分配且堆也无法再扩展时,将抛出OutOfMemoryError异常。

方法区

方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。

对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。

HotSpot 虚拟机把它当成永久代来进行垃圾回收。但很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。

运行时常量池

运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,将用于存放编译期生成的各种字面量和符号饮用,这部分内容将在类加载后进入方法区的运行时常量池中存放

除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。

当常量池无法申请到内存时会抛出OutOfMemoryError异常。

直接内存

在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。

控制参数汇总

可以通过如下参数来控制各区域的内存大小:

1
2
3
4
5
6
7
8
9
-Xms设置堆的最小空间大小

-Xmx设置堆的最大空间大小

-XX:NewSize设置新生代最小空间大小

-XX:MaxNewSize设置新生代最大空间大小

-Xss设置每个线程的堆栈大小

参考资料