JVM 系列(二)内存模型

半城伤御伤魂 提交于 2020-03-18 03:49:57

02 JVM 系列(二)内存模型

一、JVM 内存区域

JVM 会将 Java 进程所管理的内存划分为若干不同的数据区域。这些区域有各自的用途、创建/销毁时间:

一、 线程私有区域

线程私有数据区域生命周期与线程相同,依赖用户线程的启动/结束而创建/销毁(在 Hotspot VM 内,每个线程都与操作系统的本地线程直接映射,因此这部分内存区域的存/否跟随本地线程的生/死)。

(1) Program Counter Register(程序计数器)

Java 虚拟机会为每个线程创建 PC 寄存器,在任意时刻,一个 java 线程总是在执行一个方法,这个方法被称为当前方法。

如果当前方法不是本地方法,PC 寄存器就会执行当前正在被执行的指令,如果是本地方法,则 PC 寄存器值为 undefined,寄存器存放如当前执行环境指针、程序计数器、操作栈指针、计算的变量指针等信息。

这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。

(2) Java Stack(虚拟机栈)

虚拟机栈描述的是 Java 方法执行的内存模型 :每个方法被执行时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法被调用至返回的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程(VM 提供了 -Xss 来指定线程的最大栈空间,该参数也直接决定了函数调用的最大深度)。

如果线程请求的栈深度大于虚拟机所允许的深度,则 StackOverflowError。如果虚拟机栈可以动态扩展,扩展到无法申请足够的内存,则 OutOfMemoryError。

(3) Native Method Stack(本地方法栈)

本地方法栈和 Java 栈非常类似,区别是 Java Stack 为执行 Java 方法服务,而 本地方法栈则为 Native 方法服务 (Java 虚拟机允许 Java 直接调用本地方法,通常使用 C 编写)。

也会抛出 StackOverflowError 和 OutOfMemoryError。

二、 线程共享区域

随虚拟机的启动/关闭而创建/销毁。

(1) Heap(Java 堆)

Java 虚拟机启动的时候建立 Java 堆,几乎所有对象实例和数组都要在堆上分配(栈上分配、标量替换除外),因此是 VM 管理的最大一块内存,也是垃圾收集器的主要活动区域。由于现代 VM 采用分代收集算法,因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代;而从内存分配的角度来看,线程共享的 Java 堆还还可以划分出多个线程私有的分配缓冲区(TLAB)。而进一步划分的目的是为了更好地回收内存和更快地分配内存。

堆可以按照可扩展来实现:-Xmx(最大内存) 和 -Xms(初始化内存)

当堆中没有内存可以分配给实例,也无法再扩展时,则抛出 OutOfMemoryError 异常。

(2) Method Area(方法区)

即我们常说的永久代(Permanent Generation),用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 HotSpot VM 把 GC 分代收集扩展至方法区,即使用 Java 堆的永久代来实现方法区,这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存,而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载,因此收益一般很小)

当方法区无法满足内存分配需求时,则抛出 OutOfMemoryError 异常。

JDK1.7 中,已经把放在永久代的字符串常量池移到堆中。JDK1.8 撤销永久代,引入元空间。

(3) 运行时常量池

是方法区的一部分,用于存放编译期生成的各种字面量和符号引用,另外运行期间也可能将新的常量放入池中,如 String 的 intern() 方法。当常量池无法再申请到内存时,则抛出 OutOfMemoryError 异常。

三、 直接内存

直接内存并不是 JVM 运行时数据区的一部分,但也会被频繁的使用:在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存,然后使用 DirectByteBuffer 对象作为这块内存的引用进行操作,这样就避免了在 Java 堆和 Native 堆中来回复制数据,因此在一些场景中可以显著提高性能。

显然,本机直接内存的分配不会受到 Java 堆大小的限制(即不会遵守 -Xms、-Xmx 等设置),但既然是内存, 则肯定还是会受到本机总内存大小及处理器寻址空间的限制,因此动态扩展时也会出现 OutOfMemoryError 异常。

总结:

名称 特征 作用 配置 异常
栈区 线程私有,使用一段连续的内存空间 存放局部变量表、操作栈、动态链接、方法出口 -Xss StackOverflowError OutOfMemoryError
线程共享,生命周期与虚拟机相同 保存对象实例 -Xms -Xmx -Xmn OutOfMemoryError
程序计数器 线程私有、占用内存小 字节码行号
方法区 线程共享 存储类加载信息、常量、静态变量等 -XX:PermSize -XX:MaxPermSize OutOfMemoryError

二、java 堆和 GC

java 堆是和 java 应用程序关系最密切的内存空间,几乎所有的对象都存放在其中,并且 java 堆完全是自动化管理的,通过垃圾回收机制,垃圾对象会自动清理,不需要显示地释放。 Java 中的堆也是 GC 收集垃圾的主要区域。

Java 堆结构

根据垃圾回收机制不同,其中堆有可能拥有不同的结构:

  1. java 堆分为新生代和老年代。新生代存放新生的对象或者年龄不大的对象,老年代则存放老年对象。
  2. 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。新生代与老年代的比例通过参数 –XX:NewRatio 来指定。
  3. 新生代分为 eden 区、s0 区、s1 区,s0 和 s1 也被称为 from 和 to 区域,他们是两块大小相等并且可以互换角色的空间。Edem与from的比例通过参数 –XX:SurvivorRatio 来指定。
  4. Minor GC 采用的是复制算法,将还存活的对象从 s0 拷贝到 s1 区。
  5. 绝大多数情况下,对象首先分配在 eden 区,在一次新生代回收后,如果对象还存活,则会进入 s0 或者 s1 区,之后每经过一次新生代向收,如果对象存活则它的年龄就加 1。

三、参数配置

参数 说明
-Xms 初始堆大小。如:-Xms256m
-Xmx 最大堆大小。如:-Xmx512m
-Xmn 新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90%
-Xss JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。
-XX:NewRatio 新生代与老年代的比例,默认值 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3
-XX:SurvivorRatio 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10
-XX:PermSize 永久代(方法区)的初始大小
-XX:MaxPermSize 永久代(方法区)的最大值
-XX:+PrintGC 打印 GC 信息
-XX:+PrintGCDetails 打印 GC 信息
-XX:+HeapDumpOnOutOfMemoryError 让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用

每天用心记录一点点。内容也许不重要,但习惯很重要!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!