jvm-GC收集器(二)

jvm-GC收集器(二)

我们需要思考GC的3件事

  • 哪些内存需要回收
  • 什么时候回收?
  • 如何回收?

那什么是垃圾呢?

所谓“垃圾”,就是指所有不再存活的对象。在jvm堆里面几乎放着java世界中所有的对象,垃圾收集器在对堆进行收集时需要判断哪些还存活着,哪些已经死了。
常见的判断是否存活有两种方法:引用计数法和可达性分析。

引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,当引用失效时,计数器值就减1,任何时刻计数器为0的对象就是不可能再被使用的。

优点:引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的选择
缺点:Java虚拟机并没有选择这种算法来进行垃圾回收,主要原因是它很难解决对象之间的相互循环引用问题。A引用了B,B又引用了A,这样是无法回收A,B的。

可达性分析算法

主流的商用程序语言(Java,C#)的主流实现中,都是通过可达性分析(Reachability Analysis)来判定对象是否存活的。

算法的基本思想就是通过一系列的称为“GC Roots”的对象作为起始点,
从这些起始点开始向下搜索,所走过的路径称为引用链,
如果一个对象到GC Roots没有任何引用链,那么这个对象是不可用的,就是说,程序中没有谁引用了这个对象,所以可以说从根节点到叶子结点是不可达的。

Java中,以下对象可作为GC Roots对象:

  • 虚拟机栈(栈帧中本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(也就是native本地方法)引用的对象

那么什么叫做不可达?

Java中那些不可达的对象就会变成垃圾。?其实就是没有办法再引用到该对象了。
主要有以下情况使对象变为垃圾:
1.对非线程的对象来说,所有的活动线程都不能访问该对象,那么该对象就会变为垃圾。
2.对线程对象来说,满足上面的条件,且线程未启动或者已停止。

再谈引用

我们希望一类对象当内存空间还足够时,能够保留在内存中,如果内存空间回收后还是很紧张则抛弃这些对象。
jdk1.2之后java对引用的概念进行了扩充,分为强引用,软引用,弱引用,虚引用。

  • 强引用:类似Object obj=new Object();这类的引用只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用:用来描述一些还有用但非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列为第二次回收范围之中。
  • 弱引用:描述非必须的对象,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当GC工作时,无论当前内存是否足够,都会回收掉只被弱引用的对象。weakReference类来实现弱引用
  • 虚引用:一个对象是否有虚引用完全不会对其生存构成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

强>软>弱>虚

生存还是死亡(两次标记)

要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,将会被标记一次并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。

finalize()方法
- 如果一个对象被判定为有必要执行finalize()方法,那么这个对象将会放置在一个叫做F-Queue队列中,并稍后由一个虚拟机自动建立的,低优先级的Finalizer线程去执行它。
- 任何一个对象的finalize()方法只会被系统自动调用一次。

回收方法区

前面说了,Java的内存回收主要实在方法区和Java堆中,Java堆中的新生代,因为新生代的存活时间比较短,所以对新生代进行垃圾回收回收的空间比较大,但是方法区中的永久代则由于可能存活时间较长,所以下一次的垃圾回收回收该对象的可能性没有新生代那么大。所以对永久代的回收效率会大打折扣。但是这部分对象仍然是需要回收。

永久代的垃圾回收包括两部分:废弃常量和无用的类

废弃常量的回收比较好理解,因为只要没有任何对象引用常量池中的某个对象,那么这个对象就会被回收。前面说的是非常量池中的对象,废弃常量回收的是运行时常量池中的对象,所以只需要一次标记就好。

无用的类回收需要满足以下三个条件才可以宣判一个类的“死刑”:

  • 该类的所有实例都已经被回收,也就是Java堆中不存在该类的实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

注意上面的“可以”而非“必然”。与对象的回收不同,是否需要对类进行回收,需要设置相关的参数才行

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

垃圾收集算法

标记-清楚算法

算法的执行过程与名字一样,先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。该算法有两个问题:

  • 标记和清除过程效率不高。主要由于垃圾收集器需要从GC Roots根对象中遍历所有可达的对象,并给这些对象加上一个标记,表明此对象在清除的时候被跳过,然后在清除阶段,垃圾收集器会从Java堆中从头到尾进行遍历,如果有对象没有被打上标记,那么这个对象就会被清除。显然遍历的效率是很低的
  • 会产生很多不连续的空间碎片,所以可能会导致程序运行过程中需要分配较大的对象的时候,无法找到足够的内存而不得不提前出发一次垃圾回收。

复制算法

复制算法是为了解决标记-清除算法的效率问题的,其思想如下:将可用内存的容量分为大小相等的两块,每次只使用其中的一块,当这一块内存使用完了,就把存活着的对象复制到另外一块上面,然后再把已使用过的内存空间清理掉。

优点:每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:算法的代价是将内存缩小为了原来的一半,未免太高了一点。

现在的商业虚拟机都采用这种收集算法来回收新生代,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1∶1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。

当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
HotSpot虚拟机默认Eden和Survivor0,Survivor1的大小比例是8∶1:1,也就是每次新生代中可用内存空间为整个新生代容量的90%,只有10%的内存会被“浪费”。

当然,90%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时(例如,存活的对象需要的空间大于剩余一块Survivor的空间),需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

对象在收集的过程中发生了什么?

工作原理如下:

  • 首先,Eden区最大,对外提供堆内存。当 Eden 区快要满了,则进行 Minor GC,把存活对象放入 Survivor A 区,清空 Eden 区;
    Eden区被清空后,继续对外提供堆内存;
  • 当 Eden 区再次被填满,此时对 Eden 区和 Survivor A 区同时进行 Minor GC,把存活对象放入 Survivor B 区,同时清空 Eden 区和Survivor A 区;
    Eden区继续对外提供堆内存,并重复上述过程,即在 Eden 区填满后,把 Eden 区和某个 Survivor 区的存活对象放到另一个 Survivor 区;
  • 当某个 Survivor 区被填满,且仍有对象未被复制完毕时,或者某些对象在反复 Survive 15 次左右时,则把这部分剩余对象放到Old 区;
  • 当 Old 区也被填满时,进行 Major GC(Full GC),对 Old 区进行垃圾回收。
    [注意,在真实的 JVM 环境里,可以通过参数 SurvivorRatio 手动配置 Eden 区和单个 Survivor 区的比例,默认为 8。]

永久代存了一些什么,和方法区有什么关系吗?

标记-整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

与标记-清除算法过程一样,只不过在标记后不是对未标记的内存区域进行清理,二是让所有的存活对象都向一端移动,然后清理掉边界外的内存。该方法主要用于老年代。

分代收集算法

目前商用虚拟机都使用“分代收集算法”,所谓分代就是根据对象的生命周期把内存分为几块,一般把Java堆中分为新生代和老年代,这样就可以根据对象的“年龄”选择合适的垃圾回收算法。

新生代:“朝生夕死”,存活率低,使用复制算法。
老年代:存活率较高,使用“标记-清除”算法或者“标记-整理”算法。

Java 的堆结构

在选择回收算法前,我们先来看一下 Java 堆的结构。
一块 Java 堆空间一般分成三部分,这三部分用来存储三类数据:

  • 刚刚创建的对象。在代码运行时会持续不断地创造新的对象,这些新创建的对象会被统一放在一起。因为有很多局部变量等在新创建后很快会变成 不可达 的对象,快速死去 ,因此这块区域的特点是 存活对象少,垃圾多 。形象点描述这块区域为: 新生代;
  • 存活了一段时间的对象。这些对象早早就被创建了,而且一直活了下来。我们把这些 存活时间较长 的对象放在一起,它们的特点是 存活对象多,垃圾少 。形象点描述这块区域为: 老年代;
  • 永久存在的对象。比如一些静态文件,这些对象的特点是不需要垃圾回收,永远存活。形象点描述这块区域为:永久代 。(不过在 Java 8 里已经把 永久代 删除了,把这块内存空间给了 元空间)

也就是说,常规的 Java 堆至少包括了 新生代 和 老年代 两块内存区域,而且这两块区域有很明显的特征:
新生代:存活对象少、垃圾多
老年代:存活对象多、垃圾少

HotSpot的算法实现

上面讲了判断对象死亡,然后是怎么通过算法来回收这些死去的对象,而现在讲的hotspot是在上面讲的垃圾算法中进行优化,优化了一套自己的垃圾回收算法,当然也只是优化,并不是自己搞出一套什么新东西来。

GC Roots与GC停顿

我们回到标记-清除算法,在清除阶段,为了枚举未被标记的对象,所以需要从根节点(GC Roots)开始查找引用链,这个过程会导致GC停顿,意思就是在GC的时候Java的执行线程都被停顿,好像被冻结在某一个时间点,也叫“Stop the world”。然而目前主流的Java虚拟机都是用准确式GC(所谓准确式GC,即使虚拟机知道内存中的某个位置的数据是什么类型),当“Stop the world”的时候并不需要检查所有的引用位置,虚拟机通过使用OopMap这个数据结构知道哪些地方存放着对象的引用。

类加载完成后,HotSpot将对象内什么偏移量上是什么类型的数据计算出来;在JIT编译过程中,在特定的位置记录下栈和寄存器(程序计数器)中哪些位置是引用。

安全点

使用OopMap,虚拟机已经知道哪些位置存放着对象(知道了位置避免全表扫描),从而GC Roots可以迅速的枚举可达对象的引用链。但是问题来了:是不是需要对所有的指令都使用OopMap呢?答案是否定的。实际上,虚拟机只在“特定的位置”记录了对象的引用信息,比如我们使用方法调用或者循环的时候,就会设定这样的位置,如果越过这个位置的继续执行指令,然而程序是不允许因为指令流长度太长而执行过长时间,所以这个“特定位置“就成为了程序是否具有长时间运行的分界点。这个”特定的位置“也称为安全点(SafePoint)。

主动式中断线程

现在虚拟机有了安全点,于是只会到安全点寻找对象的引用信息,并且在安全点暂停Java执行线程,然而还有一个问题如何在GC发生时让所有线程(不包括JNI调用的线程)都“跑”到最近的安全点上再停顿下来?

在Hotspot使用主动式中断来中断执行线程,其思想如下:当GC的时候不需要直接对线程操作以中断线程,仅仅是设置一个标志,然后让执行线程去轮询这个标志,发现中断标志为真的时候就自己中断线程。需要注意的是,轮询标志的地方与安全点的位置是重合的,另外再加上创建对象需要分配的地方。

安全区域

现在通过GC Roots和安全点,程序能够在不太长的时间就可以到达安全点,并暂停执行线程。那么如果程序在阻塞(Blocked)或者睡眠(Sleep)的状态的时候,执行线程如何中断呢?

JVM设置了安全区域(Safe Region)。安全区域可以看成是被扩展了的安全点,是指在这个区域内,对象的引用关系不会发生改变,在这个区域内的任何地方开始GC都是安全的。

垃圾收集器

前面讲了很多,开始是怎么判断对象死亡,然后讲了怎么回收这些垃圾对象,然后是hotspot的算法优化,现在才开始真正开始hotspot里面真正的实现。

hotspot的垃圾收集器有很多,大致分为两种收集器:老年代收集器,新生代收集器。

可以看到垃圾收集器是按对象的分代来划分的,可以用双箭头连接的垃圾收集器表示两者可以配合使用。可以看到新生代垃圾收集器有Serial、ParNew、Parallel Scavenge,G1,属于老年代的垃圾收集器有CMS、Serial Old、Parallel Old和G1.其中的G1是一种既可以对新生代对象也可以对老年代对象进行回收的垃圾收集器。然而,在所有的垃圾收集器中,并没有一种普遍使用的垃圾收集器。在不同的场景下,每种垃圾收集器有各自的优势。

知识科普一下

并发和并行

这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,它们可以解释如下。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

Minor GC 和 Full GC

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

吞吐量

吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即
吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器。它是一种单线程垃圾收集器,这就意味着在其进行垃圾收集的时候需要暂停其他的线程,也就是之前提到的”Stop the world“。虽然这个过程是在用户不可见的情况下把用户正常的线程全部停掉,听起来有点狠,这点是很难让人接受的。Serial、Serial Old收集器的工作示意图如下

尽管由以上不能让人接受的地方,但是Serial收集器还是有其优点的:

  1. 简单而高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得较高的手机效率.
  2. 到目前为止:Serial收集器依然是Client模式下的默认的新生代垃圾收集器
  3. 采用新生代垃圾采用复制算法,老年代采用标记-整理算法
    通过serial收集器可以看出hotspot的GC实际工作,标记安全点,在安全点开始收集,使用上面提到过的各种算法,把2中算法组合起来,成为一个收集器。

ParNew收集器

ParNew收集器是Serial收集器的多线程版本,它们之间收集算法,是stop the world ,分配规则,回收策略都一样。ParNew收集器的工作示意图如下: ![](https://img-blog.csdn.net/20160505192308164) ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。除去性能因素,很重要的原因是除了Serial收集器外,目前只有它能与CMS收集器配合工作。 但是,在单CPU环境中,ParNew收集器绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。

Parallel Scavenge收集器

Parallel Scavenge收集器是新生代垃圾收集器,使用复制算法,也是并行的多线程收集器。与ParNew收集器相比,很多相似之处,但是Parallel Scavenge收集器更关注可控制的吞吐量。吞吐量越大,垃圾收集的时间越短,则用户代码则可以充分利用CPU资源,尽快完成程序的运算任务。 Parallel Scavenge收集器使用两个参数控制吞吐量:
  • XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
  • XX:GCRatio 直接设置吞吐量的大小。
    直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。停顿时间下降的同时,吞吐量也下降了。
除此之外,Parallel Scavenge收集器还可以设置参数-XX:+UseAdaptiveSizePocily来动态调整停顿时间或者最大的吞吐量,这种方式称为**GC自适应调节策略**,这点是ParNew收集器所没有的。

Serial Old收集器

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

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法进行垃圾回收。其通常与Parallel Scavenge收集器配合使用,“吞吐量优先”收集器是这个组合的特点,在注重吞吐量和CPU资源敏感的场合,都可以使用这个组合。 ![](https://img-blog.csdn.net/20160505192422531)

CMS收集器

CMS收集器(Concurrent Mark Sweep)的目标就是**获取最短回收停顿时间**。在注重服务器的响应速度,希望停顿时间最短,则CMS收集器是比较好的选择。 整个执行过程分为以下4个步骤:
  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除
    初始标记和重新标记这两个步骤仍然需要暂停Java执行线程,初始标记只是标记GC Roots能够关联到的对象,并发标记就是执行GC Roots Tracing的过程,而重新标记就是为了修正并发标记期间因用户程序执行而导致标记发生变动使得标记错误的记录。其执行过程如下:

    由上图可知,整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,因此,总体上CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS的优点很明显:**并发收集、低停顿**。由于进行垃圾收集的时间主要耗在并发标记与并发清除这两个过程,虽然初始标记和重新标记仍然需要暂停用户线程,但是从总体上看,这部分占用的时间相比其他两个步骤很小,所以可以认为是低停顿的。 尽管如此,CMS收集器的缺点也是很明显的:
  • 对CPU资源太敏感,这点可以这么理解,虽然在并发标记阶段用户线程没有暂停,但是由于收集器占用了一部分CPU资源,导致程序的响应速度变慢,CMS默认开启的回收线程数是(CPU数量+3)/4,当CPU在4个以上,CMS将占用25%的CPU资源,也就是随着CPU数量增加而下降。
  • CMS收集器无法处理浮动垃圾。所谓的“浮动垃圾”,就是在并发标记阶段,由于用户程序在运行,那么自然就会有新的垃圾产生,这部分垃圾被标记过后,CMS无法在当次集中处理它们(为什么?原因在于CMS是以获取最短停顿时间为目标的,自然不可能在一次垃圾处理过程中花费太多时间),只好在下一次GC的时候处理。这部分未处理的垃圾就称为“浮动垃圾”
  • 由于CMS收集器是基于“标记-清除”算法的,前面说过这个算法会导致大量的空间碎片的产生,一旦空间碎片过多,大对象就没办法给其分配内存,那么即使内存还有剩余空间容纳这个大对象,但是却没有连续的足够大的空间放下这个对象,所以虚拟机就会触发一次Full GC(这个后面还会提到)这个问题的解决是通过控制参数-XX:+UseCMSCompactAtFullCollection,用于在CMS垃圾收集器顶不住要进行FullGC的时候开启空间碎片的合并整理过程。

G1收集器

G1(Garbage-First)收集器是现今收集器技术的最新成果之一,之前一直处于实验阶段,直到jdk7u4之后,才正式作为商用的收集器。 与前几个收集器相比,G1收集器有以下特点:
  • 并行与并发
  • 分代收集(仍然保留了分代的概念)
  • 空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
  • 可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)
    此外,使用G1收集器将Java堆内存布局与其他收集器有很大差别,它将整个java对分为多个大小相等的Region(独立区域),虽然还保留了新生代与老年代的概念,但新生代与老年代不再是物理隔离了,新生代与老年代都是一部分Region的集合,G1的收集范围则是这一个个Region(化整为零)。

问题:老年代与新生代被装进混合装进了一个个小盒子里面了,每个盒子里面既有新生代由于老年代,垃圾回收时就会遇到新的问题。
解决问题:虚拟机都是使用Remembered Set避免GC时全表扫描。G1更狠,在每个Region都有一个与之对应的Rememberd set 。具体操作很复杂就不讲了,讲了也不了解。

G1收集器运作大致可分为以下几个步骤:
  • 初始标记(Initial Marking):GC Roots能够直接关联的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段的用户程序并发运行的时候,能在正确可用的Region中创建对象,这个阶段需要暂停线程
  • 并发标记(Concurrent Marking):从GC Roots进行可达性分析,找出存活的对象,这个阶段食欲用户线程并发执行的
  • 最终标记(Final Marking):则是修正在并发标记阶段因为用户程序的并发执行而导致标记产生变动的那一部分记录,这部分记录被保存在Remembered Set Logs中,最终标记阶段再把Logs中的记录合并到Remembered Set中,这个阶段是并行执行的,仍然需要暂停用户线程
  • 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划 。
整个操作如图所示: ![](https://img-blog.csdn.net/20160505194916580)

垃圾收集器常用参数总结

![](https://img-blog.csdn.net/20160505195835780) ![](https://img-blog.csdn.net/20160505195904008) 从上面可以看出
  1. 新生代垃圾收集器都是采用”复制算法“,老年代垃圾收集都是采用“标记-整理,或者标记-清除算法”
  2. 垃圾回收时无论如何都会stop the world的,只是时间长短的问题。

内存分配与回收策略

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。
对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

本文中的内存分配策略指的是Serial / Serial Old收集器下(ParNew / Serial Old收集器组合的规则也基本一致)的内存分配和回收的策略。

对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。虚拟机的参数-XX:PrintGCDetails可以打印内存回收日志。
测试用例请参看:
http://blog.csdn.net/u011080472/article/details/51326003

大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

大对象对虚拟机的内存分配来说就是一个坏消息(比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(复习一下:新生代采用复制算法收集内存)。

**注意:**PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器一般并不需要设置。如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的收集器组合。

详细测试用例请参看:《深入理解java虚拟机》94页。
或者http://blog.csdn.net/u011080472/article/details/51326003

动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代.
动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

下面解释一下“冒险”是冒了什么风险,前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁

上面按照自己的意思和排版做了一遍,并加入了一些自己的想法,大量文字参照了:

http://blog.csdn.net/u011080472/article/category/6214467
《深入理解java虚拟机》

猜你喜欢

转载自blog.csdn.net/piaoslowly/article/details/81458879