《深入理解JAVA虚拟机》

1. 对象已死吗



  • 引用计数法

    • 循环引用问题

  • 可达性分析算法
  • 可达性标记过程

    • 第一次标记:gcroots不可达,判断finalize函数被重写且未被执行过,则被放入F-Queue队列,否则没必要执行
    • jvm会自动创建一个优先级较低的Finalizer线程去执行队列。finalize方法只会被执行一次。
    • 对象的finalize方法是逃脱gc的最后机会,但是不建议使用,因为调用时间不可控。

  • 方法区的回收

    • JVM规范不要求虚拟机对方法区进行gc,因为效率较低
    • 无用的常量:没有String对象或者字面量引用常量池里的"abc"
    • 无用的类:1该类所有实例都被回收,2加载它的classloader被回收,3没有任何地方引用或者通过反射使用该类的Class对象
    • hotspot通过参数-Xnoclassgc来控制是否对无用的类进行回收
    • 大量使用反射、动态代理、cglib、动态生成jsp等场景,都需要具备类卸载的功能,防止永久代溢出




2. 垃圾回收算法



  • 标记-清除算法(Mark-Sweep)

    • 缺点:内存碎片;效率不高

  • 拷贝算法

    • 将内存划分为两个区域,一个Eden和两个Survivor(8:1),新创建的对象都会被分配到Eden区
    • 一次GC后将Eden和from区活着的对象复制到to区,此时对象年龄1,当对象的年龄达到某个值时 ( 默认是 15 岁,通过-XX:MaxTenuringThreshold 来设定参数),这些对象就会成为老年代
    • S中同龄对象之和大于一半,则大于等于该年龄的对象直接进入老年代。这两种情况需要【空间分配担保】
    • Survior不够用时会进入老年代
    • 优点:运行高效,没有碎片问题。多用于新生代回收
    • 缺点:内存变为原来的一半了

  • 标记-压缩算法(Mark-Sweep-Compact)

    • 解决了内存碎片的问题;在标记-清除算法的基础上,又进行了对象的移动,因此成本更高

  • 分代回收(Generational Collecting)

    • 所有新生成的对象首先都是放在年轻代的
    • 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中,内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC
    • 持久代(Permanent Generation)用于存放静态文件,如Java类、方法等




GC中的并发和并行



  • 并行Parallel:多条线程同时回收垃圾,但是用户线程暂停
  • 并发Concurrent:垃圾回收的同时,用户线程也在执行



2. 垃圾回收器



    • 单线程,没有线程切换消耗
    • 处理时会暂停其它工作线程

  • Serial Old(标记-整理算法)

    • Seria的年老代版本,同样是单线程
    • server模式下可作为CMS失败的后备,或者与Parallel Scavenge搭配使用(serial old与PS自带的PS MarkSweep类似)

  • ParNew -- 拷贝算法

    • Serial的多线程版本,即在Serial基础上用多条线程进行垃圾回收
    • 只有它可与CMS配合使用

  • Parallel Scavenge -- 拷贝算法

    • 多线程
    • 侧重点:与CMS关注STW时间不同,更注重提高吞吐率(CPU执行代码时间占CPU总执行时间的比率),即gc执行时间更少
    • 可以控制新生代垃圾回收的STW时间(参数-XX:MaxGCPauseMillis设置)和最大吞吐率
    • 可配置打开GC自适应调节策略,就不需要设置SurviorRate和老年代年龄了
    • 适合CPU密集型计算后台

  • Parallel Old(标记-整理算法)

    • PS的老年代版本,解决了PS+Serial Old效率不高的问题

  • Concurrent Mark Sweep(CMS)(标记-清除算法)

    • 用于老年代,目标是能够有效减少STW时长
    • 4个阶段:

      • 初始标记

        • 仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”

      • 并发标记

        • 进行GC Roots Tracing的过程

      • 重新标记

        • 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
        • 比初始标记时间稍长,需要“Stop The World”

      • 并发清理

    • 总的来说,耗时较长的并发标记和并发清理,都能与用户线程同时进行
    • 缺点:

      • 当次无法清除浮动垃圾
      • 碎片问题


  • Garbage-First(G1)(标记-整理算法)

    • G1回收器诞生于Hotspot VM的7update4版本
    • 优点:同时负责新生代和老年代的垃圾回收;Region使用拷贝算法,G1回收器不会产生内存碎片;提供更短更可控的STW时长
    • 内存模型

      • 将整个Java堆划分为多个大小相等的独立区域(Region)
      • 保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合
      • 在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region
      • 每个Region都有一个Remembered Set,Reference对象写操作时检查是否处于不同的Region,如果是则在Set中记录

    • 回收几个步骤:

      • 初始标记(Initial Marking)
      • 并发标记(Concurrent Marking)

        • 从GC Roots开始可达性分析,找到存活的对象

      • 最终标记(Final Marking)

        • 并发标记执行过程中,变动的标记数据保存在线程Remembered Set Logs
        • 该阶段将Remembered Set Logs合并到Remembered Set中,可并行执行

      • 筛选回收(Live Data Counting and Evacuation

        • 首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划


    • 调优

  • GC有两种类型

    • Scavenge GC





  • Full GC

    • 需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数
    • 如下原因可能导致Full GC:

      • 1.年老代(Tenured)被写满,或者担保空间不足
      • 2.持久代(Perm)被写满 
      • 3.System.gc()被显示调用 
      • 4.上一次GC之后Heap的各域分配策略动态变化





4. GC组合的配置

​​

  • ​https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28​
  • -XX:+UseSerialGC

    • Serial + Serial Old
    • 适合数据量很小,例如小于100M
    • 适合单处理器,且没有暂停时间要求

  • -XX:+UseParNewGC

    • ParNew + Serial Old

  • -XX:+UseParallelGC--本地默认

    • 性能是第一追求,且没有暂停时间要求或暂停1秒多可以接受

  • -XX:+UseParallelOldGC

    • Parallel Scavenge + Parallel Old

  • -XX:+UseConcMarkSweepGC

    • ParNew + CMS + Serial Old

  • -XX:+UseG1GC

    • 响应时间比吞吐量重要,且暂停时间少于1秒




4. 内存泄漏问题list



  • 1.静态集合类像HashMap、Vector等的使用最容易出现内存泄露
  • 2.各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。
  • 3.监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。