深入理解Java虚拟机(第二版) 第三章:垃圾收集器与内存分配策略

3.1 概述

Java运行时各个区域,其中程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知。而Java堆和方法区这部分的内存的分配和回收都是动态的,垃圾收集器所关注的也是这部分。

3.2 判断对象是否已死

在判断java堆中的对象是否已死时,都是根据引用来判断的。

3.2.1 再谈引用

狭义定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。
希望能描述的对象:当内存足够时,能够保留在内存中;如果内存在垃圾收集之后还非常紧张,则可以抛弃这些对象。
JDK1.2之后对引用进行了扩展,将引用分为:强引用、软引用、弱引用、虚引用。

强引用:指在程序中普遍存在的,类似“Object obj = new Object();”这类引用,只有强引用存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用:描述一些有用但是非必须得对象。对于软引用关联的对象,在系统将要发生溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收还没有足够的内存才会抛出OOM异常。
弱引用:只能生存到下次回收垃圾之前。当垃圾回收器工作时无论内存空间是否足够都会被回收。
虚引用:也称幽灵引用或者幻影引用。一个对象是否具有虚引用对其生存不会构成任何影响。

3.2.2 引用记数法(Reference counting)

虚拟机并不是通过该算法来判断对象是否存活。

对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,则对象A就不可能再被使用。

存在的问题:很难处理对象的循环引用。根对象已经不可达,垃圾对象的相互引用造成垃圾对象的引用计数都不为1,所以就不会被回收。

3.2.3 可达性分析算法(Reachability Analysis)

主流虚拟机通过该算法来判断对象是否存活。
以一系列“GC Roots”(一组必须活跃的引用对象)的对象为起点,搜索所走过的路径称为引用链,当一个对象到GC roots没有任何引用链相连,就是从GC Roots到这个对象不可达,就说明该对象是不可用的,被判定为可回收的对象
深入理解Java虚拟机(第二版) 第三章:垃圾收集器与内存分配策略
可作为GC Roots的对象

  • 虚拟机栈中引用的对象;
  • 方法区中类静态属性应用的对象;
  • 方法区中常量引用的对象;
  • Native方法引用的对象。

3.2.4 对象的自救

即使在可达性分析法中不可达的对象,也并非“非死不可”。这时候他们处于“缓刑”阶段。要宣告一个对象的死亡,至少要经历两次标记的过程。

  1. 如果对象在进行可达性分析后没有与GC Root相连接的引用链,那么他会被第一次标记并进行一次筛选,筛选的条件是是否有必要执行finalize()。即:当对象没有被覆盖 finalize() 或者 finalize() 方法已经被虚拟机调用过了,虚拟机将这两种情况都是为没有必要执行
  2. 当一个对象被判断为有必要执行finalize()方法,那么这个对象会被放置到F-Queue队列中,并且稍后JVM自动建立一个低优先级的Finalizer线程执行它,这里“执行”是虚拟机会触发这个方法,但不会承诺等待它运行结束(万一这个方法运行缓慢或者死循环,F-Queue队列其他对象岂不是永久等待)。finalize()是对象逃脱死亡的最后一次机会。稍后GC会对F-Queue进行第二次小规模标记。如果对象能在finalize()方法中重新与引用链上任何一个方法建立关联(例如把自己this关键字赋值给某个类变量或者对象的成员变量),那么第二次标记时,将会移出即将回收的集合。否则,这个对象就会被回收了

3.2.5 回收方法区

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

先来说说方法区内常量池之中主要存放的两大类常量:字面量和符号引用。字面量比较接近Java语言层次的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“abc”常量就会被系统“请”出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:

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

3.3 垃圾收集算法

3.3.1 标记—清除算法(Mark-Sweep)

标记-清除算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象
深入理解Java虚拟机(第二版) 第三章:垃圾收集器与内存分配策略
该算法有两个不足:

  • 一是效率,标记和清除两个阶段效率都不高
  • 二是空间问题,标记清除后会产生大量不连续的内存碎片,这样以后在为大对象分配空间时,由于找不到足够的连续内存会提前出发下一次GC。

3.3.2 标记—整理算法(Mark—Compact)

标记-整理算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。
和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记。但之后,它并不简单的清理未标记的对象,而是将所有的存活对象移动到内存的一端。之后,清理边界外所有的空间

3.3.3 复制算法(Copying)

与标记-清除算法相比,复制算法是一种相对高效的回收方法。
不适用于存活对象较多的场合,如老年代。
将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收
深入理解Java虚拟机(第二版) 第三章:垃圾收集器与内存分配策略

3.3.4 分代收集算法

在新生代中,每次都只有少量的对象存活,那就选用复制算法。而老年代存活率高,没有额外的空间对其做担保,使用“标记-整理”或者“标记-清除”算法进行回收。

3.4 HotSpot的算法实现

3.4.1 枚举根节点

可作为GCRoots的节点主要在全局性的引用(如常量和类静态属性)与执行上下文(例如栈帧中的本地变量表)中,如果应用的方法区就有几百兆,那么如果逐个检查里面的引用,那么必然会消耗很多时间。

枚举根节点必须能在一个能确保一致性的快照中进行,这里的一致性指的是整个分析期间整个执行系统看起来就像被冻结在某一个时间点上,不可以出现分析过程中对象引用关系还在发生不断变化的情况,这一点不满足的话分析结果准确性就无法得到保证,这就是gc时必须停顿所有java执行线程的原因。

停顿下来以后,虚拟机并不需要一个不漏的检查所有执行上下文和全局变量引用,虚拟机应当有办法得知哪些地方存在着对象引用,在HotSpot实现中有一个OopMap的数据结构,在类加载完成的时候,虚拟机就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会特定的记录下栈和寄存器中哪些位置是引用,这样gc在扫描时就可以直接得知这些信息

3.4.2 安全点

JVM为了解决这一个问题,选取一个特定位置作为“安全点(SafePoint)”。

“安全点”简单点说就是程序执行停顿下的这一个点。程序在执行时,并非在所有的地方都能停下来开始GC,只有到达这个“安全点“时才能停顿下来。安全点的选区既不能太少以至于让GC等待时间过长,也不能过于频繁以至于过分增大运行时的负荷。所以,”安全点“的选择基本上是以程序”是否具有让程序长时间执行的特征“为标准来选定的。因为每条执行指令执行的时间都非常地短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,而程序”长时间的运行“实际上就是指令序列的一个复用。例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生“安全点”。

“安全点” 的两种中断方式(抢先式中断和主动式中断)

“抢先式中断”不需要线程的执行代码去主动配合,在GC发生时,首次会把所有的线程全部中断,如果发现有些线程中断点不是安全点,就恢复该线程直到安全点上停止。

“主动式中断”实际上就是线程主动轮询的一个过程,当GC需要中断线程时,不直接对线程进行操作,仅仅简单的设置一个标志,这个轮询标志当然要与安全点相重合。各个线程在执行的时候都会主动去询问这个轮询标志:“我是否到了该中断的点了?”。现在大部分虚拟机都采用是”主动式中断”方式,因为它相对“抢先式中断”方式避免了一个中断——>启动——>又中断的一个过程。

3.4.3 安全区域

设置“安全点”虽然保证了大部分线程停顿,但总有一些线程比较“懒”。例如,当GC请求中断时,该线程正在偷懒sleep(),JVM不可能等该线程睡醒之后到达安全点之后才能进行可达性分析过程,而此时如果该线程睡醒了恰巧GC又在进行可达性分析或者是回收,那该线程又该何去何从呢?所以JVM又在安全点的基础上加了一个双重保险——安全区域。

安全区域是指在一段代码片中,引用关系不会发生改变,实际上就是一个安全点的拓展。当线程执行到安全区域时,首先标识自己已进入安全区域,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为“安全区域”状态的线程了,该线程只能乖乖的等待根节点枚举或者整个GC过程完成之后才能继续执行。

3.5 垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
深入理解Java虚拟机(第二版) 第三章:垃圾收集器与内存分配策略
存在连线的手机器之间可以搭配使用。

3.5.1 Serial收集器

采用复制算法的新生代收集器,它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”)。在新生代垃圾回收时采用复制算法
深入理解Java虚拟机(第二版) 第三章:垃圾收集器与内存分配策略
HotSpot虚拟机运行在Client模式下的默认的新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。

在用户的桌面应用场景中,分配给虚拟机管理的内存一般不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本不会再大了),停顿时间完全可以控制在几十毫秒最多一百毫秒以内,只要不频繁发生,这点停顿时间可以接收。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

3.5.2 ParNew 收集器

ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,两者共用了相当多的代码。
深入理解Java虚拟机(第二版) 第三章:垃圾收集器与内存分配策略
ParNew收集器除了使用多线程收集外,其他与Serial收集器相比并无太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作,CMS收集器是JDK 1.5推出的一个具有划时代意义的收集器,具体内容将在稍后进行介绍。

ParNew 收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。在多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用-XX:ParallerGCThreads参数设置。

3.5.3 Parallel Scavenge 收集器

Parallel Scavenge收集器也是一个并行的多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器除了会显而易见地提供可以精确控制吞吐量的参数,还提供了一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为GC自适应的调节策略(GC Ergonomics)。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

另外值得注意的一点是,Parallel Scavenge收集器无法与CMS收集器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代选择Parallel Scavenge收集器,老年代只有Serial Old收集器能与之配合使用。

3.5.4 Serial Old收集器

Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”(Mark-Compact)算法。

此收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途:

  • 在JDK1.5 以及之前版本(Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。
  • 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用

它的工作流程与Serial收集器相同,这里再次给出Serial/Serial Old配合使用的工作流程图:
深入理解Java虚拟机(第二版) 第三章:垃圾收集器与内存分配策略

3.5.5 Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程“标记-整理”算法。前面已经提到过,这个收集器是在JDK 1.6中才开始提供的,在此之前,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old以外别无选择,所以在Parallel Old诞生以后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作流程与Parallel Scavenge相同,这里给出Parallel Scavenge/Parallel Old收集器配合使用的流程图:
深入理解Java虚拟机(第二版) 第三章:垃圾收集器与内存分配策略

3.5.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。

CMS收集器工作的整个流程分为以下4个步骤:

  • 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  • 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。
  • 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
  • 并发清除(CMS concurrent sweep)

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间:
深入理解Java虚拟机(第二版) 第三章:垃圾收集器与内存分配策略
优点
CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。

缺点

  • 对CPU资源非常敏感 其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。
  • 无法处理浮动垃圾 (Floating Garbage) 可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
  • 标记-清除算法导致的空间碎片 CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象而提前执行一次Full GC。

3.5.7 G1收集器

G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:

  • 并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
  • 空间整合 G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

横跨整个堆内存

在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合

建立可预测的时间模型

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

避免全堆扫描——Remembered Set

G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。

为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking) 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
  • 并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
  • 筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
    通过下图可以比较清楚地看到G1收集器的运作步骤中并发和需要停顿的阶段(Safepoint处):
    深入理解Java虚拟机(第二版) 第三章:垃圾收集器与内存分配策略

3.5.8 总结

深入理解Java虚拟机(第二版) 第三章:垃圾收集器与内存分配策略

3.6 内存分配及回收策略

对象优先在Eden(伊甸园)分配:大多数情况下对象在新生代Eden中分配,当Eden区中没有足够的空间时,虚拟机将发起一次Minor GC。

大对象直接进入老年代:所谓大对象就是指那些需要大量连续内存空间的Java对象,最典型的大对象就是那些很长的字符串以及数组。

长期存活的进入老年代:虚拟机给每个对象都定义了一个年龄计数器。对象在Eden出生,经过一次Minor GC还存活。对象的年龄就加一,当它的年龄增加到一定程度时(默认是15岁),就会被晋升到老年代。

新生代GC(Minor GC):指发生在新生代的GC动作,因为大多数Java对象都具有朝生夕灭的特点,所以Minor GC发生较为频繁,回收速度页比较快。

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Full GC,至少会伴随一次MinorGC。Full GC的速度比Minor GC慢10倍以上。

空间分配担保

先解释Young GC

当对象生成在EDEN区失败时,出发一次Young GC,先扫描EDEN区中的存活对象,进入Survivor 0区,Survivor 0放不下的进入OLD区,再扫描Survivor 1区,若存活次数超过阀值则进入OLD区,其它进入Survivor 0区,然后Survivor 0和Survivor 1交换一次。

那么当发生Young GC时,JVM会首先检查老年代最大的可用连续空间是否大于新生代所有对象的总和,如果大于,那么这次Young GC是安全的,如果不大于的话,JVM就需要判断HandlePromotionFailure是否允许空间分配担保。

允许分配担保

JVM继续检查老年代最大的可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则正常进行一次Young GC,尽管有风险(因为判断的是平均大小,有可能这次的晋升对象比平均值大很多);

如果小于,或者HandlePromotionFailure设置不允许空间分配担保,这时要进行一次Full GC。

新生代采用的是复制收集算法,Survivor 0和Survivor 1始终只是用其中一块内存区,当出现Young GC后大部分对象仍然存活的话,就需要老年代进行分配担保,把Survivor区无法容纳的对象直接晋升到老年代。

那么这种空间分配担保的前提是老年代还有容纳的空间,一共有多少对象会活下来,在实际完成内存回收之前是无法明确知道的,所以只好取之前每次回收晋升到老年代对象容量的平均值大小作为经验值,与老年代的剩余空间比较,决定是否进行Full GC来让老年代腾出更多空间。

3.7 JavaGC、新生代、老年代

3.7.1 堆内存

Java中的堆是JVM所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
在Java中,堆被划分成两个不同的区域:新生代(Young)、老年代(Old)。新生代(Young)又被划分为
三个区域:Eden、From Survivor、To Survivor。
这样划分的目的是为了使JVM能够更好的管理堆内存中的对象,包括内存的分配以及回收。

堆的内存模型大致为:
深入理解Java虚拟机(第二版) 第三章:垃圾收集器与内存分配策略
从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
(本人使用的是 JDK1.6,以下涉及的 JVM 默认值均以该版本为准。)

默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

3.7.2 GC堆

Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )

Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法

新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。当一个对象被判定为 “死亡” 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。

当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代

Full GC Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC;。Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。

3.8 内存溢出和内存泄漏

内存溢出out of memory(memory overflow),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory。

泄漏,什么是泄漏?我举个简单的例子,不知道是不是这个意思,就比如说有人跟你关系不错,找你借了点钱,但是后来他搬家了,新地址你不知道,你想找他要钱回来,但是就是找不到他在什么地方。专业点的话就是说你向系统申请到了你想要的内存空间,但是使用完了之后却不归还,结果你申请到的内存空间你自己也访问不到(也许你把地址搞丢了),系统也无法分配该空间给其他的程序。这就是一次泄漏.
内存泄露是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成的内存空间的浪费称为内存泄露。内存泄露的堆积会造成内存溢出。
Java内存泄露根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景

造成的原因有:1、静态集合类引起内存泄漏:2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。3、监听器。4、各种连接。5、内部类和外部模块的引用。