原文来自《码出高效 Java开发手册》
写的非常好,推荐购买
内存是非常重要的系统资源 , 是硬盘和 CPU 的中 间仓库及桥梁 , 承载着操作系统和应用程序的实时运行。 JVM 内存布局规定了 Java 在运行过程中内存申请、分配、管理的策略 ,保证了 JVM 的高效稳定运行。不同的 JVM 对于内存的划分方式和管理机制存在着部分差异。结合 JVM 虚拟机规范 , 来探讨一下经典的 NM 内存布局 , 如图所示。
Heap 是 OOM 故障最主要的发源地,它存储着几乎所有实例对象,堆由垃圾收集器自动回收,堆区由各子线程共享使用。通常情况下,它占用的空间是所有内存区域中最大的,但如果无节制地创建大量对象,也容易消耗完所有的空间。堆的内存空间即可以固定大小,也可以在运行时动态地调整,通过如下参数设定初始值和最大值,比如 -Xms256M -Xmx1024M,其中 -X 表示它是 JVM 运行参数, ms 是 memory start 的简称, mx 是 memory max 的简称,分别代表最小堆容量和最大堆容量。但是在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,势必形成不必要的系统压力 , 所以在线上生产环境中 , JVM 的 Xms 和 Xmx 设置成一样大小,避免在 GC 后调整堆大小时带来的额外压力 。
堆分成两大块 新生代和老年代。对象产生之初在新生代 , 步入暮年时进入老年 代 , 但是老年代也接纳在新生代无法容纳的超大对象。新生代= 1 个 Eden 区+ 2 个 Survivor 区。绝大部分对象在 Eden 区生成 , 当 Eden 区装填满的时候 , 会触发 Young Garbage Collection , 即 YGC。垃圾回收的时候 , 在 Eden 区实现清除策略 , 没有被引用的对象则直接回收。依然存活的对象会被移送到 Survivor 区 , 这个区真是名副其实的存在。 Survivor 区分为 S0 和 S1 两块内存空间 ,送到哪块空间呢?每次 YGC 的 时候, 它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除 , 交换两块空间的使用状态。如果 YGC 要移送的对象大于 Survivor 区容量的上限 ,则直接移交给老年代。假如一些没有进取心的对象以为可以一直在新生代的 Survivor 区交换来交换去,那就错了。每个对象都有一个计数器,每次 YGC 都会加 1 。 -XX:MaxTenuringThreshold 参数能配置计数器的值到达某个阀值的时候 , 对象从新生代晋升至老年代。如果该参数配置为 1,那么从新生代的 Eden 区直接移至老年代。 默认值是 15 , 可以在 Survivor 区交换 14 次之后 , 晋升至老年代。 对象晋升流程图如图所示。
图 中,如果 Survivor 区无法放下, 或者超大对象的闹值超过上限, 则尝试在老年代中进行分配 ; 如果老年代也无法放下, 则会触发 Full Garbage Collection , 即 FGC。 如果依然无法放下, 则抛出 OOM。 堆内存出现 OOM 的概率是所有内存耗尽 异常中最高的。 出错时的堆内信息对解决问题非常有帮助 , 所以给 JVM 设置运行参数 -XX:+HeapDumpOnOutOfMemoryError ,让 JVM 遇到 OOM 异常时能输出堆内信息,特别是对相隔数月才出现的 OOM 异常尤为重要。
在不同的 JVM 实现及不同的回收机制中 , 堆内存的划分方式是不一样的。
本书源码解析和示例代码基本采用 JDK11 版本, JVM 则 为 Hotspot。 早在 JDK8 版本中, 元空间的前身 Perm 区已经被淘汰。 在 JDK7 及之前的版本中, 只有 Hotspot 才有 Perm 区,译为永久代 , 它在启动时固定大小, 很难进行调优, 并且 FGC 时会移动类元信息。 在某些场景下, 如果动态加载类过多, 容易产生 Perm 区的 OOM。 比如某个实际 Web 工程中, 因为功能点比较多, 在运行过程中, 要不断动态加载很多的类, 经常出现致命错误:
"Exception in thread 'dubbo client x.x connector' java.lang.OutOfMemoryError: PermGenspace"
为了解决该问题 ,需要设定运行参数 -XX:MaxPermSize=1280m ,如果部署到新机器上, 往往会因为 JVM 参数没有修改导致故障再现。 不熟悉此应用的人排查问题时往往苦不堪言, 除此之外, 永久代在垃圾回收过程中还存在诸多问题。 所 以, JDK8 使用元空间替换永久代。 在 JDK8 及以上版本中, 设定 MaxPermSize 参 数, JVM 在启动时并不会报锚, 但是会提示 : Java HotSpot 64Bit Server VM warning: ignoring option MaxPem1Size=2560m; support was removed in 8.0。
区别于永久代 , 元空间在本地内存中分配。 在 JDK8 里, Perm 区中的所有内容中字符串常量移至堆内存, 其他内容包括类元信息、字段、静态属性、方法、常量等都移动至元空间内, 比如图 4-10 中的 Object 类元信息、静态属性 System.out、整型常量 10000000 等。图 4-10 中显示在常量池中的 Strirg, 其实际对象是被保存在堆内 存中的。
栈( Stack )是一个先进后出的数据结构 , 就像子弹的弹夹 , 最后压入的子弹先发射 , 压在底部的子弹最后发射 , 撞针只能访问位于顶部的那一颗子弹。
相对于基于寄存器的运行环境来说 ,JVM 是基于栈结构的运行环境。栈结构移植性更好 ,可控性更强。JVM 中的虚拟机栈是描述 Java 方法执行的内存区域,它是线程私有的。栈中的元素用于支持虚拟机进行方法调用 , 每个方法从开始调用到执行完成的过程 , 就是栈帧从入栈到出栈的过程。在活动线程中 , 只有位于栈顶的帧才是有效的 , 称为当前栈帧。正在执行的方法称为当前方法 , 栈帧是方法运行的基本结构。 在执行引擎运行时 , 所有指令都只能针对当前栈帧进行操作。而 StackOverflow Error 表示请求的栈溢出 , 导致内存耗尽 , 通常出现在递归方法中。 JVM 能够横扫干军 , 虚拟机栈就是它的心腹大将 , 当前方法的栈帧 , 都是正在战斗的战场 , 其中的操作栈是参与战斗的士兵。操作栈的压栈与出栈如图所示。