深入理解 JVM 之 JVM 内存结构
Java
虚拟机在运行 Java
程序 时,把它所管理的内存划分为若干个不同的数据区域,主要包括以下五个部分:程序计数器、Java
堆、Java
虚拟机栈、方法区和本地方法栈。
[图片上传失败...(image-656367-1607673231265)]
JVM 内存结构
程序计数器
程序计数器是当前线程所执行的字节码的行号指示器,它会指出下一条将要执行的指令的地址,字节码解释器就是通过改变计数器的值来选取程序接下来执行的操作。
程序计数器是线程私有的一小块内存,每条线程都要有一个独立的程序计数器,以使线程切换后恢复到正确的执行位置。
- 如果线程正在执行
Java
方法,则计数器记录的是正在执行的虚拟机字节码指令的地址 - 如果执行
native
方法,则计数器为空
它也是唯一一个不会出现 OutOfMemoryError
的内存区域。
Java 虚拟机栈
与程序计数器一样,Java
虚拟机栈也是线程私有的,在线程创建时 Java
栈会被创建,每个方法在在执行的同时都会创建一个栈帧,用于存放局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至执行完成,都对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
一般所谓的“栈”,指的是虚拟机栈中局部变量表部分,其中存放了各种基本数据类型( 8
种),对象引用(reference
类型) 和 returnAddress
类型。局部变量表所需的空间在编译期就已经确定并完成分配,在方法运行期间不会被改变。
Java
虚拟栈中可能出现两种异常:
-
StackOverflowError
:线程请求的栈深度大于虚拟机所允许的深度 -
OutOfMemoryError
:虚拟机栈扩展时无法申请到足够的内存
本地方法栈
本地方法栈与 Java
虚拟机栈的作用类似,区别是 Java
虚拟机栈为虚拟机执行 Java
方法服务,而本地方法栈为虚拟机执行 Native
方法服务。有的虚拟机(例如 HotSpot
虚拟机)直接把本地方法栈和 Java
虚拟机栈合并在一起。
本地方法栈也可能会抛出 StackOverflowError
和 OutOfMemoryError
异常。
Java 堆
Java
堆是是虚拟机中最主要的内存区域。它为线程共享,在虚拟机启动时创建,几乎所有的对象实例都存储在 Java
堆中。
Java
堆也被称作 "GC"
堆。从内存回收角度看,可分为新生代和老年代。而新生代又可分为 Eden
区、From Survivor
区、To Survivor
区等。
Java
堆的实现,既可以实现为固定的,也可以是扩展的。当前虚拟机都按照可扩展来实现,通过 -Xmx
和 -Xms
控制堆大小。
如果堆中没有内存并且也无法再扩展时,会抛出 OutOfMemeoryError
异常。
方法区
方法区与 Java
堆一样,为线程共享。用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。也叫作 Non-Heap
(非堆)。
如果方法区无法满足内存分配需求,会抛出 OutOfMemoryError
异常。
运行时常量池
运行时常量池是方法区的一部分。Class
文件中的常量池用于编译期生成的各种字面量和符号引用,这部分内容在类加载后被存入运行时常量池。
动态性是运行时常量池相对于 Class
文件常量池的一个重要特征,即不要求常量一定只有编译期才能产生,运行期间也可能将新的常量放入池中。
运行时常量池受到方法区内存的限制,如果常量池无法再申请内存,就会抛出 OutOfMemoryError
异常。
直接内存
直接内存并不由 JVM
管理,它是利用 Native
函数库在 Java
堆外申请分配的内存区域,可以避免在 Java
堆和 Native
堆中复制数据以提高性能。
例如 NIO
中的 DirectByteBuffer
就可以作为这块内存的引用进行操作直接内存。
永久代与元空间
有时会看到方法区被称为永久代,其实两者有着本质的区别。方法区是 JVM
规范中的定义,而永久代是 JVM
规范的一种实现,并且只有在 HotSpot
虚拟机中如此,其他虚拟机中没有永久代的说法。
在 JDK1.6
之前,HotSpot
虚拟机把 GC
分代收集扩展至方法区,或者说使用永久代实现方法区。不过永久代有 -XX:MaxPermSize
的上限,很容易遇到内存溢出问题。
所以在 JDK1.7
中,将部分数据已经转移 Java Heap
或 Native Heap
中,例如:将原本放在永久代中的字符串池和类的静态变量移出到 Java Heap
中,将符号引用转移到 Native Heap
中。但永久代仍然存在,并没有移除。
在 JDK1.8
中,取消了永久代,代替为元空间实现,它也是 JVM
规范中方法区的一种实现。不过它与永久代最大的不同是:元空间并不在虚拟机中,而是将元空间放到本地内存中。所以默认情况下,它只受本地内存的限制,可以通过 -XX:MetaspaceSize
参数设置初始空间大小,默认没有最大空间限制。
常见的 OOM 及原因
Java
中的 OOM
指的就是 java.lang.OutOfMemoryError
异常。主要有以下几种:
java.lang.OutOfMemoryError:Java heap space
Java
堆中主要用于存放各种对象实例。当堆中没有足够的空间分配给新对象时,或者说达到了堆空间设置的最大空间限制,则会抛出此异常。
引起内存溢出的原因主要有:
- 流量访问量大,超过设置的堆空间大小;
- 内存泄露,不能被回收的对象消耗过多堆空间;
java.lang.OutOfMemoryError:Permgen space
在 JDK7
中,HotSpot
虚拟机使用永久代实现方法区,永久代较小,而且回收效率较低,很容易出现内存溢出。
因此,JDK8
取消了永久代,使用元空间来实现方法区,存放在本地内存中。
java.lang.OutOfMemoryError:Metaspace
方法区主要存储类的元信息,HotSpot
元数据区。当元空间没有足够的空间分配给加载的类时,会抛出此异常。
引起元数据区空间不足的原因主要有:
- 加载的类太多,常见于
jsp
页面过多时; - 元空间被实现在堆外,主要受到进程本身的内存限制,一般很难出现溢出。