JVM 内存区域

JVM会将Java进程所管理的内存划分为若干不同的数据区域. 这些区域有各自的用途、创建/销毁时间:

 


一. 线程私有区域

线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束而创建/销毁(在Hotspot VM内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的生/死).

1. Program Counter Register(程序计数器):

一块较小的内存空间, 作用是当前线程所执行字节码的行号指示器 (类似于传统CPU模型中的PC), PC在每次指令执行后自增, 维护下一个将要执行指令的地址. 在JVM模型中, 字节码解释器就是通过改变PC值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖PC完成(仅限于Java方法, Native方法该计数器值为undefined). 
不同于OS以进程为单位调度, JVM中的并发是通过线程切换并分配时间片执行来实现的. 在任何一个时刻, 一个处理器内核只会执行一条线程中的指令. 因此, 为了线程切换后能恢复到正确的执行位置, 每条线程都需要有一个独立的程序计数器, 这类内存被称为“线程私有”内存.


2. Java Stack(虚拟机栈)

虚拟机栈描述的是Java方法执行的内存模型: 每个方法被执行时会创建一个栈帧(Stack Frame)用于存储当前线程的 局部变量表、操作数栈、动态链接、方法出口等信息. 每个方法被调用至返回的过程, 就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程(VM提供了-Xss来指定线程的最大栈空间, 该参数也直接决定了函数调用的最大深度).

局部变量表:

分成一个个slot,每个长度是32位,存放了编译期可知的各种基本数据类型(如boolean、int、double等) 、对象引用(reference : 不等同于对象本身, 可能是一个指向对象起始地址的指针, 也可能指向一个代表对象的句柄或其他与此对象相关的位置, 见下: HotSpot对象定位方式) 和 returnAddress类型(指向一条字节码指令的地址). 其中longdouble占用2个局部变量空间(Slot), 其余只占用1个. 任何class文件可以使用javap命令或javassist等字节码工具得到字节码文件。

字节码文件的的命令就是把局部变量一个个放到操作数栈中并进行运算等操作。

 


3. Native Method Stack(本地方法栈)

与Java Stack作用类似, 区别是Java Stack为执行Java方法服务, 而本地方法栈则为Native方法服务, 如果一个VM实现使用C-linkage模型来支持Native调用, 那么该栈将会是一个C栈, 但HotSpot VM直接就把本地方法栈和虚拟机栈合二为一.


二. 线程共享区域

随虚拟机的启动/关闭而创建/销毁.


1. Heap(Java堆)

几乎所有对象实例和数组都要在堆上分配(栈上分配、标量替换除外), 因此是VM管理的最大一块内存, 也是垃圾收集器的主要活动区域. 由于现代VM采用分代收集算法, 因此Java堆从GC的角度还可以细分为: 新生代(Eden区、From Survivor区和To Survivor区)和老年代; 而从内存分配的角度来看, 线程共享的Java堆还还可以划分出多个线程私有的分配缓冲区(TLAB). 而进一步划分的目的是为了更好地回收内存和更快地分配内存.


2. Method Area(方法区)

1.7以前方法区也就是我们常说的永久代(Permanent Generation), 用于存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样HotSpot的垃圾收集器就可以像管理Java堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小); 在1.8中,永久区已经被彻底移除, 取而代之的是元数据区Metaspace(这一点在查看GC日志和使用jstat -gcutil查看GC情况时可以观察到),与永久代不同, 如果不指定Metaspace大小, 如果方法区持续增长, VM会默认耗尽所有系统内存。

运行时常量池 
        方法区的一部分. Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项常量池(Constant Pool Table)用于存放编译期生成的各种字面量和符号引用, 这部分内容会存放到方法区的运行时常量池中(如前面从test方法中读到的signature信息). 但Java语言并不要求常量一定只能在编译期产生, 即并非预置入Class文件中常量池的内容才能进入方法区运行时常量池, 运行期间也可能将新的常量放入池中, 如Stringintern()方法。

需要注意的是,对于字符串常量,JDK1.7及以前都是保存在方法区中,从1.8开始都是保存在堆中。

 

三、虚拟机中GC的过程

        1、在初始阶段,新创建的对象被分配到Eden区,survivor的两块空间都为空。 

         

java gc触发命令 jvm gc什么时候触发_内存模型

        2,当Eden区满了的时候,minor garbage 被触发 。

            

java gc触发命令 jvm gc什么时候触发_Java_02

        3,经过扫描与标记,存活的对象被复制到S0,不存活的对象被回收

           

java gc触发命令 jvm gc什么时候触发_老年代_03

        4,在下一次的Minor GC中,Eden区的情况和上面一致,没有引用的对象被回收,存活的对象被复制到survivor区。然而在survivor区,S0的所有的数据都被复制到S1,需要注意的是,在上次minor GC过程中移动到S0中的两个对象在复制到S1后其年龄要加1。此时Eden区S0区被清空,所有存活的数据都复制到了S1区,并且S1区存在着年龄不一样的对象,过程如下图所示:

            

java gc触发命令 jvm gc什么时候触发_Java_04

        5,再下一次MinorGC则重复这个过程,这一次survivor的两个区对换,存活的对象被复制到S0,存活的对象年龄加1,Eden区和另一个survivor区被清空。

                

java gc触发命令 jvm gc什么时候触发_java gc触发命令_05

        6,再经过几次Minor GC之后,当存活对象的年龄达到一个阈值之后(可通过参数配置,默认是8),就会被从年轻代Promotion到老年代。 

                

java gc触发命令 jvm gc什么时候触发_Java_06

        7,随着MinorGC一次又一次的进行,不断会有新的对象被promote到老年代。 

                

java gc触发命令 jvm gc什么时候触发_内存模型_07

           8,上面基本上覆盖了整个年轻代所有的回收过程。最终,MajorGC将会在老年代发生,老年代的空间将会被清除和压缩。

                

java gc触发命令 jvm gc什么时候触发_内存模型_08

        从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下(基于大部分对象存活周期很短的事实)高效,如果在老年代采用停止复制,则是非常不合适的。

            老年代存储的对象比年轻代多得多,而且不乏大对象,对老年代进行内存清理时,如果使用停止-复制算法,则相当低效。一般,老年代用的算法是标记-压缩算法,即:标记出仍然存活的对象(存在引用的),将所有存活的对象向一端移动,以保证内存的连续。在发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC,否则,就查看是否设置了-XX:+HandlePromotionFailure(允许担保失败),如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行Full GC(这代表着如果设置-XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)。

 

minor GC:收集年轻代

major GC:收集老年代

Full GC:两者的总和

 Minor GC ,Full GC 触发条件
Minor GC触发条件:当Eden区满时,触发Minor GC。

Full GC触发条件:
(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
(3)通过jmap命令主动触发Full GC