垃圾回收与内存分配

一些基础

  • 对象的四种引用类型

    • 强引用,内存不足时报错oom,但不会该类对象
    • 弱引用,当内存不足时才会回收
    • 软引用,不管内存是否充足,在gc都会回收
    • 虚引用,任何时候都可以被回收
  • 怎么判断对象是否仍在使用?

    • 引用计数法
      每个对象有一个引用计数属性,当被引用时计数+1,当引用释放时-1。两个对象互相循环使用时会导致对象无法回收
    • 可达性分析——Java默认
      若从GC Roots向下搜索时,走过的路径称为引用链。当对象没有任何引用链时代表已不可用。
  • 可作为GC Roots的对象有哪几类?

    • 虚拟机栈中引用的对象
    • 方法区类静态属性引用的对象
    • 方法区常量引用的对象
    • 本地方法中引用的对象
  • 方法区回收的必要条件

    • 该类所有实例都已被回收
    • 该类对应的类加载器已被回收(很难)
    • 无法通过反射访问该类(该类对应的java.lang,Class没有被引用)
  • 安全点和安全区域

    • 安全点特定位置

      1. 循环的末尾
      2. 方法返回前
      3. 调用方法的Call之后
      4. 抛出异常的位置
    • 安全区域

    • 线程中断的方式
    1. 主动式:不对线程操作,简单的设置一个标记位,线程不断主动轮询这个标志位状态,为真时,线程在最近的安全点挂起
    2. 被动式:系统直接把所有线程中断,如发现有的线程不在安全点时,就恢复执行到最近的安全点(不采用)
  • 并发情况下的可达性变动解决算法(G1和cms)

    • 增量空间算法——CMS
      当出现新的引用对象时,则记录下该对象,待扫描结束后再以此为根重新扫描一次
    • 原始快照算法——G1
      当出现删除引用时,将要删除的引用记录下来,待扫描结束后再将该对象为根重新扫描

垃圾回收算法

  • 标记-清除算法
    标记需要回收的对象或不需要回收的对象,再统一清除。
    缺点:

    1. 效率不稳定,无论标记还是清除,效率都会随着对象增多而降低
    2. 内存空间碎片化
  • 标记-复制算法
    把内存空间分为两块,每次只使用其中一块,当该块内存不足后,把存活的对象复制到另一块,再把原空间一次性清除。
    优点:没有内存碎片。
    缺点:内存空间利用率低;当存活对象较多时,效率变低

  • 标记-整理算法
    标记完成后,将存活的对象向一端移动,清除边界以外的内容
    优点:内存规整,对象赋值/创建速度快
    缺点:标记、清理效率不高

  • 分代收集

    • 新生代,每次垃圾收集都有大量对象死去,选用标记-复制算法
    • 老年代,对象存活率较高,没有其余区域可以进行分配担保,使用标记-清除、标记-整理算法

垃圾回收器

  • 新生代

    • Serial
      串行,标记复制算法。客户端默认新生代收集器
    • ParNew
      并行,标记复制算法;只有ParNew和serial可以配合CMS使用
    • Parallel Scavenge
      并行,标记复制算法;吞吐量优先收集器,可控制吞吐量(用户代码运行时间/总时间)
      1. 最大垃圾收集停顿时间参数[绝对值]
      2. 垃圾回收时间占总时间比率参数[相对值]
      3. 自适应调节空间参数[布尔值],开启后可自动调节Eden、Survivor区域大小
  • 老年代

    • Serial Old
      标记整理算法,主要供客户端模式,若用于服务端:与Parallel Scavenge搭配使用;或者作为CMS发生失败后的与预案
    • Parallel Old
      标记整理算法,Parallel Scavenge的老年版
    • CMS
  • CMS

    • 四个步骤:
      1. 初始标记:STW,只标记GC Roots直接关联到的对象
      2. 并发标记:与用户线程一起遍历整个对象图
      3. 重新标记:STW,修整并发标记期间,因用户线程导致引用变化的部分(增量更新算法实现)
      4. 并发清除:与用户线程并行,清除已死亡的对象
    • 三个缺点:
      1. 对处理器资源敏感,默认启动线程数(处理器核心数+3)/4
      2. 产生浮动垃圾。并发标记和清除阶段会与用户线程一起运行,所以需要预留一部分空间供用户线程使用,当空间不足时,会临时启用预案:使用serial old进行标记整理
      3. 标记清除算法导致内存碎片。大对象不够分配时,触发full gc耗时。可设置参数,多次清楚后,在下一次full gc前进行一次整理
  • G1

    • 四个步骤:
    1. 初始标记:在minor gc阶段只标记gc Roots引用的对象,所以不存在停顿
    2. 并发标记:与用户线程并发执行,标记完成后处理SATB(原始快照算法)记录在并发时有变动的对象
    3. 最终标记:STW(短暂),处理并发结束后仍遗留的少量SATB记录
    4. 筛选回收:STW,对各Region的回收价值与成本进行排序,根据期望停顿时间指定回收计划。将存活对象复制到空闲Region后,清除原Region

常见问题

  • finalize() 方法什么时候被调用?它的目的是什么?
    对象被释放前被垃圾回收器调用,目的是释放该对象所持有的资源(对外内存、长连接等),且该方法只会被调用一次。(不建议使用)
  • 为什么要有不同的引用类型?
    由于gc回收时机不可控,且有时候需要适当的控制对象被回收的时机。比如新建 Person类,每次查询信息都需要重新构建实例,对象生命周期过短,引起巨大的消耗。听过软引用和HashMap的结合可以构建高速缓存,提升性能。
07-08 15:35