JVM运行时数据区

Posted by phantomVK on October 12, 2016

一、JVM运行时数据区

1.1 意义

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

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

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

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

1.2 分类及简介

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

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

img

两种类型:

  • 线程共享数据区:
    • 方法区: 存储已加载类信息、常量、静态变量、即时编译后代码等数据。常量池位于方法区,使用永久代实现方法区GC;
    • 堆区: 用于存放对象实例和数组;
  • 线程私有数据区:
    • 虚拟机栈:执行方法时创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等消息;
    • 本地方法栈: 用于存放执行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并清空。

img

From Survivor和To Survivor名字是相对的。对象移出的Survivor区称为From Survivor,对象存入的区域称为To Survivor。大多数时间两个区中一个处于占用状态,另一个清空准备下次使用。

在Survivor区的对象会在两区之间来回经历GC。经过多次垃圾回收依然存活的对象,可以假定此对象比较稳定,达到GC年龄后移到老年代,此后在老年代中经历垃圾回收的频率将大大降低。

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

2.5 方法区

用于存放虚拟机加载的信息、常量、静态变量、即时编译器编译后的代码等数据。由于永久代存储的大部分数据生命周期非常长,GC在这个区域消耗时间长且回收效果差,所以方法区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的时候方法区才会被清理。当方法区需要空间时,该空间无法扩展又没有内存空间可以回收,就会抛出OutOfMemory错误;
  • 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后,jvisualvm中只看见Metaspace而没有PermGen

img

3.2 G1回收

G1回收机制发展到JDK8已基本成型。G1在JDK中应该是现时唯一一个,能完成从新生代到老年代所有管理的GC实现。之前的技术,如前文提到的CMS和PerNew需互相配合才能完成回收工作。

基于G1回收的特殊性,G1的数据区模型和上面介绍的模型是有差别的。进一步说,上述模型不能用在G1上。现时没有太多资料供参考G1数据区模型,如果没有特殊需求,现时CMS和PerNew已经很好地适应大部分应用场景。

G1学习资料: