目录
- 定义和说明
- JVM内存区域的定义
- 内存区域说明
- 堆说明
- 非堆-方法区说明
- 堆栈的区别
- HotSpot虚拟机
- JVM线程独占内存
- 程序计数器:Program Counter Register
- Java虚拟机栈:Java Virtual Machine Stack
- 本地方法栈:Native Method Stack
- JVM共享内存
- Java堆:Java Heap
- 方法区:Method Area
- 常量池表:Constant Pool Table
- 运行时常量池:Run-Time Constant Pool
- 参考
定义和说明
JVM内存区域的定义
JVM(Java Virtual Machine,JavaVM,Java虚拟机),定义了不同的运行时数据区域(run-time data area),一部分区域随JVM启动而创建,随JVM消亡而销毁。其它区域则属于每个线程,随线程启动而创建,随线程关闭而销毁。
内存区域说明
JavaVM的定义,决定其内存将会分为两部分:JVM共享内存、JVM线程独占内存。
- JVM共享内存分为两部分:堆(Heap)、非堆——方法区(Method Area)
- JVM线程独占内存分为:虚拟机栈(Java Virtual Machine Stacks)、本地方法栈(Native Method Stacks)、程序计数器(pc Register)
堆说明
堆就是Java代码可及的内存,是留给开发人员使用的,所有类实例和数组的内存均从此处分配。
非堆-方法区说明
- 非堆就是JVM留给自己用的
- 这部分的物理内存实际上和堆是连续的,它也是堆的一部分,只是在概念上将其与堆区分开来
- 方法区实际上只是JVM定义的一个概念,这块区域如何实现取决于具体的虚拟机实例。
- HotSpot虚拟机在JDK7之前就用永久代实现方法区的功能定义,在JDK8弃用了永久代并使用元空间(metaspace)实现方法区。
堆栈的区别
- 堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
- 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据
HotSpot虚拟机
- 大家使用的主流JDK主要是OpenJDK和OracleJDK这两个版本,他们的VM实现都是HotSpotVM,所以一般我们提到JVM都是代指HotSpotVM
- 因此下面的内存结构分析,乃至我后面的所有JVM文章,也都是基于HotSpotVM来的。
JVM线程独占内存
线程私有,生命周期与线程相同。
程序计数器:Program Counter Register
作用:又称pc Register, 是一块较小的内存空间,可以被看作当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器。
对于不同的方法程序计数器分别会记录不同的值:
- 执行普通方法:那么程序计数器就会记录当前虚拟机执行处的地址(address),当线程切换后方便线程继续不中断的执行该方法。
- 执行native方法:程序计数器的值为undefined。
此外,Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现,为了线程切换后能恢复到正确的执行位置,每个线程都会拥有一个独立的程序计数器独立存储各自的计数器值。各线程之前计数器互不影响。
Java虚拟机栈:Java Virtual Machine Stack
作用:虚拟机栈描述的是Java方法执行的线程内存模型,在物理内存上不一定是连续的。在任意一个时间点,每个JVM线程都只会执行一个方法的代码,这个方法对于该线程来说就是一个栈帧(Frame)。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
帧:
- 用于存储局部变量表、操作数栈、动态连接、方法出口等信息。它可能是对分配的。
栈的注意项:
- 虚拟机栈可以为固定大小、也允许在运行时动态扩展。如果虚拟机栈为固定大小时,它的大小将在创建时自主决定。
- 每个VM实现都会提供用户可以自行控制的栈初始化大小,在支持动态扩展时还会提供栈的最大值设置。
局部变量表:存放编译期可知的各种Java虚拟机基本数据类型、引用类型和returnAddress类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)表示。除了long和double占用两个槽外,其他都只占一个。局部变量表的内存空间在编译期就完成分配了,且在运行期不会改变;
栈溢出(虚拟机栈和本地方法栈溢出情况相似):
-
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
-
如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。
-Xss | -XX:ThreadStackSize:设置该参数以设置栈容量
本地方法栈:Native Method Stack
作用:本地方法栈与虚拟机栈的作用相似,区别在于本地方法栈是为虚拟机使用到的native方法服务;而虚拟机栈是为执行Java方法(也就是字节码)服务。
JVM共享内存
被所有线程共享的一块区域
Java堆:Java Heap
概览:Java堆是虚拟机管理的内存的最大一块,它是被所有线程共享的一块区域。它是垃圾收集器管理的内存区域,也被称为GC堆(Garbage Collected Heap)。
作用:Java堆所管理的内存区域的作用就是存放对象实例。Java几乎所有的对象实例都在这里分配内存。
堆内存:
- 堆内存大小可以是固定值,也可以根据需要动态扩展,在需要更多内存时自动扩展,并在不需要时自动缩减。
- 每个VM实现都可能会提供用户可以自行控制的堆初始化大小,并在支持动态扩展时还会提供堆的最大值设置。
- Java堆可以处于物理上不连续的内存空间中,但在逻辑上他们应该被视为连续的。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的物理内存空间。
堆溢出:当我们不断地创建对象,且大量对象被使用(GC roots到对象可达),那么随着对象数量的增加,总容量触及最大堆的容量限制后,通过full gc 也不法清理内存空间时,就会产生内存溢出异常(OutOfMemoryError)。
OOM信息:java.lang.OutOfMemoryError:Java heap space
-XX:+/-HeapDumpOnOutOfMemoryError:可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析
OOM解决:通过dump堆转存快照来分析异常
-
首先应确认内存中导致OOM的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory
Overflow)。 -
内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们,根据泄漏对象的类型信息以及它到GC Roots引用链的信息,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。
-
内存溢出,如果OOM对象都是必须存在的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
-Xmx | -XX:MaxHeapSize:设置堆最大容量
-Xms | -XX:InitalHeapSize:设置堆最小容量
方法区:Method Area
概览:方法区实际上是一种概念,与Java堆一样,都是线程共享的内存区域。虽然在逻辑上方法区是Java堆的一个逻辑部分,但是它有一个别名“非堆”Non-Heap,用以于Java堆区分开来。
作用:用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
方法区内存:
- 方法区内存大小可以是固定值,也可以根据需要动态扩展,在需要更多内存时自动扩展,并在不需要时自动缩减。
- 每个VM实现都可能会提供用户可以自行控制的方法区初始化大小,并在支持动态扩展时还会提供方法区的最大值设置。
- 方法区在内存上亦不要求连续。
注意:
- JDK7之后,HotSpot把放在永久代的字符串常量池、静态变量移至了Java堆中。
- JDK8之后,完全废弃了永久代的概念,改用元空间(Metaspace)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
- 方法区的内存空间:与Java堆一样,此外它甚至还可以选择不实现垃圾收集(这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载)。
方法区溢出:当方法区要去分配的内存超过最大内存限制时,抛出内存溢出异常(OutOfMemoryError)。
设置方法区大小:
JDK7及前:
- -XX:PermSize:设置永久代初值
- -XX:MaxPermSize:设置永久代最大值
JDK8及以后:
- -XX:MetaspaceSize:元空间初值
- -XX:MaxMetaspaceSize:元空间最大值
常量池表:Constant Pool Table
概览:它实际上是某个类、接口中的常量集合,任意一个类中,都有会无数常量(例如类名、接口名、方法名都是一个常量),在编译完成后常量池表就固定了,运行时也不会发生改变。它是属于方法区中类型信息的一部分。
作用:类型信息中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池表(Constant Pool Table)。它用于存放编译期生成的各种字面量与符号引用,这些内容将在类加载后放到方法区的运行常量池中。
运行时常量池:Run-Time Constant Pool
概览:运行时常量池实际上是类文件中一个类或接口中的常量池表(constant pool table)的集合展现,是方法区的一部分。JDK7之前常量池都是分配在永久代中,JDK7之后,HotSpot把放在永久代的字符串常量池、静态变量移至了Java堆中。
作用:与常量池表相比,运行时常量池具备动态性,运行期间也可以将新的常量放入池中,比如String的intern()方法。
运行时常量池溢出:它不像常量池表不可变动,运行时常量池在扩展时,需要分配的内存超过方法区限制时,抛出内存溢出异常(OutOfMemoryError)。
参考
深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明
Run-Time Data Areas