1.GC 垃圾回收

GC就是垃圾收集的意思(Gabage Collection)
在开发中会创建很多对象,这些对象一股脑的都扔进了堆里,如果这些对象只增加不减少,那么堆空间很快就会被耗尽。所以我们需要把一些没用的对象清理掉。
这个时候JVM就提供了垃圾回收机制

垃圾回收,就是要把那些不再使用的对象找出来然后清理掉,释放其占用的内存空间

2.判断是否为垃圾对象的两种方式

  • 引用计数法
  • 可达性分析法

2.1 引用计数法

它的做法是给对象添加一个引用计数器,每当有一个地方引用该对象,这个计数器就加1。当引用失效时,计数器就减1。如果计数器为0了,说明该对象不再被引用,成为死亡对象
不过这种算法有一个致命缺点,就是无法处理对象相互引用的情况
假如有A、B两个对象,它们互相引用,那么对象中的引用计数器会始终大于0。

2.2 可达性分析法

可达性分析法就是目前的主流算法,也是java正在使用的算法。

它的做法是,通过一系列被称为“GC Roots”的对象作为起点,从这些起点开始往下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象没有和任何引用链相连,即称为该对象不可达(图论的说法),认为该对象死亡。

GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等

  • 哪些对象可以做为GC Roots?
    有四类对象可作为可达性分析的GC Roots
    1. 栈(栈帧中的本地变量表)中引用的对象
    2. 方法区中类静态属性引用的对象
    3. 方法区中常量引用的对象
    4. 本地方法栈中JNI引用的对象

GC Roots是所有Java线程中处于活跃状态的栈帧,静态引用等指向GC堆里的对象的引用。换句话说,就是当前所有正在被调用的方法的引用类型的参数/局部变量/临时值。

3.对象引用分类

对象是否死亡,关键就在于引用。在java中,引用其实有四种:强引用、软引用、弱引用、虚引用。

  • 强引用
    强引用就是我们日常开发中最常见的引用,例如
    String str = new String(“hello”);
    只要强引用还在,对象就不会被回收。
  • 软引用
    软引用需要专门声明,例如
    SoftReference str = new SoftReference(“hello”);
    被软引用关联的对象在内存不足时会被回收。
    这个特性特别适合用来做缓存。
  • 弱引用
    弱引用也需要专门声明,例如
    WeakReference str = new WeakReference(“hello”);
    被弱引用关联的对象每次GC时都会被回收。
    弱引用最常见的用途是实现可自动清理的集合或者队列。
  • 虚引用
    虚引用是最弱的引用,需要用PhantomReference来声明,例如
    PhantomReference phantom = new PhantomReference<>(new String(“hello”), new ReferenceQueue<>());
    它完全不会影响对象的生存时间,唯一的作用是在对象被回收时发一个系统通知。

4.垃圾回收算法分类

我们需要了解的垃圾回收算法有以下几种:

  • 标记-清除算法
  • 复制算法
  • 标记-整理算法
  • 分代回收算法

4.1 标记-清除算法


标记-清除算是最基本的回收算法了。它的思想就是先标记,再清除。标记过程如2.4节所述,有两次标记。

它的主要缺点有两个:

  • 效率不高
  • 会产生大量内存碎片
    内存碎片是指内存的空间比较零碎,缺少大段的连续空间。这样假如突然来了一个大对象,会找不到足够大的连续空间来存放,于是不得不再触发一次gc。

4.2 复制算法

复制算法的思想是,把内存分成两块,假设分成A、B两个区域吧。
每次对象过来之后,都放到A区域里,当A区域满了之后,把存活的对象复制到B区域,然后清空A区域。
接下来的对象就全部放到B区域,等B区域满了,就把存活对象复制到A区域,然后清空B区域。
就这样来回倒腾,完成垃圾回收。
优点是不会有空间碎片,缺点是每次只用得到一半内存

缺点是在对象存活率较高的场景下(比如老年代那样的环境),需要复制的东西太多,效率会下降。

4.3 标记-整理算法

标记-整理算法中的“标记”阶段和“标记-清理”中的标记一样。不同的是,死亡对象并不会直接清理,而是把他们在内存中都移动到一起,然后一起清理。

4.4 分代回收算法

分代收集算法其实没什么新东西,只是把对象按存活率分块,然后选用合适的收集算法。
java中使用的就是分代收集算法
存活率低的对象放在一起,称为年轻代,使用复制算法来收集。
存活率高的对象放在一起,称为老年代,使用标记-清除或者标记-整理算法

5.内存分配策略

5.1 年轻代的策略

在年轻代分为三个区域,Eden区、Survivor1区、Survivor2区。有时候Survivor1区、Survivor2区又叫from区和to区。
对象优先分配到Eden区。Eden区要满的时候,会有一次复制回收,把存活的对象放到Survivor1区。
等Eden区再次要满的时候,又会有一次复制回收,把Eden区和Survivor1区的存活对象放到Survivor2区。
然后如此循环。

5.2 大对象的策略

虚拟机提供了一个-XX:PretenureSizeThreshold参数,大于这个参数的对象会直接进入老年代,防止年轻代发生大量内存复制。

5.3 晋升策略

年轻代的对象没熬过一次Minor GC,年龄就加一岁。默认15岁时,就会进入老年代。

不过这个条件并非绝对,如果Survivor中相同年龄的对象总和大于Survivor空间的一半,那么年龄大于等于该年龄的对象可以直接晋升到老年代

5.4 空间分配担保

年轻代在Minor GC后会有对象进入老年代,在极端情况下,年轻代所有对象都存活并进入老年代
所以在MinorGC之前,虚拟机会检查老年代的连续内存空间是否大于年轻代所有对象总和
如果空间不够,那么这次MinorGC是有风险的
如果允许冒险,Minor GC会直接执行,如果失败,会再发起一次full GC
如果不允许冒险,则先执行一次full GC,再进行Minor GC

相关面试题

  1. GC 是什么? 为什么要有 GC?
    GC就是垃圾回收,释放掉没用的对象占用的空间,保证内存空间不被迅速耗尽。
  2. 简单说一下java的垃圾回收机制。
    java采用分代回收,分为年轻代、老年代、永久代。年轻代又分为E区、S1区、S2区。
    到jdk1.8,永久代被元空间取代了。
    年轻代都使用复制算法,老年代的收集算法看具体用什么收集器。默认是PS收集器,采用标记-整理算法。
  3. JVM的常见垃圾回收算法有哪些?
    复制、标记清除、标记整理、分代回收
  4. 为什么要使用分代回收机制?
    因为没有一种算法能适用所有场合。在对象存活率低的场景下,复制算法最合适。
    对象存活率高时,标记清除或者标记整理算法最合适。
    所以才需要分代来处理。
  5. 如何判断一个对象是否存活?
    现在主流使用的都是可达性分析法。从GC Roots对象计算引用链,能链上的就是存活的。
  6. 如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?
    不会。对象回收需要一个过程,这个过程中对象还能复活。而且垃圾回收具有不确定性,指不定什么时候开始回收