首页 > 代码库 > JVM 内存管理

JVM 内存管理

JVM 通过 垃圾收集-GC 自动管理内存堆中对象内存的分配和回收。JVM 通常采用分代垃圾收集器,以便于整理内存碎片。分代垃圾收集器就是基于对象不同生命周期,将堆分成不同的内存区域,然后组合使用不同的垃圾收集算法,可简单认为分为两部分组成:

  • Young Generation:年轻代,由Eden和两个相等的Survivor空间组成,其中一个Survivor始终为空,用来复制Minor GC后在Eden和另一个Survivor存活的对象。
  • Old Generation:老年代,对象生命周期比较长。

内存回收

内存回收主要考虑两个问题:

  • 如何判断对象可被回收,判断策略:

    • Tracing GC,跟踪收集,也叫可达性分析算法,其思想是从某些根对象引用(GC roots)出发总能找到一个到一组存活对象的引用链。
    • Reference counting引用计数法,不能解决循环引用。
    • Escape analysis逃逸分析,可以将堆分配转为栈分配,动态编译优化手段,减轻GC压力。
  • 采用何种方式进行回收,垃圾收集算法:

    • Copying,复制,将存活对象从一块内存复制到另一块内存,不产生内存碎片,由于要保留一个空的备份内存,所以空间利用率较低。
    • Mark-Sweep,标记清理,从GC roots出发标记所有存活对象,然后清理所有未标记的对象,会产生内存碎片。
    • Mark-Compact,标记整理,标记清除后,会压缩内存,避免内存碎片。

内存分配

对象实例首先在 Eden 区分配,为了快速分配,Hotspot JVM 采用的是一种 bump-the-pointer线性分配方法。分配时一般都有大块连续内存可用,此方法就是检查剩余内存是否足够,给对象分配内存,然后更新指针偏移量和初始化对象。

线性分配效率固然高,但对多线程程序来说,分配内存的操作必须是线程安全的。可以使用全局锁但会影响性能,Hotspot JVM 采用的是一种 Thread-Local Allocation Buffers (TLABs) 的方法,为每个线程分配一个缓冲区,当TLAB满了,再加锁去申请,在线程内部就能使用bump-the-pointer,进而提高分配的吞吐量。

分配内存时,一些大对象有可能直接在老年代分配,在年轻代经过几轮Minor GC存活,达到一定年龄的对象,会被提升复制到老年代。

Hotspot JVM

Hotspot JVM 自JDK 6u23开始支持逃逸分析,采用 Tracing GC 追踪堆中存活对象,显然,GC roots就是堆外的对象引用

  • 栈帧中局部变量或方法参数的对象引用
  • 类引用类型静态成员变量
  • JNI 本地方法局部变量,参数和JNI 全局引用

需要注意GC roots是一组对象引用而不是引用对象

Hotspot JVM 提供了多个垃圾收集器让分代收集器组合使用:

  • 串行收集器,单个GC线程执行所有垃圾收集工作

    • Serial,年轻代,使用复制算法DefNew
    • Serial Old,老年代,使用标记压缩整理算法Tenured
  • 并行收集器,吞吐量收集器,多个GC线程加速垃圾回收

    • ParNew,年轻代,使用复制算法,可看做并行的Serial,ParNew
    • Parallel Scavenge,年轻代,使用复制算法,与 ParNew 的区别,它可以动态调节最大停顿时间和吞吐量,PSYoungGen
    • Parallel Old,老年代,使用标记压缩整理算法,通常与Parallel Scavenge配合使用,ParOldGen
  • 并发收集器,看重响应时间而不是吞吐量

    • CMS, Concurrent Mark Sweep,低停顿,采用标记清除算法,会有内存碎片,Java堆空间需求比较大,CMS
    • G1, Garbage-First,相比CMS,压缩内存,G1

常用JVM参数指定GC组合:

  • -XX:+UseSerialGC:Serial + Serial Old
  • -XX:+UseParNewGC:ParNew + Serial Old
  • -XX:+UseParallelGC:Parallel Scavenge + Serial Old
  • -XX:+UseParallelOldGC:Parallel Scavenge + Parallel Old
  • -XX:+UseConcMarkSweepGC:ParNew + CMS + Serial Old
  • -XX:+UseG1GC:G1

64位JVM 默认配置,以JDK 8为例

使用命令java -XX:+PrintFlagsFinal -version或者jmap -heap <pid>可以查看默认配置。

JVM堆最小为物理内存的 1/64,64位至少2G,即最小32M;最大为物理内存的 1/4,如128G内存,默认JVM最大32G,可以使用-Xms-Xmx指定初始和最大大小。年轻代默认比例为NewRatio=2,即占总堆的 1/3,可使用-Xmn指定大小,Survivor空间比例为SurvivorRatio=8,即每个Survivor占Eden的 1/8,因为有两个,所以占年轻代的 1/10。

JDK 8 默认垃圾收集器是ParallelOldGC,其中Parallel Scavenge默认打开AdaptiveSizePolicy,自适应调整各种参数,默认的SurvivorRatio 配置需要手动指定才能生效。比如指定堆大小为270M,那么各区域大小如下:

技术分享

图 1 并行垃圾收集,指定SurvivorRatio=8,堆空间大小

永久代, Permanent Generation

JVM 中方法区的实现,不同版本之间存储内容存在差别:

  • JDK 6:类元数据、类静态变量、intern字符串
  • JDK 7:将intern字符串移到堆中
  • JDK 8:移除永久代,新增一个元空间存储类元数据,将类静态变量和intern字符串移到堆中

永久代逻辑上是堆的组成部分,64位JVM默认大小为82M,但最大大小难以估计,因为程序里面有多少类,有多少方法以及常量池大小都很难估算,此外永久代垃圾收集与老年代绑定,任一区域满了都会触发Full GC,所以对永久代的调优很困难。

在JDK 8中,移除永久代,类元数据分配在本地内存中,默认情况下,可用内存不受限制,可使用MaxMetaspaceSize设置可用的内存上限。Hotspot JVM 显式管理这部分内存,从OS申请空间,然后分成块,一个块绑定到一个特定的类加载器,类元数据就在这些块中分配,当类卸载或加载器标记被回收,这些块被释放重用或返回OS。

小结

JDK 8中 GC cause有:年轻代内存分配失败,引起的Minor GC;年轻代提升到老年代,而老年代空间不足,引起的Full GC;元数据空间达到阈值,引起的GC。JDK 7之前,由于永久代空间不足引起的Full GC。不管是永久代或者元数据空间,也都会存在内存泄漏,比如web应用,当应用程序被卸载,那么它的war包中的所有类都是垃圾,如果不移除就会有内存泄漏。

在分析内存回收时,一定要谨记 GC roots 是一组对象引用或者说指针,拿废弃常量来说,完全可以通过GC root来识别,进而回收。

参考

  • Memory Management Whitepaper
  • Our Collectors
  • HotSpot Virtual Machine Garbage Collection Tuning Guide
  • JEP 122: Remove the Permanent Generation
  • Java HotSpot VM Options
  • String.intern in Java 6, 7 and 8 – string pooling
  • Java永久代去哪儿了
?

JVM 内存管理