虚拟机篇

1. JVM 内存结构

要求

结合一段 java 代码的执行理解内存划分

Image

说明

会发生内存溢出的区域

方法区、永久代、元空间

Image

从这张图学到三点

Image

从这张图可以学到

2. JVM 内存参数

要求

堆内存,按大小设置

Image

解释:

堆内存,按比例设置

Image

解释:

元空间内存设置

Image

解释:

注意:

代码缓存内存设置

Image

解释:

线程内存设置

Image

官方参考文档

3. JVM 垃圾回收

要求

三种垃圾回收算法

标记清除法

Image

解释:

  1. 找到 GC Root 对象,即那些一定不会被回收的对象,如正执行方法内局部变量引用的对象、静态变量引用的对象
  2. 标记阶段:沿着 GC Root 对象的引用链找,直接或间接引用到的对象加上标记
  3. 清除阶段:释放未加标记的对象占用的内存

要点:

标记整理法

Image

解释:

  1. 前面的标记阶段、清理阶段与标记清除法类似
  2. 多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片产生

特点:

标记复制法

Image

解释:

  1. 将整个内存分成两个大小相等的区域,from 和 to,其中 to 总是处于空闲,from 存储新创建的对象
  2. 标记阶段与前面的算法类似
  3. 在找出存活对象后,会将它们从 from 复制到 to 区域,复制的过程中自然完成了碎片整理
  4. 复制完成后,交换 from 和 to 的位置即可

特点:

GC 与分代回收算法

GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度

GC 要点:

分代回收

  1. 伊甸园 eden,最初对象都分配到这里,与幸存区 survivor(分成 from 和 to)合称新生代,

Image

  1. 当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象

Image

  1. 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放

Image

  1. 将 from 和 to 交换位置

Image

  1. 经过一段时间后伊甸园的内存又出现不足

Image

  1. 标记伊甸园与 from(现阶段没有)的存活对象

Image

  1. 将存活对象采用复制算法复制到 to 中

Image

  1. 复制完毕后,伊甸园和 from 内存都得到释放

Image

  1. 将 from 和 to 交换位置

Image

  1. 老年代 old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

GC 规模

三色标记

即用三种颜色记录对象的标记状态

  1. 起始的三个对象还未处理完成,用灰色表示

Image

  1. 该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色

Image

  1. 依次类推

Image

  1. 沿着引用链都标记了一遍

Image

  1. 最后为标记的白色对象,即为垃圾

Image

并发漏标问题

比较先进的垃圾回收器都支持并发标记,即在标记过程中,用户线程仍然能工作。但这样带来一个新的问题,如果用户线程修改了对象引用,那么就存在漏标问题。例如:

  1. 如图所示标记工作尚未完成

Image

  1. 用户线程同时在工作,断开了第一层 3、4 两个对象之间的引用,这时对于正在处理 3 号对象的垃圾回收线程来讲,它会将 4 号对象当做是白色垃圾

Image

  1. 但如果其他用户线程又建立了 2、4 两个对象的引用,这时因为 2 号对象是黑色已处理对象了,因此垃圾回收线程不会察觉到这个引用关系的变化,从而产生了漏标

Image

  1. 如果用户线程让黑色对象引用了一个新增对象,一样会存在漏标问题

Image

因此对于并发标记而言,必须解决漏标问题,也就是要记录标记过程中的变化。有两种解决方法:

  1. Incremental Update 增量更新法,CMS 垃圾回收器采用
    • 思路是拦截每次赋值动作,只要赋值发生,被赋值的对象就会被记录下来,在重新标记阶段再确认一遍
  2. Snapshot At The Beginning,SATB 原始快照法,G1 垃圾回收器采用
    • 思路也是拦截每次赋值动作,不过记录的对象不同,也需要在重新标记阶段对这些对象二次处理
    • 新加对象会被记录
    • 被删除引用关系的对象也被记录

垃圾回收器 - Parallel GC

垃圾回收器 - ConcurrentMarkSweep GC

垃圾回收器 - G1 GC

G1 回收阶段 - 新生代回收

  1. 初始时,所有区域都处于空闲状态

Image

  1. 创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象

Image

  1. 当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程

Image

  1. 复制完成,将之前的伊甸园内存释放

Image

  1. 随着时间流逝,伊甸园的内存又有不足

Image

  1. 将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代

Image

  1. 释放伊甸园以及之前幸存区的内存

Image

G1 回收阶段 - 并发标记与混合收集

  1. 当老年代占用内存超过阈值后,触发并发标记,这时无需暂停用户线程

Image

  1. 并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来)。

Image

  1. 混合收集阶段中,参与复制的有 eden、survivor、old,下图显示了伊甸园和幸存区的存活对象复制

Image

  1. 下图显示了老年代和幸存区晋升的存活对象的复制

Image

  1. 复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集

Image

4. 内存溢出

要求

典型情况

5. 类加载

要求

类加载过程的三个阶段

  1. 加载

    1. 将类的字节码载入方法区,并创建类.class 对象

    2. 如果此类的父类没有加载,先加载父类

    3. 加载是懒惰执行

  2. 链接

    1. 验证 – 验证类是否符合 Class 规范,合法性、安全性检查
    2. 准备 – 为 static 变量分配空间,设置默认值
    3. 解析 – 将常量池的符号引用解析为直接引用
  3. 初始化

    1. 静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个 <cinit> 方法,在初始化时被调用
    2. static final 修饰的基本类型变量赋值,在链接阶段就已完成
    3. 初始化是懒惰执行

验证手段

  • 使用 jps 查看进程号
  • 使用 jhsdb 调试,执行命令 jhsdb.exe hsdb 打开它的图形界面
    • Class Browser 可以查看当前 jvm 中加载了哪些类
    • 控制台的 universe 命令查看堆内存范围
    • 控制台的 g1regiondetails 命令查看 region 详情
    • scanoops 起始地址 结束地址 对象类型 可以根据类型查找某个区间内的对象地址
    • 控制台的 inspect 地址 指令能够查看这个地址对应的对象详情
  • 使用 javap 命令可以查看 class 字节码

代码说明

  • day03.loader.TestLazy - 验证类的加载是懒惰的,用到时才触发类加载
  • day03.loader.TestFinal - 验证使用 final 修饰的变量不会触发类加载

jdk 8 的类加载器

名称 加载哪的类 说明
Bootstrap ClassLoader JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为 Bootstrap,显示为 null
Application ClassLoader classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application

双亲委派机制

所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器

双亲委派的目的有两点

  1. 让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到 jdk 提供的核心类

  2. 让类的加载有优先次序,保证核心类优先加载

对双亲委派的误解

下面面试题的回答是错误的

Image

错在哪了?

代码说明

  • day03.loader.TestJdk9ClassLoader - 演示类加载器与模块的绑定关系

6. 四种引用

要求

强引用

  1. 普通变量赋值即为强引用,如 A a = new A();

  2. 通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收

Image

软引用(SoftReference)

  1. 例如:SoftReference a = new SoftReference(new A());

  2. 如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象

  3. 软引用自身需要配合引用队列来释放

  4. 典型例子是反射数据

Image

弱引用(WeakReference)

  1. 例如:WeakReference a = new WeakReference(new A());

  2. 如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象

  3. 弱引用自身需要配合引用队列来释放

  4. 典型例子是 ThreadLocalMap 中的 Entry 对象

Image

虚引用(PhantomReference)

  1. 例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);

  2. 必须配合引用队列一起使用,当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理

  3. 典型例子是 Cleaner 释放 DirectByteBuffer 关联的直接内存

Image

代码说明

  • day03.reference.TestPhantomReference - 演示虚引用的基本用法
  • day03.reference.TestWeakReference - 模拟 ThreadLocalMap, 采用引用队列释放 entry 内存

7. finalize

要求

finalize

finalize 原理

  1. 对 finalize 方法进行处理的核心逻辑位于 java.lang.ref.Finalizer 类中,它包含了名为 unfinalized 的静态变量(双向链表结构),Finalizer 也可被视为另一种引用对象(地位与软、弱、虚相当,只是不对外,无法直接使用)
  2. 当重写了 finalize 方法的对象,在构造方法调用之时,JVM 都会将其包装成一个 Finalizer 对象,并加入 unfinalized 链表中

Image

  1. Finalizer 类中还有另一个重要的静态变量,即 ReferenceQueue 引用队列,刚开始它是空的。当狗对象可以被当作垃圾回收时,就会把这些狗对象对应的 Finalizer 对象加入此引用队列
  2. 但此时 Dog 对象还没法被立刻回收,因为 unfinalized -> Finalizer 这一引用链还在引用它嘛,为的是【先别着急回收啊,等我调完 finalize 方法,再回收】
  3. FinalizerThread 线程会从 ReferenceQueue 中逐一取出每个 Finalizer 对象,把它们从链表断开并真正调用 finallize 方法

Image

  1. 由于整个 Finalizer 对象已经从 unfinalized 链表中断开,这样没谁能引用到它和狗对象,所以下次 gc 时就被回收了

finalize 缺点

代码说明

  • day03.reference.TestFinalize - finalize 的测试代码