JVM运行时数据区

October 12, 2016

一、JVM运行时数据区

1.1 意义

Java源码首先编译成字节码,然后由Java虚拟机读取运行。字节码是静态代码,需要加载到内存才能成为动态运行的对象。

虽然Java虚拟机自动内存管理为Java开发者节省很多开发时间,不需要为内存管理编写冗余易错的代码。但要是对JVM的运行时数据区和内存回收机制不熟悉,容易在不经意间造成内存泄漏,面对内存占用问题时也会束手无策。

这是必须了解虚拟机内存管理的几点理由:

  • 利用有限内存保存更多对象;
  • 避免内存泄漏或造成内存溢出;
  • 配合运行时数据区编写高性能代码;
  • 有助于进行虚拟机内存调优;

1.2 分类及简介

内存数据区理论上分为 5个数据区两种类型

5个运行时数据区 分别是:方法区堆区虚拟机栈本地方法栈程序计数器

img

两种类型:

  • 线程共享数据区:
    • 方法区: 存储已加载的类信息、常量、静态变量、即时编译后代码等数据。常量池位于方法区,使用永久代实现方法区垃圾回收;
    • 堆区: 用于存放运行时对象实例和数组;
  • 线程私有数据区:
    • 虚拟机栈:执行方法的栈帧,存储局部变量、操作数栈、动态链接、方法出口等信息;
    • 本地方法栈: 存放Native方法的运行数据;
    • 程序计数器: 当前线程所执行字节码指示器,改变计数器指向选取下一条字节码指令;

二、具体介绍

2.1 程序计数器

程序计数器可看作当前线程所执行字节码行号的指针,每个线程有独立的程序计数器,线程间计数器互不影响。执行Java方法时,这个计数器记录执行字节码指令的地址。

如果当前线程执行的是Native方法,则计数器为空。

2.2 虚拟机栈

虚拟机栈是线程私有的,生命周期与线程生命周期一致。方法开始执行时,创建新栈帧放入虚拟机栈中,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

img

方法执行完毕,栈帧出栈并销毁(移动内存指针),然后继续执行虚拟机栈顶的栈帧。

2.3 本地方法栈

虚拟机执行Native方法时使用,不同的虚拟机有不同的实现方法。HotSpot虚拟机的本地方法栈和虚拟机栈二合一。

2.4 Java堆区

2.4.1 概览

堆区是开发过程中,工程师接触最频繁、最关注的内存区域。

该区域被所有线程共享,所有对象都在这个区域分配内存并初始化,而内存分配及回收操作也会消耗运算资源。相比之下,栈更容易管理且轻巧,所有内存都在进出栈过程中,自然而然地完成申请和释放。

有些原本保存在堆区的对象通过动态分析,可在栈内完成生命周期,把本来存放在堆内存的数据分配到栈中。数据的生命周期随着进出栈而完成管理,不需要像堆内存一样进行繁杂的回收操作,减轻堆内存压力。

img

虚拟机新生代 EdenSurvivor 默认比例是8: 1,即 Eden: Survivor s0: Survivor s1= 8: 1: 1

堆区分代回收根据对象不同生命周期,做出合理的内存分配和回收操作。大部分对象生命周期很短,熬不过第一次垃圾回收。

从分配的角度来看,线程本地缓冲区(Thread local allocation buffer, TLAB)有利于高效地从堆内存,划分部分空间为线程私有缓冲区。避免每次线程需要内存时,都直接请求堆区开辟新空间。

2.4.2 幸存者区域

新创建对象先存放在 Eden 区,经过第一次垃圾回收且存活的对象会进入两个 Survivor 中的一个。此时,这个 Survivor 区称为 To Survivor ,另一个区称为 From Survivor

同时,把上次回收存活对象从 From Survivor 移到 To Survivor,并清空 From Survivor

img

From SurvivorTo Survivor 名字是相对的。对象移出的区称为 From Survivor,对象存入的区域称为 To Survivor

多数时间两个区中一个正在使用,另一个已清空留给下次使用。

在Survivor区的对象会在两区之间来回经历GC。经过多次(CMS、G1默认都是15次)垃圾回收依然存活的对象,可假定此对象生命周期较长,达到GC年龄后就移到老年代。此后,在老年代中经历垃圾回收的频率将大幅降低。

如果新对象体积太大,以致新生代经过一次垃圾回收后,依然没有足够空间存放它,JVM会通过分配担保的方式,把这个对象放在老年代。如果老年代经过多次 Full GC 仍然没有空间,则虚拟机抛出OOM错误。

2.5 方法区

用于存放虚拟机加载的信息,如常量、静态变量、即时编译器编译后代码等数据。由于永久代存储的大部分数据生命周期非常长,这个区域垃圾回收消耗时间长且效果差,所以方法区GC频率相比其他分区要低。

来自官方的文档内容:

With G1 collector, PermGen is collected only at a Full GC which is a stop-the-world (STW) GC. If G1 is running optimally then it does not do Full GCs. G1 invokes the Full GCs only when the PermGen is full or when the application does allocations faster than G1 can concurrently collect garbage.

With CMS garbage collector, we can use option -XX:+CMSClassUnloadingEnabled to collect PermGen space in the CMS concurrent cycle. There is no equivalent option for G1. G1 only collects PermGen during the Full stop-the-world GCs.

不同回收策略对方法区的处理:

  • G1:只在 Full GC 的时候方法区才会被清理。当方法区需要空间时,该空间无法扩展又没有内存空间可以回收,就会抛出 OutOfMemoryError
  • CMS:可以使用 -XX:+CMSClassUnloadingEnabled 参数,在CMS并行收集阶段回收PermGen空间.

2.6 常量池

又称运行时常量池,是方法区的一部分,使用空间受方法区大小限制,用于存放编译器生成的各种字面量和符号引用。此外,运行期间新的常量也会放入常量池中,常见运行时常量池添加是通过String.intern()方法。

  1. 字面量:如文本字符串、final常量值
  2. 符号引用:编译语言层面的概念,包括以下3类:
    • 字段的名称和描述符
    • 方法的名称和描述符
    • 类和接口的全限定名

2.7 直接内存

直接内存不属于虚拟机运行时数据区内存,该空间划分在虚拟机外,大小不受堆内存容量限制。

但是,直接内存受物理机剩余可用内存、处理器寻址空间的限制。如果虚拟机堆内存分配太大,会导致剩余直接内存空间不足而出现运行时异常。

三、最新变化

3.1 元数据区

由于 PermGen 内存管理的效果远没有达到预期,所以JCP着手去除 PermGen 的工作。自JDK7起,字符串常量已经从永久代移除。在JDK8中,PermGen 被彻底移除,取而代之的是 metaspace 数据区。

元数据区使用堆外内存,申请和释放由虚拟机负责管理:

  • 失效参数 -XX:PermSize-XX:MaxPermSize 会被忽略并发出警告;
  • Metaspace通过参数 -XX:MetaspaceSize-XX:MaxMetaspaceSize 设定;

在JDK8u102中开启Android Studio后,Java VisualVM中只看见Metaspace,没有PermGen。 img

3.2 G1回收

G1回收机制发展到JDK8已基本成型。G1在JDK中应该是现时唯一一个,能完成从新生代到老年代所有管理的GC实现。

之前的技术,如前文提到的CMS和PerNew,需互相配合才能完成回收工作。

基于G1回收的特殊性,G1的数据区模型和上面介绍的模型是有差别的。进一步说,上述模型不能用在G1上。如果堆空间没有超过4G,CMS和PerNew已能很好地适应大部分应用场景。

G1相关资料: