前言

笔者就Java的垃圾回收机制进行一番复习和探讨

一、题目

1.什么情况下会内存栈溢出

两种情况

  • 无限递归,会导致StackOverFlow
  • 不断创建线程,会导致OOM

2.new一个对象流程

先来看创建对象的详细过程

  • 当虚拟机遇到一条new指令时,会去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查代表的类是否已经被类加载器加载。如果没有被加载那么必须先执行这个类的加载。
  • 类加载检查通过后,虚拟机将为新对象分配内存,对象所需内存的大小在类加载后便可以确定。
  • 内存分配完成后,虚拟机需要将对象初始化为零值,保证对象的实例变量在代码中不赋初始值就能直接使用。类变量在类加载的准备阶段初始化为零值。
  • 对对象头进行必要信息的设置,比如如何找到类的元数据信息、对象的HashCode、GC分代年龄等。
  • 经过上述操作,一个新的对象已经产生,但是方法还没有执行,所有的字段都是零值。这时候需要执行方法(构造方法)把对象按照程序员的意愿进行初始化。类变量的初始化操作在类加载的初始化阶段方法完成

简略版的过程

  • 分配对象的内存空间
  • 初始化对象
  • 设置引用指向分配的内存地址

3.对象会不会被分配在栈中

符合逃逸分析的对象就会被分配在栈中

4.如何判断一个对象是否被回收,有哪些算法,实际虚拟机采用的是哪种算法

垃圾收集器会对Java堆里面的对象进行判断,判断里面的某一个对象是存活还是死亡

判断对象为死亡才会进行回收

引用计数算法(存在缺点)

方式描述
1.给 Java 对象添加一个引用计数器
2.每当有一个地方引用它时,计数器 +1;引用失效则 -1;

判断对象存活准则
当计数器不为 0 时,判断该对象存活;否则判断为死亡(计数器 = 0)。

优点

  • 实现简单
  • 判断高效

缺点

  • 无法解决 对象间相互循环引用 的问题
  • 即该算法存在判断逻辑的漏洞

作者:Carson带你学Android
链接:https://juejin.cn/post/6844903684036362254
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

可达性分析

很多主流商用语言(如Java、C#)都采用 引用链法 判断 Java对象是否存活。
含3个步骤:

  • 可达性分析
  • 第一次标记 & 筛选
  • 第二次标记 & 筛选

将一系列的 GC Roots 对象作为起点,从这些起点开始向下搜索。

  • 可作为 GC Root 的对象有:
    1.Java虚拟机栈(栈帧的本地变量表)中引用的对象
    2.本地方法栈 中 JNI引用对象
    3.方法区 中常量、类静态属性引用的对象
  • 向下搜索的路径 = 引用链

相关GR ROOTS对象具体介绍可看这篇博客
GC ROOTS
在这里插入图片描述

如图,当一个对象到 GC Roots 没有任何引用链相连时,则判断该对象不可达

没有任何引用链相连 = GC Root到对象不可达 = 对象不可用

注意:

  • 可达性分析 仅仅只是判断对象是否可达,但还不足以判断对象是否存活 / 死亡
  • 当在可达性分析 中判断不可达的对象,只是“被判刑” = 还没真正死亡

不可达对象会被放在”即将回收“的集合里。

要判断一个对象真正死亡,还需要经历两个阶段:

  • 第一次标记 & 筛选
    对象 在 可达性分析中 被判断为不可达后,会被第一次标记 & 准备被筛选

a. 不筛选:继续留在 ”即将回收“的集合里,等待回收;
b. 筛选:从 ”即将回收“的集合取出

筛选的标准:该对象是否有必要执行 finalize()方法

若有必要执行(人为设置),则筛选出来,进入下一阶段(第二次标记 & 筛选);
若没必要执行,判断该对象死亡,不筛选 并等待回收

当对象无 finalize()方法 或 finalize()已被虚拟机调用过,则视为“没必要执行”

  • 第二次标记 & 筛选
    当对象经过了第一次的标记 & 筛选,会被进行第二次标记 & 准备被进行 筛选

a. 方式描述
该对象会被放到一个 F-Queue 队列中,并由 虚拟机自动建立、优先级低的Finalizer 线程去执行 队列中该对象的finalize()

注:
1.finalize()只会被执行一次
2.但并不承诺等待finalize()运行结束。这是为了防止 finalize()执行缓慢 / 停止 使得 F-Queue队列其他对象永久等待。

b. 筛选标准
在执行finalize()过程中,若对象依然没与引用链上的GC Roots 直接关联 或 间接关联(即关联上与GC Roots 关联的对象),那么该对象将被判断死亡,不筛选(留在”即将回收“集合里)并等待回收

实际虚拟机更多使用可达性分析法

5.GC收集算法有哪些,他们的特点是什么

一般认为有四种

复制

介绍:
按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。

优点:
不容易产生内存碎片

缺点:
内存使用率不高,只有原来的一半。

标记清除

介绍:
分为俩阶段
标记阶段:标记出可以回收的对象。
清除阶段:回收被标记的对象所占用的空间。

优点:
实现简单,不需要对象进行移动。

缺点:
效率不高,无法清除垃圾碎片。容易产生内存碎片

标记整理算法

介绍:
标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。

优点:
解决了标记-清理算法存在的内存碎片问题。

缺点:
仍需要进行局部对象移动,一定程度上降低了效率。且会暂停用户线程,效率比较低

分代收集算法

当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思
义是根据对象的存活周期将内存划分为几块。
一般包括年轻代、老年代和永久代,如图所示:
在这里插入图片描述
新生代基本采用复制算法,老年代采用标记整理算法。

6.完整的GC流程是怎么样的,对象是如何晋级到老年代的

垃圾收集器

垃圾收集算法是内存回收的方法论,垃圾收集器则是内存回收的具体实现

  • 回收新生代的收集器包括Serial、PraNew、Parallel Scavenge
  • 回收老年代的收集器包括Serial Old、Parallel Old、CMS
  • 回收整个Java堆的G1收集器

以下是关系图,其中不同收集器的连线表示他们可以搭配使用
在这里插入图片描述
对以上垃圾回收器的具体介绍

  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线
    程,优点是简单高效;
  • ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器
    的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐
    量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时
    间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任
    务,适合后台应用等对交互相应要求不高的场景;
  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器
    的老年代版本;
  • Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优
    先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并
    行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、
    低停顿的特点,追求最短GC回收停顿时间。
  • G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收
    集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实
    现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集
    器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年
    代),而前六种收集器回收的范围仅限于新生代或老年代。

CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来
获得最短回收停顿时间的垃圾回收器。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内
存碎片,当剩余内存不能满足程序运行要求时,系统将会出现
Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾
清除,此时的性能将会被降低

总结

新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺
点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃
圾回收。

分代垃圾回收器如何工作?

分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间
的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、
From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:

  • 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
  • 清空 Eden 和 From Survivor 分区;
  • From Survivor 和 To Survivor 分区交换,From Survivor 变 To
    Survivor,To Survivor 变 From Survivor。

每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,
当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进
入老生代。

老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记
整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执
行流程。

内存分配策略

内存管理,包含内存分配和内存回收两个方向。以上介绍了内存回收的方程,下面重点阐述
内存分配过程。

  • 对象的内存分配通常是在 Java 堆上分配(随着虚拟机优化技术的诞生,
    某些场景下也会在栈上分配,后面会详细介绍),对象主要分配在新生代
    的 Eden 区,如果启动了本地线程缓冲,将按照线程优先在 TLAB 上分
    配。少数情况下也会直接在老年代上分配。

  • 多数情况,对象都在新生代 Eden 区分配。当 Eden 区分配没有足够的空
    间进行分配时,虚拟机将会发起一次 Minor GC。如果本次 GC 后还是没
    有足够的空间,则将启用分配担保机制在老年代中分配内存。
    这里我们提到 Minor GC,如果你仔细观察过 GC 日常,通常我们还能从
    日志中发现 Major GC/Full GC。

  • Minor GC 是指发生在新生代的 GC,因为 Java 对象大多都是朝生夕
    死,所有 Minor GC 非常频繁,一般回收速度也非常快;
    Major GC/Full GC 是指发生在老年代的 GC,出现了 Major GC 通常
    会伴随至少一次 Minor GC。Major GC 的速度通常会比 Minor GC 慢
    10 倍以上

  • 大对象优先进入老年代,所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命
    的,会导致在内存还有不少空间的情况下提前触发 GC 以获取足够的连续
    空间来安置新对象。新生代使用的是标记-清除算法来处理垃圾回收的,如果
    大对象直接在新生代分配就会导致 Eden 区和两个 Survivor 区之间发生大
    量的内存复制。因此对于大对象都会直接在老年代进行分配。

  • 长期存活对象将进入老年代。虚拟机采用分代收集的思想来管理内存,那么内存回收时就必须判断哪些
    对象应该放在新生代,哪些对象应该放在老年代。因此虚拟机给每个对象
    定义了一个对象年龄的计数器,如果对象在 Eden 区出生,并且能够被
    Survivor 容纳,将被移动到 Survivor 空间中,这时设置对象年龄为 1。
    对象在 Survivor 区中每「熬过」一次 Minor GC 年龄就加 1,当年龄达到
    一定程度(默认 15) 就会被晋升到老年代。

  • Java在部分场合下也会在栈中分配内存,这里就涉及到逃逸分析,这是比较前沿的虚拟机管理技术

  • 逃逸分析的主要作用就是分析对象作用域。
      当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种行为就叫做 方法逃逸。甚至该对象还可能被外部线程访问到,例如赋值被类变量或可以在其他线程中访问的实例变量,称为 线程逃逸。
      通过逃逸分析技术可以判断一个对象不会逃逸到方法或者线程之外。根据这一特点,就可以让这个对象在栈上分配内存,对象所占用的内存空间就可以随帧栈出栈而销毁。在一般应用中,不会逃逸的局部对象所占比例很大,如果能使用栈上分配,那么大量的对象就会随着方法的结束而自动销毁了,垃圾收集系统的压力就会小很多。
      除此之外,逃逸分析的作用还包括 标量替换 和 同步消除 ;
       标量替换 指:若一个对象被证明不会被外部访问,并且这个对象可以被拆解成若干个基本类型的形式,那么当程序真正执行的时候可以不创建这个对象,而是采用直接创建它的若干个被这个方法所使用到的成员变量来代替,将对象拆分后,除了可以让对象的成员变量在栈上分配和读写之外,还可以为后续进一步的优化手段创造条件。
       同步消除 指:若一个变量被证明不会逃逸出线程,那么这个变量的读写就肯定不会出现竞争的情况,那么对这个变量实施的同步措施也就可以消除掉。

    但是目前的Java虚拟机并没有直接应用逃逸分析技术,因为其实现起来比较复杂,包括Hotspot虚拟机也同样未进行使用

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐