interview

虚拟机篇

1. JVM 内存结构

要求

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

image-20210831165728217

说明

会发生内存溢出的区域

方法区、永久代、元空间

image-20210831170457337

从这张图学到三点

image-20210831170512418

从这张图可以学到

*

2. JVM 内存参数

要求

堆内存,按大小设置

image-20210831173130717

解释:

堆内存,按比例设置

image-20210831173045700

解释:

元空间内存设置

image-20210831173118634

解释:

注意:

代码缓存内存设置

image-20210831173148816

解释:

线程内存设置

image-20210831173155481

官方参考文档

3. JVM 垃圾回收

要求

三种垃圾回收算法

标记清除法

image-20210831211008162

解释:

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

要点:

标记整理法

image-20210831211641241

解释:

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

特点:

标记复制法

image-20210831212125813

解释:

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

特点:

GC 与分代回收算法

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

GC 要点:

分代回收

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

image-20210831213622704

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

image-20210831213640110

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

image-20210831213657861

  1. 将 from 和 to 交换位置

image-20210831213708776

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

image-20210831213724858

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

image-20210831213737669

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

image-20210831213804315

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

image-20210831213815371

  1. 将 from 和 to 交换位置

image-20210831213826017

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

GC 规模

三色标记

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

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

image-20210831215016566

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

image-20210831215033510

  1. 依次类推

image-20210831215105280

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

image-20210831215146276

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

image-20210831215158311

并发漏标问题

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

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

image-20210831215846876

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

image-20210831215904073

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

image-20210831215919493

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

image-20210831220004062

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

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

垃圾回收器 - Parallel GC

垃圾回收器 - ConcurrentMarkSweep GC

垃圾回收器 - G1 GC

G1 回收阶段 - 新生代回收

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

image-20210831222639754

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

image-20210831222653802

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

image-20210831222705814

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

image-20210831222724999

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

image-20210831222737928

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

image-20210831222752787

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

image-20210831222803281

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

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

image-20210831222813959

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

image-20210831222828104

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

image-20210831222841096

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

image-20210831222859760

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

image-20210831222919182

4. 内存溢出

要求

典型情况

5. 类加载

要求

类加载过程的三个阶段

  1. 加载

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

    2. 如果此类的父类没有加载,先加载父类
    3. 加载是懒惰执行
  2. 链接
    1. 验证 – 验证类是否符合 Class 规范,合法性、安全性检查
    2. 准备 – 为 static 变量分配空间,设置默认值
    3. 解析 – 将常量池的符号引用解析为直接引用 * 超过short最大值32797使用常量池 * 调用类的静态常量引用类型static final Object = new Object、静态引用类型和 *.class类型Student.class会触发类加载和初始化,静态常量static final int不会。 * 每一个类都有常量池,其他类未加载时是符号引用,类加载后变成直接引用(拥有具体的内存地址,常量池和类的其他信息都存放到方法区中)。
  3. 初始化
    1. 静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个 <cinit> 方法,在初始化时被调用
    2. static final 修饰的基本类型变量赋值,在链接阶段就已完成
    3. 初始化是懒惰执行

验证手段

代码说明

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-20210901110910016

错在哪了?

代码说明

6. 四种引用

要求

强引用

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

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

image-20210901111903574

软引用(SoftReference)

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

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

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

  4. 典型例子是反射数据

image-20210901111957328

弱引用(WeakReference)

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

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

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

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

image-20210901112107707

// 弱引用队列可解决ThreadLocal的ThreadLocalMap内存泄露,但是成本高不建议使用。
// (一般手动使用remove移除map的value对象,而不是让gc回收)
// ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题
// ThreadLocal 同时实现了线程内的资源共享
// ThreadLocal 不同线程间隔离,同一个线程共享变量。
package day03.reference;

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

public class TestWeakReference {

    public static void main(String[] args) {
        MyWeakMap map = new MyWeakMap();
        map.put(0, new String("a"), "1");
        // 强引用不会被当前方法gc回收
        map.put(1, "b", "2");
        map.put(2, new String("c"), "3");
        map.put(3, new String("d"), "4");
        System.out.println(map);

        System.gc();
        System.out.println(map.get("a"));
        System.out.println(map.get("b"));
        System.out.println(map.get("c"));
        System.out.println(map.get("d"));
        System.out.println(map);
        map.clean();
        System.out.println(map);
    }

    // 模拟 ThreadLocalMap 的内存泄漏问题以及一种解决方法
    static class MyWeakMap {
        // 给ThreadLocalMap增加引用队列,清除value的强引用
        static ReferenceQueue<Object> queue = new ReferenceQueue<>();
        static class Entry extends WeakReference<String> {
            String value;

            public Entry(String key, String value) {
                super(key, queue);
                this.value = value;
            }
        }
        // 回收资源
        public void clean() {
            Object ref;
            while ((ref = queue.poll()) != null) {
                System.out.println(ref);
                for (int i = 0; i < table.length; i++) {
                    if(table[i] == ref) {
                        table[i] = null;
                    }
                }
            }
        }

        Entry[] table = new Entry[4];

        public void put(int index, String key, String value) {
            table[index] = new Entry(key, value);
        }

        public String get(String key) {
            for (Entry entry : table) {
                if (entry != null) {
                    String k = entry.get();
                    if (k != null && k.equals(key)) {
                        return entry.value;
                    }
                }
            }
            return null;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("[");
            for (Entry entry : table) {
                if (entry != null) {
                    String k = entry.get();
                    sb.append(k).append(":").append(entry.value).append(",");
                }
            }
            if (sb.length() > 1) {
                sb.deleteCharAt(sb.length() - 1);
            }
            sb.append("]");
            return sb.toString();
        }
    }
}

执行结果:

[a:1,b:2,c:3,d:4] null 2 null null [null:1,b:2,null:3,null:4] day03.reference.TestWeakReference$MyWeakMap$Entry@b81eda8 day03.reference.TestWeakReference$MyWeakMap$Entry@68de145 day03.reference.TestWeakReference$MyWeakMap$Entry@27fa135a [b:2]

进程已结束,退出代码0

总结一下: 在MyWeakMap类中,Entry类继承了WeakReference,其中使用了弱引用来持有键(key)。通过将键包装在Entry对象中并使用弱引用,可以实现在没有强引用持有键时,键所对应的Entry对象可以被垃圾回收器回收。 在调用gc,由于软引用的键new string acd 和字符串b都是强引用,所以都不会被回收,可以获取到数据。

转换一下就是:当对象只被弱引用引用时,如果垃圾回收器扫描到这个对象时发现只有弱引用指向它,那么该对象就会被标记为可回收,并在适当的时机被垃圾回收器回收。 由于都拥有强引用,所以不能被回收。

  1. 在给定的代码中,字符串 "b" 是一个强引用对象,而不是一个弱引用对象或可引用对象。

    通过 map.put(1, "b", "2") 将字符串 "b" 直接作为键传递给了 put 方法,它不会被包装在弱引用或其他特殊引用对象中。因此,字符串 "b" 是一个强引用对象,其生命周期与持有它的变量或数据结构相关联。

    弱引用对象是指在没有其他强引用持有它时可以被垃圾回收器回收的对象。在给定的代码中,并没有使用弱引用来包装字符串 "b",因此它不属于弱引用对象。

    可引用对象是一个更广义的概念,它包括强引用对象、软引用对象、弱引用对象和虚引用对象。在给定的代码中,并没有使用任何引用类型来包装字符串 "b",所以它也不属于可引用对象。

    综上所述,在给定的代码中,字符串 "b" 是一个强引用对象,它的生命周期取决于具体的引用和使用方式。它不是一个弱引用对象或可引用对象。

  2. 弱引用的作用是确保在没有其他强引用持有对象时,对象可以被垃圾回收器回收。对于字符串常量来说,它们是被 JVM 管理的共享资源,存在于常量池中,并且可以被多个引用共享。因此,字符串常量不需要使用弱引用来确保垃圾回收。
  3. 所以该Entry对象不会进入引用队列,可以看到最终打印结果:[b:2]
  4. 如果把map.put(0, new String("a"), "1");改成String a = new String("a");map.put(0, a, "1");该Entry也不会被回收。

最终:弱引用自身需要配合引用队列来释放,像软引用一样。

static ReferenceQueue<Object> queue = new ReferenceQueue<>();// 引用队列
static class Entry extends WeakReference<String> {
    String value;
    public Entry(String key, String value) {
        super(key, queue);// 入队
        this.value = value;
    }
}
public void clean() {
    Object ref;
    while ((ref = queue.poll()) != null) {// 检查引用队列中是否有可用的引用对象,并将其从队列中移除并返回。
        System.out.println(ref);
        for (int i = 0; i < table.length; i++) {
            if(table[i] == ref) {
                table[i] = null;// 将整个Entry设为null,等待垃圾回收
            }
        }
    }
}
Entry[] table = new Entry[4];// 缓存

虚引用(PhantomReference)

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

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

  3. 典型例子是 jdk.internal.ref.Cleaner 释放 java.nio.DirectByteBuffer 关联的直接内存

image-20210901112157901

public class TestPhantomReference {
    public static void main(String[] args) throws IOException, InterruptedException {
        ReferenceQueue<String> queue = new ReferenceQueue<>();// 引用队列
        List<MyResource> list = new ArrayList<>();
        list.add(new MyResource(new String("a"), queue));
        list.add(new MyResource("b", queue));
        list.add(new MyResource(new String("c"), queue));

        System.gc(); // 垃圾回收
        Thread.sleep(100);
        Object ref;
        // 循环出队
        while ((ref = queue.poll()) != null) {
            // 语言级别 '8' 不支持 'instanceof' 中的模式 升级到16
//            if (ref instanceof MyResource resource) {
//                resource.clean();
//            }
            if (ref instanceof MyResource) {
                ((MyResource)ref).clean();
            }
        }
    }

    static class MyResource extends PhantomReference<String> {
        public MyResource(String referent, ReferenceQueue<? super String> q) {
            super(referent, q);
        }
        // 释放外部资源的方法
        public void clean() {
            LoggerUtils.get().debug("clean");
        }
    }
}

代码说明

//import java.lang.ref.Cleaner;
// 前面讲的弱、虚引用配合引用队列,目的都是为了找到哪些 java 对象被回收,从而进行对它们关联的资源进行进一步清理
// 为了简化 api 难度,从 java 9 开始引入了 Cleaner 对象
public class TestCleaner1 {
    public static void main(String[] args) throws IOException {
        Cleaner cleaner = Cleaner.create();

        cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 1"));
        cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 2"));
        cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 3"));
        MyResource obj = new MyResource();
        cleaner.register(obj, ()-> LoggerUtils.get().debug("clean 4"));
        cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 5"));
        cleaner.register(new MyResource(), ()-> LoggerUtils.get().debug("clean 6"));

        System.gc();
        System.in.read();
    }
    static class MyResource {
    }
}
//import jdk.internal.ref.Cleaner;
public class TestCleaner2 {
    public static void main(String[] args) throws IOException {
        Cleaner cleaner1 = Cleaner.create(new MyResource(), ()-> LoggerUtils.get().debug("clean 1"));
        Cleaner cleaner2 = Cleaner.create(new MyResource(), ()-> LoggerUtils.get().debug("clean 2"));
        Cleaner cleaner3 = Cleaner.create(new MyResource(), ()-> LoggerUtils.get().debug("clean 3"));
        Cleaner cleaner4 = Cleaner.create(new MyResource(), ()-> LoggerUtils.get().debug("clean 4"));

        System.gc();
        System.in.read();
    }
    static class MyResource {
    }
}

7. finalize

要求

finalize

finalize 原理

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

image-20210901121032813

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

image-20210901122228916

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

finalize 缺点

代码说明