JVM内存 — 《java核心技术》

JVM内存区域划分

  1. 程序计数器,每个线程都有自己的程序计数器,存储当前线程正在执行的Java方法的JVM指令地址;
  2. Java虚拟机栈,每个线程在创建时都会创建一个虚拟机栈,内部保存一个个栈帧,对应着一次次方法调用。如果在该方法中调用了其他方法,则会创建新的栈帧。栈帧中存储着局部变量表、操作数栈、动态链接、方法正常退出或异常退出的定义等;
  3. 堆,放置Java对象实例,是垃圾收集器重点照顾的区域;
  4. 方法区,所有线程共享的一块内存区域,用于存储元数据,比如类结构信息,以及对应的运行时常量池、字段、方法代码等。由于早期的 Hotspot JVM 实现,很多人习惯于将方法区称为永久代(Permanent Generation)。Oracle JDK 8 中将永久代移除,同时增加了元数据区(Metaspace);
  5. 运行时常量池,方法区的一部分,用于存放各种常量信息,比如编译期生成的各种字面量,以及需要在运行时决定的符号引用;
  6. 本地方法栈,与Java虚拟机栈非常相似,支持对本地方法的调用,也是在每个线程都会创建一个。

360b8f453e016cb641208a6a8fb589bc

有可能发生OOM的地方

  1. 堆内存不足,抛出“java.lang.OutOfMemoryError:Java heap space”,比如需要分配的数据过大,或者JVM处理引用不及时,堆积起来使得内存无法释放;
  2. Java虚拟机栈和本地方法栈,如果调用过深,会导致StackOverFlowError。如果这时候JVM试图扩展栈空间失败,会抛出OutOfMemoryError;
  3. 老版本的JDK永久代的大小有限,JVM对永久代垃圾回收也不积极,如果不断添加新的类型,则会导致永久代出现OutOfMemoryError。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError: PermGen space”;
  4. 元数据区引入后,方法区内存不再窘迫。如果出现OOM则变成“java.lang.OutOfMemoryError: Metaspace”;
  5. 直接内存不足也会导致OOM。

堆内部

年代视角的堆结构

721e97abc93449fbdb4c071f7b3b5289

按照GC年代划分,Java堆内存分为:

新生代

大部分对象创建和销毁的区域。内部分为Eden区域作为对象初始分配的区域,以及两个Survivor(from、to区域),被用来放置从Minor GC中保留下来的对象。

  • JVM随机选取一个Survivor作为“to”,在GC过程中进行区域间拷贝,也就是将 Eden 中存活下来的对象和 from 区域的对象,拷贝到这个“to”区域。
  • 从内存模型而不是垃圾收集的角度,对 Eden 区域继续进行划分,Hotspot JVM 还有一个概念叫做 Thread Local Allocation Buffer(TLAB),目的是为了防止在多线程同时分配内存时,避免操作同一地址可能需要加锁等机制。我们分配新对象,JVM 就会移动 top,当 top 和 end 相遇时,即表示该缓存已满,JVM 会试图再从 Eden 里分配一块儿。

f546839e98ea5d43b595235849b0f2bd

老年代

放置长生命周期的对象,通常是从Survivor拷贝过来的对象。也有特殊情况,当分配对象较大,TLAB容不下,JVM会尝试直接分配在Eden其他位置,如果无法在新生代找到足够长的连续空闲空间,JVM会直接分配到老年代。

永久代

方法区实现,存储了Java类元数据、常量池、Intern字符串缓存。

常见的垃圾收集器

  1. (新生代)Serial GC,单线程工作,垃圾收集的时候会Stop-The-World。新生代使用复制算法,老年代使用标记-整理(Mark-Compact)算法,client模式的默认选择;
  2. (新生代)ParNew GC,Serial GC的多线程版本,通常配合老年代的CMS GC;
  3. (新生代)Parallel Scavenge。并行运行,复制算法;吞吐量优先;适用于后台运算而不需要太多交互的场景。
  4. (老年代)CMS(Concurrent Mark Sweep)GC,基于标记-清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间。存在内存碎片化问题,难以避免在长时间运行等情况下发生full GC。且并发会占用更多CPU资源,并会和用户线程争抢,在JDK9中已被标为废弃;
  5. (老年代)Parallel GC,server模式的默认选择,吞吐量优先,特点是新生代和老年代GC并行进行,可以通过参数设置暂停事件和吞吐量等目标;
  6. (新生代、老年代)G1 GC,兼顾吞吐量和停顿时间的GC实现,JDK9之后的默认GC。G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个 region。Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。

垃圾收集的原理

哪些内存可被回收

对象实例

Java选择可达性分析,其原理简单来说,就是将对象及其引用关系看作一个图,选定活动的对象作为 GC Roots,然后跟踪引用链条,如果一个对象和 GC Roots 之间不可达,也就是不存在引用链条,那么即可认为是可回收对象。

JVM会把虚拟机栈和本地方法栈中正在引用的对象、静态属性引用的对象和常量,作为GC Roots。

方法区

一般来说初始化类加载器加载的类型是不会进行类卸载(unload)的;而普通的类型的卸载,往往是要求相应自定义类加载器本身被回收,所以大量使用动态类型的场合,需要防止元数据区(或者早期的永久代)不会 OOM。

常见垃圾收集方法

复制

将活着的对象复制到 to 区域,拷贝过程中将对象顺序放置,就可以避免内存碎片化。

需要提前预留内存空间。对于G1这种氛围大量region的GC,则意味着GC需要维护region之间对象引用关系,内存占用和时间开销也不小。

标记-清理

首先进行标记工作,标识出所有要回收的对象,然后进行清除。会导致碎片化。

标记-整理

类似于标记-清理,为避免内存碎片化,会在清理过程中将对象移动,以确保移动后的内存占用连续内存空间。

垃圾收集过程

  1. Java不断创建对象,分配在Eden区,当其空间占用达到一定阈值时,触发 minor GC。仍然被引用的对象(绿色方块)存活下来,被复制到 JVM 选择的 Survivor 区域,而没有被引用的对象(黄色方块)则被回收。1表示对象存活时间。
    44d4a92e8e20f46e6646eae53442256d

  2. 经过一次Minor GC后,Eden 就会空闲下来,直到再次达到 Minor GC 触发条件,这时候,另外一个 Survivor 区域则会成为 to 区域,Eden 区域的存活对象和 From 区域对象,都会被复制到 to 区域,并且存活的年龄计数会被加 1。
    3be4ac4834e2790a8211252f2bebfd48

  3. 类似第二步的过程会发生很多次,直到有对象年龄计数达到阈值,这时候就会发生所谓的晋升(Promotion)过程,如下图所示,超过阈值的对象会被晋升到老年代。
    dbcb15c99b368773145b358734e10e8d

  4. 后面就是老年代 GC,具体取决于选择的 GC 选项,对应不同的算法。下面是一个简单标记 - 整理算法过程示意图,老年代中的无用对象被清除后, GC 会将对象进行整理,以防止内存碎片化。
    399a0c23d1d57e08a2603fb76f328e25

通常我们把老年代 GC 叫作 Major GC,将对整个堆进行的清理叫作 Full GC。

comments powered by Disqus