JVM内存结构

JVM内存结构概述

在开发过程中经常会遇到的堆内存的概念具体是什么?常量池在JVM中的内存区域是怎么管理的?遇到OutOfMemory异常应该如何处理?

平时对上述问题可能只有一个模糊的概念,本文将会详细地介绍JVM的内存结构,让上面问题的答案变得清晰明了。

JVM的内存结构主要分为堆内存、方法区和栈三大块。而堆内存又分为新生代和老生代。方法区存储的信息为常量、类信息和静态变量等。栈分为程序计数器、JVM方法栈和本地方法栈。堆和方法区是线程共享的内存区域,而栈是线程的私有内存区域。

JVM内存参数

这里盗用一张图来展示JVM的内存结构和参数信息。

-Xms:堆的最小占用空间
-Xmx:堆的最大占用空间
-XX:NewSize:新生代的最小占用空间
-XX:MaxNewSize:新生代的最大占用空间
-XX:PermSize:永久代(方法区)的最小占用空间
-XX:MaxPermSize:永久代(方法区)的最大占用空间
-Xss:每个线程的栈的占用空间

堆空间

堆是JVM管理的最大的一块内存区域,几乎所有的对象在创建的时候,都会在这里进行内存分配。平时我们所说的垃圾回收的工作就是在这里进行的,常用的垃圾回收算法都是基于分代回收的算法,因此堆空间可以分为新生代和老生代,新生代还可以分为Eden区、From Survive区和To Survive区。

堆可以处于物理不连续的内存空间上,和磁盘的管理一样,只需要保证逻辑上的连续就可以。并且堆的大小是可扩展的,以保证操作系统内存分配的灵活性。

新生代

新生代是所有对象被初始化分配内存的地方,当有对象不再被引用的时候,Minor GC线程就会来把这一块内存回收掉。

老生代

当有对象在经历了几次Minor GC之后仍然存活,就会被移动到老生代中。或者虚拟机在分配占用内存较大的对象的时候,会直接在老生代中分配内存。Full GC会清理并回收老生代的内存。

方法区

方法区用于存储JVM的类信息、编译后的代码、常量和静态变量等数据。方法区和堆空间一样,都是内存共享的区域,也同样不需要连续的内存,大小也可扩展。

方法区的特殊性在于,用户可以选择不在该区域进行垃圾回收,因此该区域也可被称为永久代。但通常来说,该区域的垃圾回收仍然是必要的,只是如何提升该区域的垃圾回收效果至今仍是一个较为困难的问题。

常量池

在class文件被加载到JVM中时,基础数据类型的常量会被加载到运行时常量池中。

静态数据池

静态数据池包括静态的变量和方法,以及静态的代码块。

编译后的代码池

这里保存着class文件的内容。

本地内存区

本地内存区由程序计数器、虚拟机栈和本地方法栈组成。这块区域是线程私有的。

程序计数器

顾名思义,程序计数器是程序运行的行号指示器。线程在执行时的逻辑循环、分支与跳转都通过程序计数器所标记的数据来执行。在多线程工作时,每个线程都有自己独立的程序计数器,保证一个CPU在同一时刻只会执行一个线程中的一个指令。

如果线程执行的是Java方法,程序计数器记录的是编译后的虚拟机字节码指令的地址,而如果执行的是Native的方法,则计数器记录空值。

这是JVM中唯一不会抛出OutOfMemory异常的区域。

虚拟机栈

与程序计数器相同,虚拟机栈也是线程私有的,生命周期与线程相同。虚拟机栈存储的是线程中的方法的局部变量、方法出入口等信息。

虚拟机栈会抛出两种异常。当栈的深度超出JVM所规定的深度时,会抛出StackOverFlow异常。当请求栈的大小超出JVM所规定的内存大小时,会抛出OutOfMemory异常。

本地方法栈

本地方法栈与虚拟机栈基本是类似的,只是本地方法栈存储的是Native方法的局部变量和方法信息。

堆外内存

除了JVM所管理的内存区域之外,JVM也可以访问操作系统直接管理的内存区域,这被称为堆外内存或者直接内存。使用堆外内存的好处在于,脱离了JVM的管理,不受GC线程的影响,并且在IO操作时会有更好的性能。

堆外内存可以通过java.nio.ByteBuffer的allocateDirect方法来创建。但是在不使用后需要及时回收,否则造成内存泄漏会难以排查。使用时也要注意存储的数据不要过于复杂,或者数据仍然需要参与内存计算。堆外内存性能好在于可以少拷贝一次内存,直接进行IO,如果需要参与内存计算,则还需要把数据拷贝到JVM中,增加开销。