内存模型
堆
堆(Heap)是JVM所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,该内存区域存放了对象实例及数组(但不是所有的对象实例都在堆中)。堆由垃圾收集器自动回收,是OOM故障最主要的发源地。
通过下两个参数来分配堆使用的内存大小:
- -Xms:最小堆容量,默认是物理内存的1/64。
- -Xmx:最大堆容量,默认是物理内存的1/4。
上下界调节的方式:
- 默认空余堆内存小于40%时,JVM 就会增大堆直到-Xmx 的最大限制,可以由 -XX:MinHeapFreeRatio 指定。
- 默认空余堆内存大于70%时,JVM 会减少堆直到-Xms的最小限制,可以由 -XX:MaxHeapFreeRatio 指定。
堆分成两大块:新生代和老年代。对象产生之初在新生代,步入暮年时进入老年代,但是老年代也接纳在新生代无法容纳的超大对象。
新生代:1个Eden区 + 2个Survivor区。绝大部分对象在Eden区生成,当Eden区装填满的时候,会触发Young GC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区,这个区真是名副其实的存在。
方法区
方法区(Method Area)也称"永久代",它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。默认最小值为16MB,最大值为64MB。它是一片连续的堆空间,永久代的垃圾收集是和老年代(old generation)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。
从JDK7开始移除永久代(但并没有移除,还是存在),贮存在永久代的一部分数据已经转移到了Java Heap或者是Native Heap:
- 符号引用(Symbols)转移到了native heap
- 字面量(interned strings)转移到了java heap
- 类的静态变量(class statics)转移到了java heap
从JDK8开始使用元空间(Metaspace),元空间的大小受本地内存限制。通过下面的参数可以设置:
- -XX:MetaspaceSize:class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值
- -XX:MaxMetaspaceSize:可以为class metadata分配的最大空间。默认是没有限制的
上下界调节的方式:
- -XX:MinMetaspaceFreeRatio:在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集
- -XX:MaxMetaspaceFreeRatio:在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集
由于类的元数据可以在本地内存(native memory)之外分配,所以其最大可利用空间是整个系统内存的可用空间。这样,你将不再会遇到OOM错误,溢出的内存会涌入到交换空间。最终用户可以为类元数据指定最大可利用的本地内存空间,JVM也可以增加本地内存空间来满足类元数据信息的存储。
JVM使用一个块分配器(chunking allocator)来管理Metaspace空间的内存分配。块的大小依赖于类加载器的类型。其中有一个全局的可使用的块列表(a global free list of chunks)。当类加载器需要一个块的时候,类加载器从全局块列表中取出一个块,添加到它自己维护的块列表中。当类加载器死亡,它的块将会被释放,归还给全局的块列表。块(chunk)会进一步被划分成blocks,每个block存储一个元数据单元(a unit of metadata)。Chunk中Blocks的分配线性的(pointer bump)。这些chunks被分配在内存映射空间(memory mapped(mmapped) spaces)之外。在一个全局的虚拟内存映射空间(global virtual mmapped spaces)的链表,当任何虚拟空间变为空时,就将该虚拟空间归还回操作系统。
程序计数寄存器
程序计数寄存器(Program Counter Register)是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器,在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。
它是线程私有。每条线程都有一个独立的程序计数器。生命周期随着线程的创建而创建,随着线程的结束而死亡。是唯一一个不会出现OOM的内存区域。
虚拟机栈
虚拟机栈(JVM Stack)是java方法执行的内存模型。每个方法被执行的时候都会创建一个"栈帧",用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。声明周期与线程相同,是线程私有的。
虚拟机栈通过压/出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上。在执行的过程中,如果出现异常,会进行异常回溯,返回地址通过异常处理表确定。
栈帧由下面几部分组成:
- 局部变量表:存放方法参数和局部变量
- 操作数栈:是一个初始状态为空的桶式结构栈,在方法执行过程中,会有各种指令往栈中写入和提取信息
- 动态连接:每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接
- 方法返回地址:方法执行时有两种退出情况,正常退出(RETURN、IRETURN、ARETURN指令),异常退出
本地方法栈
本地方法栈(Native Stack)与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。(栈的空间大小远远小于堆)。
在JVM内存布局中,也是线程对象私有的,但是虚拟机栈“主内”,而本地方法栈“主外”。这个“内外”是针对JVM来说的,本地方法栈为Native方法服务。线程开始调用本地方法时,会进入一个不再受JVM约束的世界。本地方法可以通过JNI(Java Native Interface)来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和JVM相同的能力和权限。当大量本地方法出现时,势必会削弱JVM对系统的控制力,因为它的出错信息都比较黑盒。
直接内存
直接内存(也称堆外内存)并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小。
总结
JVM内存模型从线程维度归类分为:线程私有内存、线程共享内存、以及不在堆内的直接内存。如下图:
各空间的分配参数:
GC
GC(Garbage Collection):即垃圾回收器,诞生于1960年MIT的Lisp语言,主要是用来回收,释放垃圾占用的空间。java GC泛指java的垃圾回收机制。
Java堆是GC回收的“重点区域”。堆中基本存放着所有对象实例,gc进行回收前,第一件事就是确认哪些对象存活,哪些死去。
为了高效的回收,Jvm将堆分为三个区域:
- 新生代(Young Generation)NewSize和MaxNewSize分别可以控制年轻代的初始大小和最大的大小
- 老年代(Old Generation)
- 永久代(Permanent Generation)(1.8以后采用元空间,就不在堆中了)
对象是否存活算法
引用计数算法
早期判断对象是否存活大多都是以这种算法,这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。
- 优点:实现简单效率高,被广泛使用与如python何游戏脚本语言上。
- 缺点:难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。
可达性分析算法
目前主流的商用语言(如java、c#)采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。
可作为GC Roots的对象有四种:
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象,就是平时所指的java对象,存放在堆中。
- 方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。
- 方法区中的常量引用的对象
- 本地方法栈中JNI(native方法)引用的对象
要真正宣告对象死亡需经过两个过程:
- 可达性分析后没有发现引用链
- 查看对象是否有finalize方法,如果有重写且在方法内完成自救(比如再建立引用),还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会时F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。
垃圾收集算法
新生代采用复制算法
新生代中因为对象都是"朝生夕死的",适用于复制算法。它优化了标记/清除算法的效率和内存碎片问题。由于存活率低,不需要复制保留那么大的区域造成空间上的浪费,因此不需要按1:1(原有区域:保留空间)划分内存区域,而是将内存分为一块Eden空间和From Survivor、To Survivor(保留空间),三者默认比例为8:1:1,优先使用Eden区,若Eden区满,则将对象复制到第二块内存区上。但是不能保证每次回收都只有不多于10%的对象存货,所以Survivor区不够的话,则会依赖老年代年存进行分配。
GC开始时,对象只会存于Eden和From Survivor区域,To Survivor(保留空间)为空。
GC进行时,Eden区所有存活的对象都被复制到To Survivor区,而From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阈值(默认15是因为对象头中年龄战4bit,新生代每熬过一次垃圾回收,年龄+1),则移到老年代,没有达到则复制到To Survivor。
老年代采用标记/清除算法或标记/整理算法
由于老年代存活率高,没有额外空间给他做担保,必须使用这两种算法。
标记/清除算法是几种GC算法中最基础的算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
- 标记阶段:标记的过程其实就是前面介绍的可达性分析算法的过程,遍历所有的GC Roots对象,对从GC Roots对象可达的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象;
- 清除阶段:清除的过程是对堆内存进行遍历,如果发现某个对象没有被标记为可达对象(通过读取对象header信息),则将其回收。
标记/整理算法,与标记/清除算法一样,但后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象都向一端移动,然后直接清理掉端边线以外的内存。
三种算法的对比
- 效率:复制算法 > 标记/整理算法 > 标记/清除算法(标记/清除算法有内存碎片问题,给大对象分配内存时可能会触发新一轮垃圾回收)
- 内存整齐率:复制算法 = 标记/整理算法 > 标记/清除算法
- 内存利用率:标记/整理算法 = 标记/清除算法 > 复制算法
垃圾收集器
新生代收集器
- Serial:Serial是单线程收集器,Serial收集器只能使用一条线程进行收集工作,在收集的时候必须得停掉其它线程,等待收集工作完成其它线程才可以继续工作。对于Client模式下的jvm来说是个好的选择。
- ParNew收集器:可以认为是Serial的升级版,因为它支持多线程GC。运行在Server模式下新生代首选的收集器。
- Parallel Scavenge:采用复制算法的收集器,和ParNew一样支持多线程。该收集器重点关心的是吞吐量。也成为"吞吐量优先"收集器。
老年代收集器
- Serial Old:和新生代的Serial一样为单线程,Serial的老年代版本,不过它采用"标记-整理算法",这个模式主要是给Client模式下的JVM使用。
- Parallel Old:支持多线程,Parallel Scavenge的老年版本,jdk6开始出现, 采用"标记-整理算法"。Parallel Old的出现结合Parallel Scavenge,真正的形成“吞吐量优先”的收集器组合。
- CMS:CMS收集器(Concurrent Mark Sweep)是以一种获取最短回收停顿时间为目标的收集器。重视响应,可以带来好的用户体验,被sun称为并发低停顿收集器
- G1:G1(garbage first:尽可能多收垃圾,避免full gc)收集器是当前最为前沿的收集器之一(1.7以后才开始有),同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。
GC分类
- Minor GC:在年轻代Young space(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC,Minor GC只会清理年轻代。
- Major GC:Major GC清理老年代(old GC),但是通常也可以指和Full GC是等价,因为收集老年代的时候往往也会伴随着升级年轻代,收集整个Java堆。所以有人问的时候需问清楚它指的是full GC还是old GC。
- Full GC:Full GC是对新生代、老年代、永久代(jdk1.8后没有这个概念了)统一的回收。
- Mixed GC:只有G1有这个模式,收集整个young gen以及部分old gen的GC。
触发GC点
简单来说,触发的条件就是GC算法区域满了或将满了。
Minor GC:
- 当年轻代中eden区分配满的时候触发。
- 值得一提的是因为Minor GC后部分存活的对象会已到老年代(比如对象熬过15轮),所以过后old gen的占用量通常会变高。
Full GC:
- 手动调用System.gc()方法 ,增加了full GC频率,不建议使用而是让jvm自己管理内存,可以设置-XX:+ DisableExplicitGC来禁止RMI调用System.gc
- 发现perm gen(如果存在永久代的话)需分配空间但已经没有足够空间
- 老年代空间不足,比如说新生代的大对象大数组晋升到老年代就可能导致老年代空间不足。
- CMS GC时出现Promotion Faield
- 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间。这个比较难理解,这是HotSpot为了避免由于新生代晋升到老年代导致老年代空间不足而触发的FUll GC。比如程序第一次触发Minor GC后,有5m的对象晋升到老年代,姑且现在平均算5m,那么下次Minor GC发生时,先判断现在老年代剩余空间大小是否超过5m,如果小于5m,则HotSpot则会触发full GC
GC日志
JVM的GC日志的主要参数包括如下几个:
- -XX:+PrintGC 输出GC日志
- -XX:+PrintGCDetails 输出GC的详细日志
- -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
- -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
- -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
- -XX:+PrintGCApplicationStoppedTime // 输出GC造成应用暂停的时间
- -Xloggc:logs/gc.log 日志文件的输出路径
GC日志样例:
0.070: [GC (Allocation Failure) [PSYoungGen: 7127K->616K(9216K)] 11223K->4720K(19456K), 0.0008663 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.072: [GC (Allocation Failure) --[PSYoungGen: 6923K->6923K(9216K)] 11027K->15123K(19456K), 0.0016749 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
0.073: [Full GC (Ergonomics) [PSYoungGen: 6923K->0K(9216K)] [ParOldGen: 8200K->6660K(10240K)] 15123K->6660K(19456K), [Metaspace: 2559K->2559K(1056768K)], 0.0044663 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 9216K, used 4404K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 53% used [0x00000000ff600000,0x00000000ffa4d1a0,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
ParOldGen total 10240K, used 6660K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 65% used [0x00000000fec00000,0x00000000ff281398,0x00000000ff600000)
Metaspace used 2565K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 281K, capacity 386K, committed 512K, reserved 1048576K
- 最前面的数字 “0,070” 代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数
- GC日志开头的“[GC 和 [Full GC” 说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是年老代GC的。
- PSYoungGen, ParOldGen,PSPermGen表示GC发生的区域,这里显示的区域名称与使用的GC收集器密切相关,不同收集器对于不同区域所显示的名称可能不同。
- 后面方括号内部的 “ 7127K->616K(9216K) ”含义是“GC前该内存区域已使用容量 -> GC后该内存区域已使用容量(该内存区域总容量)”。方括号之外的 11223K->4720K(19456K) 表示GC前java堆已使用容量 -> GC后java堆已使用容量(Java堆总容量)
- 0.0008663 secs表示该内存区域GC所占用的时间,单位是秒。
- [Times: user=0.00 sys=0.00, real=0.00 secs] 这里面的user、sys、和real与Linux的time命令所输出的时间含义一致。分别代表用户消耗的CPU时间,内存态消耗的CPU时间,和操作从开始到结束所经过的墙钟时间。
注:以上内容收集于互联网多篇文章,在此感谢原作者们。