面试JVM(二)垃圾回收

6 篇文章 0 订阅

1回收什么?

程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行(对象)。

垃圾收集(Garbage Collection ,GC)

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

判断一个对象是否可回收

1. 引用计数算法

给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。

两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。

正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

2. 可达性分析算法(跟搜索算法)    

所有当前正在运行的Java线程中活跃的栈帧里指向堆中对象的引用

通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。

https://blog.csdn.net/leishenop/article/details/53728605gcroot

GC Roots 就是一组必须活跃的引用

 所有当前正在运行的Java线程中活跃的栈帧里指向堆中对象的引用,换句话说,就是当前正在执行的所有方法中的对象的引用

Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:

  • 虚拟机栈中引用的对象
  • 本地方法栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中的常量引用的对象

3. 引用类型

无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否可被回收都与引用有关。

Java 具有四种强度不同的引用类型。

(一)强引用

被强引用关联的对象不会被垃圾收集器回收。使用 new 一个新对象的方式来创建强引用。

Object obj = new Object();

(二)软引用

被软引用关联的对象,只有在内存不够(内存溢出)的情况下才会被回收。使用 SoftReference 类来创建软引用。

Object obj = new Object();SoftReference<Object> sf = new SoftReference<Object>(obj);obj = null; // 使对象只被软引用关联

(三)弱引用

被弱引用关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集发生之前。

使用 WeakReference 类来实现弱引用。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

WeakHashMap 的 Entry 继承自 WeakReference,主要用来实现缓存。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>

Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。ConcurrentCache 采取的是分代缓存,经常使用的对象放入 eden 中,而不常用的对象放入 longterm。eden 使用 ConcurrentHashMap 实现,longterm 使用 WeakHashMap,保证了不常使用的对象容易被回收。

public final class ConcurrentCache<K, V> {

    private final int size;

    private final Map<K, V> eden;

    private final Map<K, V> longterm;

    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        this.longterm = new WeakHashMap<>(size);
    }

    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            v = this.longterm.get(k);
            if (v != null)
                this.eden.put(k, v);
        }
        return v;
    }

    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            this.longterm.putAll(this.eden);
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}

(四)虚引用

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。

为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

4. 方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代差很多,因此在方法区上进行回收性价比不高。

主要是对常量池的回收和对类的卸载。

类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:

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

可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。

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

5. finalize()

finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。

当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。

 

垃圾收集算法

https://blog.csdn.net/wen7280/article/details/54428387点击打开链接

 

1. 标记 - 清除

 

将需要存活的对象进行标记,然后清理掉未被标记的对象。

 

不足:

  • 标记和清除过程效率都不高;
  • 会产生大量不连续的内存碎片,导致无法给大对象分配内存。

2. 标记 - 整理

 

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

 

3. 复制

 

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。

 

主要不足是只使用了内存的一半。

现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。

4. 分代收集

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

一般将 Java 堆分为新生代和老年代。

  • 新生代使用:复制算法
  • 老年代使用:标记 - 清理 或者 标记 - 整理 算法

 

   年代划分

l  刚才描述了许多GC算法,有这么多GC算法,使用哪种GC是根据分代情况来决定的

l  根据对象的生命周期长短,我们把对象划分为:新生代老年代,永久带。由于永久带在1.8中就没了,因此我们就先不说了。顾名思义,生命周期短的对象就是新生代,生命周期长的对象叫做老年代。

l  什么是新生代?什么是老年代?堆中是如何划分新生代和老年代的?

对象存在于堆中,刚new出来的对象大多属于新生代,在一次GC发生之后(新生代发生的GC称为Minor GC,这个等下再讲),如果此对象还存活并且达到了老年代的年龄,这个对象就可以移步到老年代中。

 

 

Eden:s1:s2 = 8:1:1

新生代:老年代 = 1:2

l  新生代对象如何转化成老年代对象?

      大对象直接进入老年代(多大的对象算大对象?)

    新生代发生Minor GC之后仍然存活的对象

少量对象存活,使用压缩算法,大量对象存活,使用标记清除和标记压缩算法。

新生代采用压缩复制算法,效率高,老年代使用标记清除标记压缩算法,效率低

垃圾收集器

 

以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

 

串行收集器(Serial)

这是最古老的一种垃圾收集器,比较稳定。但是存在Stop-The-World,会造成应用的停顿,而且还是单线程进行垃圾回收,速度比较慢

l  Serial Old

对比串行收集器,这个是收集老年代对象的,使用标记-压缩算法进行回收。

 

并行收集器1(ParNew)

从名字就可以看出来,New代表了此收集器处理的是新生代对象的垃圾收集。

ParNew收集器在新生代采用并行老年代采用串行,也就是第一种。

l  Parallel Old

Parallel Scavenge的老年代版本,一般配合ParallelScavenge使用。

l  并行收集器2(Parallel Scavenge)

这个也是个并行收集器,其实和ParNew差不多。新生代采用复制压缩算法,老年代采用标记-压缩算法。

Parallel有两种不同的配置,Parallel在老年爱也可以并行收集

说道ParNew和Parallel Scavenge的区别,Parallel更加关注吞吐量。怎么说这个吞吐量呢?

吞吐量=代码执行时间/代码执行时间+GC垃圾收集时间

由此可以看出,GC时间越短,吞吐量越大。但是GC所要执行完成的任务量肯定是一致的,如果缩短了每一次GC所执行的时间,那么GC执行的次数肯定就会变多

 

l  CMS收集器(Concurrent Mark Sweep)

1.      与前边的几个并行收集器不同,CMS收集器是并发收集器。并行收集器指的是在进行GC的时候多线程的进行垃圾回收,而并发收集器指的是GC垃圾回收与应用线程(创建对象线程)同时的执行。

2.      CMS收集器采用标记-清除的方法,来处理老年代区间的对象。由于CMS里GC和应用线程是同时进行的(新生代对象仍然是Stop-The-World,因为新生代的垃圾回收时间短),因此CMS的标记过程十分复杂:

         1.初始标记:从根对象(GCRoots)开始标记,可触及的对象会被标记。会造成用户线程短暂停顿,不过速度非常快。

         2.并发标记:和用户线程一起标记

         3.重新标记:独占的

         4.清除:和用户线程一起

 

从上面我们可以看到,CMS虽说叫并发收集器,但是并发的部分也就只有并发标记和并发清理两部分而已。但是初始标记和重新标记的执行速度是很快的,会大量消耗时间的是并发标记的部分。把消耗大量时间的操作用并发来解决,节省了很多的时间。

下面我们说一下CMS收集器存在的几个问题:

1.吞吐量与性能:在应用程序运行时,需要分出50%的CPU去执行GC,因此系统的性能与吞吐量会下降。

2.清理不彻底:如果在并发清理的时候,应用程序还在运行,可能会有新的对象需要被回收,那么在这一次GC中,这个对象就不能被回收到了。

3.产生内存碎片:由于CMS是采用标记-清除的方式来处理垃圾的,所以不可避免的会产生内存碎片,导致内存利用率低。你可能会问,为啥不采用标记-压缩呢?因为在清理对象的同时应用程序还在创建对象,我们无法通过压缩的方式操作内存。

l  G1垃圾收集器

G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。

Java 堆被分为新生代、老年代和永久代,其它收集器进行收集的范围都是整个新生代或者老生代,而 G1 可以直接对新生代和永久代一起回收。

G1 把新生代和老年代划分成多个大小相等的独立区域(Region),新生代和永久代不再物理隔离。

 

 

 

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

  • 初始标记
  • 并发标记
  • 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
  • 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。

具备如下特点:

  • 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
  • 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。

1.      什么是G1垃圾收集器,和CMS有什么区别?

1.       OopMap和RememberSet和safepoint

2.      OopMap是存储GCRoot的

 

比较

收集器串行/并行/并发新生代/老年代收集算法目标适用场景
Serial串行新生代复制响应速度优先单 CPU 环境下的 Client 模式
Serial Old串行老年代标记-整理响应速度优先单 CPU 环境下的 Client 模式、CMS 的后备预案
ParNew串行 + 并行新生代复制算法响应速度优先多 CPU 环境时在 Server 模式下与 CMS 配合
Parallel Scavenge串行 + 并行新生代复制算法吞吐量优先在后台运算而不需要太多交互的任务
Parallel Old串行 + 并行老年代标记-整理吞吐量优先在后台运算而不需要太多交互的任务
CMS并行 + 并发老年代标记-清除响应速度优先集中在互联网站或 B/S 系统服务端上的 Java 应用
G1并行 + 并发新生代 + 老年代标记-整理 + 复制算法响应速度优先面向服务端应用,将来替换 CMS

 

 

 

 

内存分配与回收策略

对象的内存分配,也就是在堆上分配。主要分配在新生代的 Eden 区上,少数情况下也可能直接分配在老年代中。

1. Minor GC 和 Full GC

  • Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
  • Full GC:发生在老年代上,老年代对象和新生代的相反,其存活时间长,因此 Full GC 很少执行,而且执行速度会比 Minor GC 慢很多。

2. 内存分配策略

(一)对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。

(二)大对象直接进入老年代

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

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。

(三)长期存活的对象进入老年代

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。

-XX:MaxTenuringThreshold 用来定义年龄的阈值。

(四)动态对象年龄判定

虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

(五)空间分配担保

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

3. Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

(一)调用 System.gc()

此方法的调用是建议虚拟机进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存。可通过 -XX:DisableExplicitGC 来禁止 RMI 调用 System.gc()。

(二)老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组。

(三)空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。

(四)JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError,为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

(五)Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

 

 

 

 

 

      垃圾回收的起点

 

栈是真正进行程序执行地方,所以要获取哪些对象正在被使用,则需要从Java栈开始。同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。

同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。这样,以栈或寄存器中的引用为起点,我们可以找到堆中的对象,又从这些对象找到对堆中其他对象的引用,这种引用逐步扩展,最终以null引用或者基本类型结束,这样就形成了一颗以Java栈中引用所对应的对象为根节点的一颗对象树,如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收。而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。

因此,垃圾回收的起点是一些根对象(java栈中的引用的对象,静态变量, 寄存器...)。而最简单的Java栈就是Java程序执行的main函数。这种回收方式,也是上面提到的“标记-清除”的回收方式。

 

      伴随GC所产生的问题

 

l  如何处理内存碎片

当程序执行了一段时间之后,经过了许多次的对象的创建和对象的收集。如果不进行内存碎片的整理,许多非常小的内存碎片就不能组合起来存放大一点的对象,会造成内存浪费

l  GC的过程中整个应用程序会停止运行,如何让对象的创建和对象的回收合理进行?

在一条线程中,垃圾收集不断地收集对象,程序运行的线程是不断生成对象从而消耗内存的,这个是很矛盾的。在你收集垃圾的时候,不断有新对象创建,那我怎么判断哪个对象是垃圾?我怎么从根节点遍历出对象的可达性?

因此,在GC过程中,整个应用停止运行了。

那如果我的堆非常大有很多很多对象,那GC进行的时候岂不是会浪费很多时间吗?

由此,并发收集就应运而生了。

当然,这个并发收集说的是垃圾回收变为多线程,而不是边收集垃圾边产生对象。

 

 

GC 优化配置

 

 

配置描述
-Xms初始化堆内存大小
-Xmx堆内存最大值
-Xmn新生代大小
-XX:PermSize初始化永久代大小
-XX:MaxPermSize永久代最大容量

GC 类型设置

配置描述
-XX:+UseSerialGC串行垃圾回收器
-XX:+UseParallelGC并行垃圾回收器
-XX:+UseConcMarkSweepGC并发标记扫描垃圾回收器
-XX:ParallelCMSThreads=并发标记扫描垃圾回收器 = 为使用的线程数量
-XX:+UseG1GCG1 垃圾回收器

 

 

 

 

 

 

 

GC Root

从名字就可以知道,GCRoot就是判断对象可达性的根节点,告诉了我们垃圾回收从哪个对象开始。只有知道了GC Root是哪个,我们才能利用后续的GC算法,根据GC Root判断对象是否可达,然后做对象的回收工作。GC Root(根节点,根对象)GC中相当重要的一点,那到底什么是GC Root?

l  所有当前正在运行的Java线程中活跃的栈帧里指向堆中对象的引用(当前正在执行的所有方法中的对象的引用)

l  静态变量修饰的对象的引用

l  运行时常量池的引用类型常量(String)

l  当前被加载的Java类

这里需要注意一点,上述提到的全部都是活跃的引用,不是对象本身。

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值