深刻理解JVM(三)垃圾收集器与内存分配策略

1.如何断定对象已死java

判断对象是否已死有两种方法,一种是引用计数法,另外一种是可达性分析算法。算法

1.1引用计数法数据库

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器就减一;任什么时候刻为0的对象就是不愿再被使用的。数组

1.2.可达性分析缓存

经过一系列的称为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路称为引用链,当一个对象到GC Roots没有人格引用链项链,则证实对象是不可用的。在java语言中,能够做为GC Roots的对象包括下面几种:多线程

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

1.2.2.方法区中静态属性引用的对象。框架

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

1.2.4.本地方法栈中JNI(即通常说的Native方法)引用的对象。 2.java中的四种引用布局

2.1. 强引用:相似“Object o = new Object()”,只要强引用还存在,就不会被回收;

2.2.软引用:用来描述一些有用但非必须的对象。若是一个对象只具备软引用,则内存空间足够, 垃圾回收器就不会回收它;若是内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没 有回收它,该对象就能够被程序使用。软引用可用来实现内存敏感的高速缓存(若是内存够,软 引用没有被回收,则能够直接使用,若是内存不够,软引用已经被回收,则从新读取数据(如从 数据库中))。(java.lang.ref 包)SoftReferencesoftRef = new SoftReference(str);

2.3.弱引用:也是用来描述非必须对象的,可是它的强度比软引用更弱一些,被弱引用关联的对 象只能生存到下一次垃圾收集发生以前。当垃圾收集器工做时,不管当前内存是否足够,都会回 收只被弱引用关联的对象。若是这个对象是偶尔的使用,而且但愿在使用时随时就能获取到,但 又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象。

2.4.虚引用:它是最弱的一种引用关系,一个对象是否有虚引用的存在,彻底不会对其生存时间 构成影响,也没法经过虚引用来取得一个对象实例。为一个对象设置虚引用的惟一目的就是能在 这个对象被收集器回收时收到一个系统通知。jdk1.2之后,提供了PhantomReference类来实现虚引用。 3.回收无效对象的过程

finalize()方法

即便在可达性分析算法中不可达的对象,也并不是是“非死不可”的,这时候它们暂时处于 “缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程。标记的前提是对象在进 行可达性分析后发现没有与 GC Roots 相链接的引用链。

3.1.第一次标记并进行一次筛选。

筛选的条件是此对象是否有必要执行 finalize()方法。当对象没有覆盖 finalize 方法,或者 finalize 方法已经被虚拟机调用过(finalize 只会调用一次),虚拟机将这两种状况都视为“没 有必要执行”,对象被回收。

3.2.第二次标记

若是这个对象被断定为有必要执行 finalize()方法,那么这个对象将会被放置在一个 名为:F-Queue 的队列之中,并在稍后由一条虚拟机自动创建的、低优先级的 Finalizer 线 程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。 这样作的缘由是,若是一个对象 finalize()方法中执行缓慢,或者发生死循环(更极端的 状况),将极可能会致使 F-Queue 队列中的其余对象永久处于等待状态,甚至致使整个内存 回收系统崩溃。

finalize()方法是对象脱逃死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对 象进行第二次小规模标记,若是对象要在 finalize()中成功拯救本身----只要从新与引用 链上的任何的一个对象创建关联便可,譬如把本身赋值给某个类变量或对象的成员变量,那 在第二次标记时它将移除出“即将回收”的集合。若是对象这时候还没逃脱,那基本上它就真 的被回收了。 4.回收方法区

方法区中主要清除两种垃圾:

4.1. 废弃常量

4.2. 无用的类

4.1.1.判断废弃的常量

清除废弃的常量和清除对象相似,只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。

4.2.1.如何判断废弃的类

清除废弃类的条件较为苛刻:

4.2.1. 该类的全部对象都已被清除。

4.2.2. 该类的java.lang.Class对象没有被任何对象或变量引用。

只要一个类被虚拟机加载进方法区,那么在堆中就会有一个表明该类的对象:java.lang.Class。这个对象在类被加载进方法区的时候建立,在方法区中该类被删除时清除。

4.2.3. 加载该类的ClassLoader已经被回收。 5.垃圾收集算法

5.1.标记-清除算法(Mark-Sweep)。

首先标记处全部须要回收的对象,在标记完成后统一回 收。缺点:标记和清除两个过程都效率低;标记清除后会产生大量不连续的内存碎片,空间 碎片太多可能会致使之后在程序运行中须要分配大对象时,没法找到足够的连续内存而不得 不提取触发 GC。

5.2.复制算法。

将可用内存按容量划分红大小相等的两块,每次只使用一块。当这一块使用 完了,就将还存活着的对象复制到另外一块上面,而后再把已使用过的内存一次清理掉。这样 不用考虑内存碎片的问题,只要移动堆顶指针,按顺序分配便可,实现简单、运行高效。缺点:内存缩小为原来的一半。现代商用虚拟机都采用这种算法回收新生代。而新生代中约 98%的对象都是“朝生夕死”,因此不需按 1:1 划分。HotSpot 默认 Eden 和 Survivor 是 8:1,因此每次可用内存为 90%。但 咱们无法保证每次回收只有很少于 10%的对象存活,当 Survivor 空间不够时,须要依赖其余 内存(这里指老年代)进行分配担保(直接进入老年代)。

缺点:若是对象存活率过高,要进行较多复制操做,效率低。且须要额外空间担保,老 年代不能选用这种算法。

三、标记-整理算法。

过程与“标记-清除”同样,但后续步骤不是直接对可回收对象进行清理,而是让全部存 活的对象都向一端移动,而后直接清理掉端边界之外的内存。老年代由于对象存活率高、没 有额外空间进行分配担保,必须使用“标记-清理”或“标记-整理”算法。

  1. 分代收集算法

将内存划分为老年代和新生代。老年代中存放寿命较长的对象,新生代中存放“朝生夕死”的对象。而后在不一样的区域使用不一样的垃圾收集算法。 6.JVM垃圾收集器

6.1.Serial 是一个单线程收集器,在它进行垃圾收集时,必须暂停其余全部工做线程(StopTheWorld);简单高效,是虚拟机在 Client模式下默认的新生代收集器(复制算法)。停顿 时间在几十到一百多毫秒之内,能够接受。

6.2.ParNew 其实就是 Serial 收集器的多线程版本;ParNew 收集器是许多运行在 Server模式下的虚拟机中首选的新生代收集器。除去性能因素,很重要的缘由是除了 Serial 收集 器外,目前只有它能与 CMS收集器(老年代)配合工做。(复制算法)

可是,在单 CPU环境中,ParNew收集器绝对不会有比 Serial 收集器更好的效果,甚至 因为存在线程交互的开销,该收集器在经过超线程技术实现的两个 CPU的环境中都不能百分 之百地保证能够超越 Serial收集器。然而,随着可使用的 CPU的数量的增长,它对于 GC 时系统资源的有效利用仍是颇有好处的。

6.3.Parallel Scavenge 收集器是新生代垃圾收集器,使用复制算法,也是并行的多线程 收集器。与 ParNew 收集器相比,不少类似之处,可是 Parallel Scavenge 收集器更关注可控 制的吞吐量(运行用户代码时间/(运行用户代码+垃圾收集时间))。吞吐量越大,垃圾收集 的时间越短,则用户代码则能够充分利用 CPU 资源,尽快完成程序的运算任务。

直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,可是 GC 停顿时间的缩 短是以牺牲吞吐量和新生代空间做为代价的。好比原来 10 秒收集一次,每次停顿 100 毫秒,如今变成 5 秒收集一次,每次停顿 70 毫秒。停顿时间降低的同时,吞吐量也降低了。

6.4.Serial Old 收集器是 Serial收集器的老年代版本,也是一个单线程收集器,采用“标记-整理算法”进行回收。其运行过程与 Serial收集器同样。SerialOld收集器的主要意义也是在于给 Client 模式下的虚拟机使用。若是在 Server模式下,那么它主要还有两大用途:一种用途是在 JDK 1.5 以及以前的版本中与 Parallel Scavenge收集器搭配使用,另外一种用途就是做为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure时使用。

6.5.Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法进行垃圾回收。其一般与 Parallel Scavenge 收集器配合使用,“吞吐量优先”收集器 是这个组合的特色,在注重吞吐量和 CPU 资源敏感的场合,均可以使用这个组合。

6.6.CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法,从整体上来讲,CMS收集器的内存回收过程是与用户线程 一块儿并发执行的(有的过程也是 StopTheWorld)。

CMS分为四个步骤:初始标记(GCRoots能直接关联到的对象,速度快,可达性分析, Stop The World),并发标记(可达性分析),从新标记(修正并发标记期间因用户程序继续 运做而致使的变更,速度快,Stop The World),并发清除

CMS 的优势很明显:并发收集、低停顿。因为进行垃圾收集的时间主要耗在并发标记 与并发清除这两个过程,虽然初始标记和从新标记仍然须要暂停用户线程,可是从整体上看,

这部分占用的时间相比其余两个步骤很小,因此能够认为是低停顿的。

缺点:

对 CPU 资源太敏感,这点能够这么理解,虽然在并发标记阶段用户线程没有暂停,但 是因为收集器占用了一部分 CPU 资源,致使程序的响应速度变慢

CMS 收集器没法处理浮动垃圾。所谓的“浮动垃圾”,就是在并发标记阶段,因为用户程 序在运行,那么天然就会有新的垃圾产生,这部分垃圾被标记事后,CMS 没法在当次集中 处理它们(为何?缘由在于 CMS 是以获取最短停顿时间为目标的,天然不可能在一次垃 圾处理过程当中花费太多时间),只好在下一次 GC 的时候处理。这部分未处理的垃圾就称为“浮 动垃圾”。因为垃圾收集阶段用户线程还须要运行,那就不能等老年代几乎全满了再收集, 通常达到 92%时就开始收集,而 CMS 运行期间预留的内存没法知足程序须要,就会出现 “Concurrent Mode Failure”,此时将启动备用方案 serial old

因为 CMS 收集器是基于“标记-清除”算法的(多是为了时间短),前面说过这个算法会致使大量的空间碎片的产生,一旦空间碎片过多,大对象就没办法给其分配内存,那么即 使内存还有剩余空间容纳这个大对象,可是却没有连续的足够大的空间放下这个对象,因此 虚拟机就会触发一次 Full GC。

在使用 CMS收集老年代时,新生代只能选用 ParNew或者 Serial 收集器中的一个(CMS 与其余不配套,其余的没有使用传统的 GC 收集器框架)

6.7.(Garbage-First)收集器,JDK1.7 才开始商用。使用 G1 收集器时,Java 堆内存 布局与其余收集器有很大差异,它将整个 Java 堆分为多个大小相等的独立区域(Region), 虽然还保留新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,他们都是 Region(不须要连续)的集合。

特色:并行与并发。分代收集(不须要其余收集器配合)。空间整合(总体来看采用“标 记-整理”,局部(两个 Region 之间)采用复制)。可预测的停顿。

G1 跟踪各个 Region 里面的垃圾堆积价值大小(回收所得到的空间大小以及回收所需的 时间),在后台维护一个优先列表,每次优先收集价值最大的 Region(因此叫 Garbage-First),

从而保证了 G1 在有限时间内能够获取尽量高的收集效率。

(老年代)过程:初始标记(StopTheWorld)、并发标记、最终标记(StopTheWorld)、筛选回收(Stop The World)

G1 的 YoungGC 就是将 E 区和 S 区复制到灰色的空白区。

G1 中有 Humongous 区(巨大区)用于存放比标准块大 50%的对象

7.JVM垃圾回收机制

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算 法,只须要付出少许存活对象的复制成本就能够完成收集(有 eden和 survivor供复制,有 老年代最分配担保)。而老年代中由于对象存活率高、没有额外空间对它进行分配担保,就 必须使用“标记-清理”或者“标记-整理”算法来进行回收。

发生 Minor GC,采用复制算法,发现

7.1.复制对象没法所有放入 Survivor,只好经过分配担保机制提早转移到老年代中

7.2.大对象(长字符串或长数组等须要大量连续空间的对象)直接进入老年代(防止大

对象在 eden和 Survivor中常常复制)经过-XX:PretenureSizeThreshold 参数设置 (如 3MB),大于这个参数的直接进入老年代

7.3.长期存活对象进入老年代(默认 15岁)

Minor GC:新对象先放入 eden区,当 eden满了会触发 Minor GC。

Full GC(等于 Major GC):

7.3.一、每次进行 Minor GC 时,JVM 会计算 Survivor 区移至老年区的对象的平均大小,如 果这个值大于老年区的剩余值大小则进行一次 Full GC

7.3.二、老年代空间不足时触发 Full GC,只有在新生代对象转入或建立为大对象、大数组 时才会出现不足的现象(大对象直接进入老年代),分配担保

7.3.三、永久代满(永久代 JDK8被移除)

优化 Full GC 自己不会先进行 Minor GC,咱们能够配置,让 Full GC 以前先进行一次 Minor GC,由于老年代不少对象都会引用到新生代的对象,先进行一次 Minor GC能够提升老年代 GC 的速度。

在 jvm分带垃圾回收机制中,将应用程序可用的堆空间分为年轻代和老年代,又将年轻 代分为 eden区、from区、to 区,新建对象老是在 eden 区中被建立,当 eden区空间已满,

就触发一次 Minor gc,将还被使用的对象复制到 from 区,这样整个 eden 区都是未被使用 的空间,可供继续建立对象,当 eden区再次用完,再触发一次 Minor gc,将 eden 区和from 区还在被使用的对象复制到 to 区,下一次 Minor gc则是将 eden 区和 to区还被使用的对象 复制到 from区。所以,通过屡次 Minor gc,某些对象会在 from区和 to 区屡次复制,若是 超过某个阈值对象还未被释放,则将对象复制到老年代。若是老年代空间也已用完,那么就 会触发 full gc,即所谓的全量回收。

永久代的垃圾回收主要有两部分:废弃常量和无用的类。如没有任何 String对象引用 “abc”。在大量使用反射、动态代理、CGlib等 ByteCode框架,动态生成 JSP 以及 OSGi这 类频繁自定义 ClassLoader 的场景都须要虚拟机具有类卸载功能(回收永久代),以保证永 久代不会溢出。

相关文章
相关标签/搜索