前置

JVM[0]-运行时数据区

意义

堆(包括 Java 堆和方法区)是垃圾回收的主要对象

Stop-the-World

Stop-the-world 意味着 JVM 由于要执行 GC 而停止了应用程序的执行,并且这种情形会在任何一种 GC 算法中发生。当 Stop-the-world 发生时,除了 GC 所需的线程以外,所有线程都处于等待状态直到 GC 任务完成。事实上,GC 优化很多时候就是指减少 Stop-the-world 发生的时间,从而使系统具有 高吞吐 、低停顿 的特点。


确定一个对象是否可以被回收

引用计数算法

引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。
在这种方法中,堆中的每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个引用变量,该对象实例的引用计数设置为 1。当任何其它变量被赋值为这个对象的引用时,对象实例的引用计数加 1(a = b,则 b 引用的对象实例的计数器加 1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数减 1。特别地,当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器均减 1。任何引用计数为 0 的对象实例可以被当作垃圾收集。

但是,这种方法无法回收对象实例相互引用的情况,因为他们的引用技术器无法被减为 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ReferenceCountingGC {

public Object instance = null;

public static void testGC(){

ReferenceCountingGC objA = new ReferenceCountingGC ();
ReferenceCountingGC objB = new ReferenceCountingGC ();

// 对象之间相互循环引用,对象objA和objB之间的引用计数永远不可能为 0
objB.instance = objA;
objA.instance = objB;

objA = null;
objB = null;

System.gc();
}
}

可达性分析算法

可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收。

可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,通过一系列的名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。在 Java 中,可作为 GC Root 的对象包括以下几种:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 Native 方法引用的对象

垃圾收集算法

标记清除算法

标记-清除算法分为标记和清除两个阶段。该算法首先从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象并进行回收

可达性分析算法.jpg可达性分析算法.jpg

标记-清除算法的主要不足有两个:

  • 效率问题:标记和清除两个过程的效率都不高;
  • 空间问题:标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
**这种算法适用于对象存活率低的场景,比如新生代。**这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
现在商用的虚拟机都采用这种算法来回收新生代。

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

标记整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费 50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都 100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
标记整理算法的标记过程类似标记清除算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程,该垃圾回收算法适用于对象存活率高的场景(老年代)

标记整理算法与标记清除算法最显著的区别是:标记清除算法不进行对象的移动,并且仅对不存活的对象进行处理;而标记整理算法会将所有的存活对象移动到一端,并对不存活对象进行处理,因此其不会产生内存碎片。

分代收集算法

分代收集算法是基于这样一个事实: 不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。
当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。

新生代(Young Generation)

新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。新生代内存按照 8:1:1 的比例分为一个 eden 区和两个 survivor(survivor0,survivor1)区,大部分对象在 Eden 区中生成。在进行垃圾回收时,先将 eden 区存活对象复制到 survivor0 区,然后清空 eden 区,当这个 survivor0 区也满了时,则将 eden 区和 survivor0 区存活对象复制到 survivor1 区,然后清空 eden 和这个 survivor0 区,此时 survivor0 区是空的,然后交换 survivor0 区和 survivor1 区的角色(即下次垃圾回收时会扫描 Eden 区和 survivor1 区),即保持 survivor0 区为空,如此往复。特别地,当 survivor1 区也不足以存放 eden 区和 survivor0 区的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了,就会触发一次 FullGC,也就是新生代、老年代都进行回收。

注意,新生代发生的 GC 也叫做 MinorGC,MinorGC 发生频率比较高,不一定等 Eden 区满了才触发。

老年代(Old Generation)

老年代存放的都是一些生命周期较长的对象,就像上面所叙述的那样,在新生代中经历了 N 次垃圾回收后仍然存活的对象就会被放到老年代中。此外,老年代的内存也比新生代大很多(大概比例是 2:1),当老年代满时会触发 Major GC(Full GC),老年代对象存活时间比较长,因此 FullGC 发生的频率比较低。
导致 Full GC 的原因包括:老年代被写满、永久代(Perm)被写满和 System.gc()被显式调用等。

永久代(Permanent Generation)

永久代主要用于存放静态文件,如 Java 类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class,例如使用反射、动态代理、CGLib 等 bytecode 框架时,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。

在 jdk1.8 中 永久代被 Metaspace 取代。元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。


垃圾收集器

如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

垃圾收集器.jpg垃圾收集器.jpg

新生代收集器

  • Serial 收集器(复制算法): 新生代单线程收集器,在进行垃圾回收的时候仅使用单条线程并且在回收的过程中会挂起所有的用户线程(Stop The World)。Serial 收集器是 JVM client 模式下默认的新生代收集器;

  • ParNew 收集器 (复制算法):新生代收并行集器,实际上是 Serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现;

  • Parallel Scavenge 收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC 线程时间),高吞吐量可以高效率的利用 CPU 时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;

老年代收集器

  • Serial Old 收集器 (标记-整理算法): 老年代单线程收集器,采用单线程和标记-整理算法来实现垃圾回收。在回收垃圾的时候同样会挂起所有用户线程,造成应用的停顿。一般来说,老年代的容量都比新生代要大,所以当发生老年代的垃圾回收时,STW 经历的时间会比新生代所用的时间长得多。该收集器是 JVM client 模式下默认的老年代收集器。

  • Parallel Old 收集器 (标记-整理算法):老年代并行收集器,吞吐量优先,Parallel Scavenge 收集器的老年代版本;

  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法):老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短 GC 回收停顿时间。MS 收集器以获取最短回收停顿时间为目标,采用多线程并发以及标记-清除算法来实现垃圾回收。CMS 只在初始化标记和重新标记阶段需要挂起用户线程,造成一定的应用停顿(STW),而其他阶段收集线程都可以与用户线程并发交替进行,不必挂起用户线程,所以并不会造成应用的停顿。CMS 收集器可以最大程度地减少因垃圾回收而造成应用停顿的时间。
    show-cms-collector.pngshow-cms-collector.png
    CMS 垃圾收集分为以下几个阶段:

    1. 初始化标记 (inital mark)
      这个阶段仅仅是标记了 GC Roots 能够直接关联到的对象,速度很快,所以基本上感受不到 STW 带来的停顿。
    2. 并发标记 (concurrent mark)
      并发标记阶段完成的任务是从第一阶段收集到的对象引用开始,遍历所有其他的对象引用,并标记所有需要回收的对象。这个阶段,收集线程与用户线程并发交替执行,不必挂起用户线程,所以并不会造成应用停顿。
    3. 并发预清除 (concurrent-pre-clean)
      并发预清除阶段是为了下一个阶段做准备,为的是尽量减少应用停顿的时间。
    4. 重新标记 (remark)
      这个阶段将会修正并发标记期间因为用户程序继续运作而导致标记产生变动的那部分对象的标记记录(有可能对象重新被引用或者新对象可以被回收)。这个阶段的停顿时间比初始标记阶段要长一些,但是远比并发标记的时间短。
    5. 并发清除 (concurrent sweep)
      这个阶段将真正执行垃圾回收,将那些不被使用的对象内存回收掉。
    6. 并发重置 (concurrent reset)
      收集器做一些收尾的工作,以便下一次 GC 周期能有一个干净的状态。
  • G1(Garbage First)收集器 (标记-整理算法):Java 堆并行收集器,G1 收集器是 JDK1.7 提供的一个新收集器,G1 收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1 收集器不同于之前的收集器的一个重要特点是:G1 回收的范围是整个 Java 堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。


内存分配与回收策略

  • 对象优先在 Eden 分配,当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 MinorGC。
  • 大对象直接进入老年代。
    所谓的大对象是指,需要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组。
  • 长期存活的对象将进入老年代。
    当对象在新生代中经历过一定次数(默认为 15)的 Minor GC 后,就会被晋升到老年代中。
  • 动态对象年龄判定。
    为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。

Java 中的内存泄露问题

虽然 Java 拥有垃圾回收机制,但同样会出现内存泄露问题,比如下面提到的几种情况:

  • 诸如 HashMap、Vector 等集合类的静态使用最容易出现内存泄露,因为这些静态变量的生命周期和应用程序一致,所有的对象 Object 也不能被释放,因为他们也将一直被 Vector 等应用着。
  • 各种资源连接包括数据库连接、网络连接、IO 连接等没有显式调用 close 关闭,不被 GC 回收导致内存泄露。
  • 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

引用的种类及其定义

  • 强引用(StrongReference)
    如果一个对象具有强引用,那么垃圾回收器绝对不会回收它,当内存不足时宁愿抛出 OOM 错误,使得程序异常停止。

  • 软引用(SoftReference)
    如果一个对象只具有软引用,那么垃圾回收器在内存充足的时候不会回收它,而在内存不足时会回收这些对象。软引用对象被回收后,Java 虚拟机会把这个软引用加入到与之关联的引用队列中。

  • 弱引用(WeakReference)
    如果一个对象只具有弱引用,那么垃圾回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存。与软引用相同,弱引用对象被回收后,Java 虚拟机会把这个弱引用加入到与之关联的引用队列中。

  • 虚引用(PhantomReference)
    虚引用并不决定对象生命周期,如果一个对象只具有虚引用,那么它和没有任何引用一样,任何时候都可能被回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。与软引用和弱引用不同的是,虚引用必须关联一个引用队列。

    当垃圾回收器准备回收一个对象之前,如果发现它还具有虚引用,就会在对象回收前把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否加入了虚引用,来了解被引用的对象是否将要被回收,那么就可以在其被回收之前采取必要的行动。

1
2
3
4
5
6
7
8
9
// 软引用
SoftReference<Person> person0 = new SoftReference<Person>(new Person("name"));
// 弱引用
WeakReference<Person> person1 = new WeakReference<Person>(new Person("name"));

// 需引用
Person person = new Person("nameTest", 5);
ReferenceQueue<Person> rfq = new ReferenceQueue<Person>();
PhantomReference<Person> pph = new PhantomReference<Person>(person, rfq);

方法区的回收

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

判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是“无用的类”:

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

虚拟机可以对满足上述 3 个条件的无用类进行回收(卸载),这里说的仅仅是“可以”,而不是和对象一样,不使用了就必然会回收。特别地,在大量使用反射、动态代理、CGLib 等 bytecode 框架的场景,以及动态生成 JSP 和 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。


引用:https://blog.csdn.net/justloveyou_