参考:
https://mp.weixin.qq.com/s/oDeO8Td-SJn9g4mRygqtSw
JVM 运行时的内存结构
1. Heap
内存区域中最大的一块区域,被所有线程共享,存储着几乎所有的实例对象、数组。
堆大小设置
根据 Java 虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,既可以实现成固定大小的,也可以在运行时动态地调整。
-Xms
和 -Xmx
设置初始值和最大值,如 -Xms256M -Xmx1024M
. ms 是 memory start,mx 是 memory max.
通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,会形成不必要的系统压力。因此线上生产环境中 JVM 的 Xms 和 Xmx 会设置成同样大小,避免在 GC 后调整堆大小时带来的额外压力。
堆内部结构
默认堆空间分配:Young Generation 为 1/3, Old Generation 为 2/3. 其中,Young Generation 中,Eden 占 8/10, Suivivor0 和 Survivor1 均为 1/10.
相关参数:
-
-XX:InitialSurvivorRatio
, 表示 Eden 与 Survivor 的比例,默认为 8 表示 Eden 与两个 Survivor 比例为 8:1:1 - -XX:NewRatio, 表示 Old Generation 与 Young Generation 的比例,默认为 2 表示 2:1.
新对象内存分配流程
绝大部分对象在 Eden 区生成,当 Eden 区装填满的时候,会触发 Young Garbage Collection,即 YGC。垃圾回收的时候,在 Eden 区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor 区。Survivor 区分为 so 和 s1 两块内存空间。每次 YGC 的时候,它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。如果 YGC 要移送的对象大于 Survivor 区容量的上限,则直接移交给老年代。一个对象也不可能永远呆在新生代,就像人到了 18 岁就会成年一样,在 JVM 中 -XX:MaxTenuringThreshold
参数就是来配置一个对象从新生代晋升到老年代的阈值。默认值是 15,可以在 Survivor 区交换 14 次之后,晋升至老年代。
2. Metaspace
存放类和方法的元数据以及常量池。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
Java 8 中 PermGen 被移出 HotSpot JVM, 主要原因是避免 java.lang.OutOfMemoryError: PermGen 以及更好地与 JRockit VM 融合。
Java 7 字符串常量池被从方法区拿到了堆,Java 8 时,PermGen 被元空间代替,其他内容比如类元信息、字段、静态属性、方法、常量等都移动到元空间区。
3. JVM Stacks
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
虚拟机栈的生命周期和线程是一致的,并且是线程私有的。
StackOverflowError 表示请求的栈溢出, 导致内存耗尽, 通常出现在递归方法中。
栈帧(Stack Frame)包含局部变量表,操作栈(Operand Stack),动态连接,方法返回地址。
可以通过 -Xss
这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小。JDK 5.0以后每个线程堆栈大小为1M, 以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的, 不能无限生成,经验值在3000~5000左右。
该区域可能抛出以下异常:
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常
4. Native Method Stacks
有的虚拟机(譬如 Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
5. Program Counter Register
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。可以看作是当前线程所执行的字节码的行号指示器。
代码是在线程中运行的,线程有可能被挂起。即 CPU 一会执行线程 A,线程 A 还没有执行完被挂起了,接着执行线程 B,最后又来执行线程 A 了,CPU 得知道执行线程A的哪一部分指令,线程计数器会告诉 CPU。
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或恢复都要依赖程序计数器。此区域也不会发生内存溢出异常。
6. Direct Memory
在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。如果内存区域总和大于物理内存的限制,也会出现 OOM。
7. Code Cache
JVM 代码缓存是 JVM 将其字节码存储为本机代码的区域。实时(JIT)编译器是代码缓存区域的最大消费者。这就是为什么一些开发人员将此内存称为 JIT 代码缓存的原因。