JVM学习系列(一)—内存模型与垃圾收集

一  java的内存模型

(一)运行时数据区域

JVM学习系列(一)—内存模型与垃圾收集

 

1. 程序计数器

 线程私有,该区域是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。记录正在执行的虚拟机字节码指令(分支、循环、跳转、异常处理等基础功能)的地址,如果正在执行native方法,计数器为空。此内存区域是唯一一个在JVM里没有规定OOM情况的区域。

2. java虚拟机栈

线程私有,它的生命周期与线程相同,该区域由栈帧构成(每个栈帧即是一个方法),栈帧存储的数据即为对应方法里的数据(局部变量表、操作数栈、动态链接、方法出口等),当线程请求的栈深度大于允许的深度抛出SOF,深度扩展时(大部分JVM实现允许扩展)无法申请到足够的内存,将抛出OOM。

3. 本地方法栈

线程私有,与虚拟机栈帧类似,只是其方法为native方法,同时存在SOF、OOM。

4. java堆(GC堆)

线程共享,垃圾收集器的重点照顾对象,几乎所有的对象实例都在这里分配内存(随着JIT编译器和逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术会导致一些微量的变化,该结论就不再绝对),目前主流虚拟机划分为该区域为新生代和老年代,再细致划分为:新生区(Eden空间,From Survivor,To Survivor)、老年代以及线程私有的分配缓冲区(TLAB)。大部分OOM场景在此发生。

5. 方法区(非堆)

线程共享,存储已被虚拟机加载的类信息(类全名、字段名、方法名等)、常量、静态变量等数据,常被称作“永久代(PermGen)”,实则两者并不等价,永久代只是HotSpot虚拟机对方法区的实现,其他虚拟机(IBM J9、BEA JRockit)并不存在永久代的说法。

6. 运行时常量池

       此区域是方法区的一部分(JDK8以前,JDK8已将该区域放入堆中),主要是存放编译期生成的各种字面量和符号引用(这部分数据会在类加载后才进入方法区的运行时常量池,存在OOM。

补充:

在JDK8以后 HotSpot虚拟机使用元空间(Metaspace,元空间的数据存放在本地内存中)代替永久代(PermGen),在JDK7中就已经将放在永久代的字符串常量池移至堆中,符号引用转移至本地内存。

关于做这个调整的几点原因:

1、字符串存在永久代中,容易出现性能问题和内存溢出。

2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出(此消彼长,永久代设置过大,堆内存就相对较小)。

3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

(二) 直接内存

直接内存并不存在虚拟机运行时的数据区,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁使用,而且也可能导致OOM异常,所以单独列出来。JDK1.4后新加入NIO类,引入了一种基于通道与缓冲区的IO方式,它可以使用Native函数直接分配对外内存,然后通过堆内DirectByteBuffer对象对其引用以提升性能。因此,该内存不受java堆大小限制,而受本机总内存(RAM、SWAP等)限制.

 

二 垃圾收集器与内存分配策略

(一) 对象生命周期

1. 引用计数算法

该算法实现较简单,判断效率也很高,其实现方法为:每个对象一个引用计数器,被引用时,计数器 + 1;引用失效,计数器 - 1,当计数为0,对象就不可再被使用。但是该算法很难解决对象之间相互循环引用的问题

如:

JVM学习系列(一)—内存模型与垃圾收集

这两个对象已经不可能再被访问,但是因为它们互相引用,导致它们的引用计数都不为0,于是,引用无法被回收。

2.可达性分析

可达性分析算法(GC Roots),该算法通过一系列GC Roots对象为起点,从这些节点往下搜索,搜索走过的路径称之为引用链,当一个对象到Gc Roots没有任何引用链相连,则该对象不可达也就是该对象不可用。

JVM学习系列(一)—内存模型与垃圾收集

 

GC Roots的对象包括下面几种:

1. 虚拟机栈(栈帧中的本地变量表)中引用的对象。

2. 方法区中类静态属性引用的对象。

3. 方法区中常量引用的对象。

4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。

补充:

对象不可达并不意味该对象的死亡,要宣告一个对象的死亡,需经过两次标记(筛选条件为是否有必要执行finalize方法),如果某个对象被判定为有必要执行finalize()方法,那么这个对象会被放置在一个F-Queue队列中,稍后由虚拟机自动创建的、低优先级Finalizer线程去执行,但并不承诺它会运行结束(可以理解为异步的,因为该线程如果执行缓慢或发生死循环可能会导致整个内存回收系统崩溃)。也就是说,一个对象可在finalize方法中被重新引用,逃脱死亡,但任何对象的finalize方法只会被系统自动调用一次(因此对象最多只能自救一次)。该方法由于不确定太大,所以并不提倡使用。

 

3.引用类型

在JDK1.2后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(PhantomReference)

强引用:new Object()引用,只要引用在,对象就不会被垃圾收集器回收。

软引用:SoftReference类实现引用,内存足够,对象就不会被回收。

弱引用:WeakReference类实现引用,不论内存是否足够,当垃圾收集器工作时,对象就会被回收。

虚引用:PhantomReference类实现引用,对对象的生存周期没有任何影响,唯一作用是在对象被回收时,会收到一个系统通知。

 

4. 回收方法区

永久代的垃圾收集为非必需实现,因为其效率较远低于新生代回收效率,但目前开源框架经常使用反射、动态代理、CGLIB、JSP等这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

永久代垃圾收集主要分为两部分:废弃常量无用的类,回收常量相对简单,只要当前系统没有任何对用该常量,该常量会被回收。而无用类的回收则必须满足:1. 该类所有实例都已被回收(java堆中不存在该类的任何实例)2.加载该类的ClassLoader已经被回收 3.该类的java,lang.Class对象没有在任何地方被引用(无法在任何地方通过反射获取该类的方法)。

(二) 垃圾收集算法

1. 标记-清除算法

顾名思义,该算法分为两个阶段,标记和清除,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它有两个不足之处:1.标记和效率都不太高  2.标记清除后会产生大量不连续的内存碎片,碎片较多,在分配较大的对象时,因为无法扎到足够的连续内存而触发另一次垃圾收集动作。

JVM学习系列(一)—内存模型与垃圾收集

 

2. 复制算法

把内存区域分成等量的两块,每次只使用其中一块,当这一块内存用尽后,将存活的对象复制到另一块内存,然后清空已使用过的内存空间,这样每次都是对半区进行内存回收,因此不存在碎片问题。实现简单,运行高效,同时代价也较大,将内存缩小为原先的一半,同时在对象存活率比高时就要进行较多的复制操作,效率会变低。

现在的商业虚拟机都是采用这种算法,因为新生代的对象存活率不高,因此并不需要按照1:1来分配内存空间(默认Eden:Survivor为8:1分配),也就是10%的内存会被“浪费",这里会出现一个状况,因为我们没法保证每次回收都只有不多于10%的对象存活,当Survivor不够用时,需要依赖其他内存(这里值老年代)进行分配担保

JVM学习系列(一)—内存模型与垃圾收集

 

3. 标记-整理算法

复制算法在对象存活率比高时就要进行较多的复制操作,效率会变低,如果不想浪费较多的空间,就需要额外的空间进行分配担保,因为老年代对象存活率较高,因此一般不适用复制算法。根据老年代的特点提出了”标记-整理“算法。

此标记和标记-清除的标记一样,整理则是让所有存活的对象都向一端移动,然后清理掉边界以外的内存。

JVM学习系列(一)—内存模型与垃圾收集

补充:

当前商业虚拟机都采用”分代收集“算法,一般把Java堆分为新生代、老年代。新生代对象存活率较低因此比较适合使用复制算法,老年代存活率高并且没有额外空间对它担保,就必须使用标记-清理\标记-整理算法。

 

(三) 垃圾收集器

1. 新生代

             新生代垃圾收集器均是采用复制算法。

(1)  Serial收集器

            如图所示,Serial收集器是一个单线程的收集器,“单线程”并不意味着它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。比如说你的计算机每运行一个小时就会暂停5分钟,这会怎样? 当然这并不是说Serial收集器就是一个鸡肋,它依然简单、高效(与其他收集器的单线程比),因此,比较比较适合Client模式,JVM内存占用不大,停顿时间较短。

JVM学习系列(一)—内存模型与垃圾收集

 

(2) ParNew收集器

如图所示,ParNew收集器其实就是Serail收集器的多线程版本,其余行为(所有控制参数、收集算法、对象分配规则、回收策略等)与Serail收集器一致,在实现上,两者也共用相当多的代码。

JVM学习系列(一)—内存模型与垃圾收集

 

(3) Parallel Scavenge收集器

Parallel Scavenge收集器是一个并行的多线程收集器它的关注点与其他收集器不同,其他收集器关注点主要是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。该收集器还有一个特点是自适应调节策略,设置好最大堆,然后设置最大停顿时间或者吞吐量,JVM会自动优化其余具体细节参数。

JVM学习系列(一)—内存模型与垃圾收集

2. 老年代

             老年代收集器算法不尽相同。

(1) Serial Old 收集器(标记-整理)

      Serial Old 收集器和Serial 收集器类似,同样是一个单线程收集器,使用标记整理算法,主要也是在Client模式下使用

JVM学习系列(一)—内存模型与垃圾收集

 

(2) Parallel Old 收集器(标记-整理)

          Parallel Old 收集器则是Parallel Scavenge收集器的老年代版本,使用多线程“标记-整理”算法。在注重吞吐量以及以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge + Parallel Old收集器。

JVM学习系列(一)—内存模型与垃圾收集

 

(3) CMS 收集器(标记-清除)

            CMS收集器一款真正意义上的并发收集器,以获取最短回收停顿事件为目标的收集器。整个收集过程分为4个步骤:初始标记 -> 并发标记 -> 重新标记 -> 并发清除,其中初始标记和重新标记仍需要“Stop The World”。

CMS收集器有3个明显的缺点:

1. 对CPU资源非常敏感,虽然不会导致用户线程停顿,但会因为占用一部分CPU资源而导致应用变慢,总吞吐量降低。默认回收线程为(CPU数量 + 3)/4,也就是说适合多CPU环境,当CPU数量不足4个,CMS对用户程序的影响就比较大。

2. 无法处理浮动垃圾,可能出现“Concurrent Model Failure”失败而导致Full GC的产生,原因是因为CMS并发清理阶段,用户线程仍在制造“垃圾”。因此CMS不能等到老年代空间几乎被沾满再进行收集,需要预留一部分空间,JDK5默认空间为68%,也就是老年代内存达到68%,收集线程就会被**,而JDK6以后,其启动阈值以提升到92%。

上面说到可能出现“Concurrent Model Failure”失败,那么失败后会怎样呢?CMS默认失败后启动后备预案,临时启用Serial Old 收集器来重新进行老年代的垃圾收集。

 

JVM学习系列(一)—内存模型与垃圾收集

3. CMS收集器基于“标记-清除”算法,自然会造成大量内存碎片,不得不提前进行Full GC。因此CMS收集器提供 + UseCMSCompactAtFullCollection参数用于Full GC之前进行内存碎片合并。

3. G1收集器

          G1收集器堆内存划分不同于传统收集器,从整体上看,是基于“标记-整理”算法,但从局部(Region)上而言,确实“标记-清除”算法,它主要是将Java堆划分为多个大小相等的区域(Region),虽然仍保留新生代和老年代的概念,但它们不再物理隔离而是一部分Region(不需要连续)的集合。主要特征:并行和并发、分代收集 、空间整合和可预测的停顿。其操作步骤和CMS类似: 初始标记 -> 并发标记 -> 最终标记 -> 筛选回收。

补充:

并行: 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

并发:指用户线程和垃圾收集线程同时执行(不一定并行执行,可能交替执行),用户线程在继续运行,而垃圾收集线程运行于另一CPU上。

JVM学习系列(一)—内存模型与垃圾收集

如:本地使用JDK8

JVM学习系列(一)—内存模型与垃圾收集

对照上面参数得到JDK8的默认垃圾收集器是:Parallel Scavenge + Serial Old(PS MarkSweep)的组合

(四) 内存分配、回收策略

1. 对象优先在Eden分配

2. 大对象直接进入老年代

3. 长期存活的对象将进入老年代

4. 动态对象年龄判定

         相同年龄对象所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代

5. 空间分配担保

在发生Minor Gc(老年代gc)之前,虚拟机先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果是,则Minor GC安全,如果小于,设置HandlePromotionFailure是否冒险(冒Full GC的风险),避免频繁Full GC。