虚拟机篇

本贴最后更新于 509 天前,其中的信息可能已经东海扬尘

wallhaven6d2vl61920x1080.png

1. JVM 内存空间&垃圾回收器

1.1.JVM 内存结构

image.png

Java Source 属于源代码,编译成字节码,编程 Java Class

JVM 会创建 main 主线程,由 JVM stacks 虚拟机栈分配空间,然后通过类加载子系统将字节码存储到方法区。

遇到 Student 没有见过的类,再次触发类加载子系统存储到方法区。

当我们在进行 new Student()时,将会存储在堆中

stu,args 占用的内存有 JVM 虚拟机栈中的栈帧内存

解析器将字节码解析成机械码

JIT 及时编译器将热点代码翻译成机械码并缓存起来

垃圾回收器,当对象不在使用,将会将对象回收

  • 哪些部分会出现内存溢出

不会出现内存溢出的区域 – 程序计数器

出现 OutOfMemoryError 的情况

① 堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收

② 方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类

③ 虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时

出现 StackOverflowError 的区域

① 虚拟机栈内部 - 方法调用次数过多

image.png

要通过.class 才能访问对象

方法区是永久代和元空间的定义

而永久代和元空间是 JVM 实现

image.png

当堆内存中的对象不在被使用,内存不足,进行堆内存中垃圾回收后,元空间中的数据才会被释放。堆中部分对象不被使用时,元空间数据并不会被释放

1.2.JVM 内存参数

image.png

  • -XX:NewRatio=2:1 表示老年代占两份,新生代占一份
  • -XX:SurvivorRatio=4:1 表示新生代分成六份,伊甸园占四份,from 和 to 各占一份

10G,2G

image.png

  • -Xms 最小堆内存(包括新生代和老年代)
  • -Xmx 最大堆内存(包括新生代和老年代)
  • 通常建议将 -Xms 与 -Xmx 设置为大小相等,即不需要保留内存,不需要从小到大增长,这样性能较好
  • -XX:NewSize 与 -XX:MaxNewSize 设置新生代的最小与最大值,但一般不建议设置,由 JVM 自己控制
  • -Xmn 设置新生代大小,相当于同时设置了 -XX:NewSize 与 -XX:MaxNewSize 并且取值相等
  • 保留是指,一开始不会占用那么多内存,随着使用内存越来越多,会逐步使用这部分保留内存。

1.3.JVM 垃圾回收算法

image.png

GC Root 代表不能被回收的对象。比如:方法正在运行,一个局部变样正在引用的对象;静态变量引用的对象;

标记不能被回收的对象,回收未被标记的对象。

缺点

  • 标记清除会出现内容碎片问题,当要求要一个连续的内存时,可能不够用。

image.png

缺点

  • 标记速度与存活对象线性关系
  • 清除与整理速度与内存大小成线性关系

image.png

缺点

  • 标记与复制速度与存活对象成线性关系

总结

  • 标记整理适用于老年代的回收,标记复制适用于新生代的回收

1.4.面试题:说说 GC 和分代回收算法

  • GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度
  • GC 要点
    • 回收区域是堆内存,不包括虚拟机栈,在方法调用结束会自动释放方法占用内存
    • 判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
    • GC 具体的实现称为垃圾回收器
    • GC 大都采用了分代回收思想,理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收,根据这两类对象的特性将回收区域分为新生代和老年代,不同区域应用不同的回收策略
    • 根据 GC 的规模可以分成 Minor GC,Mixed GC,Full GC

1.5.分代回收

  • 伊甸园 eden,最初对象都分配到这里,与幸存区合称新生代
  • 幸存区 survivor,当伊甸园内存不足,回收后的幸存对象到这里,分成 from 和 to,采用标记复制算法
  • 老年代 old,当幸存区对象熬过几次回收(最多 15 次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

image.png

image.png

GC 规模

  • Minor GC 发生在新生代的垃圾回收,暂停时间短
  • Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
  • Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免

1.6.三色标记与并发漏标问题

  • 用三种颜色记录对象的标记状态
    • 黑色 – 已标记
    • 灰色 – 标记中
    • 白色 – 还未标记

image.png

  • 漏标问题 – 记录标记过程中变化
    • Incremental Update(增量更新)
      • 只要赋值发生,被赋值的对象就会被记录
    • Snapshot At The Beginning,SATB(原始快照)
      • 新加对象会被记录
      • 被删除引用关系的对象也被记录

image.png

1.7.垃圾回收器

Parallel GC

  • eden 内存不足发生 Minor GC,标记复制 STW
  • old 内存不足发生 Full GC,标记整理 STW
  • 注重吞吐量

ConcurrentMarkSweep GC

  • old 并发标记,重新标记时需要 STW,并发清除
  • Failback Full GC
  • 注重响应时间

G1 GC

  • 响应时间与吞吐量兼顾
  • 划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous
  • 新生代回收:eden 内存不足,标记复制 STW
  • 并发标记:old 并发标记,重新标记时需要 STW
  • 混合收集:并发标记完成,开始混合收集,参与复制的有 eden、survivor、old,其中 old 会根据暂停时间目标,选择部分回收价值高的区域,复制时 STW
  • Failback Full GC

2. 内存溢出&类加载

2.1.误用线程池导致内存溢出

image.png

image.png

该工具底层用了 LinkedBlockingQueue,代表用的是无边界的队列,导致内存占用过多

2.2.查询数据量太大导致的内存溢出

image.png

2.3.动态生成类导致的内存溢出

image.png

GroovyShell 内部存在一个类加载器,因为 GroovyShell 定义为了静态变量,导致不能被垃圾回收,导致内存溢出

2.4.类加载过程分为三个阶段

加载

  • 将类的字节码载入方法区,并创建类 class 对象
  • 如果此类的父类没有加载,先加载父类
  • 加载是懒惰执行

链接

  • 验证--验证类是否符合 class 规范,合法性、安全性检查
  • 准备--为 static 变量分配空间,设置默认值
  • 解析--将常量池的符号引用解析为直接引用

初始化

  • 执行静态代码块与非 final 静态变量的赋值
  • 初始化是懒惰执行

何为双亲委派

  • 所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器
    • 能找到这个类,由上级加载,加载后该类也对下级加载器可见
    • 找不到这个类,则下级类加载器才有资格执行加载

image.png

一道错误的面试题解答

image.png

错在哪了?
自己编写类加载器就能加载一个假冒的 java.lang.System 吗?
不行。

  • 假设你自己的类加载器用双亲委派,那么优先由启动类加载器加载真正的 java.lang.System,自然不会加载假冒的
  • 假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的 java.lang.System 时,它需要先加载父类 java.lang.Object,而你没有用委派,找不到 java.lang.Object 所以加载会失败
  • 以上也仅仅是假设。实际操作你就会发现自定义类加载器加载以 java. 打头的类时,会抛安全异常,在 jdk9 以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了

双亲委派的目的有两点

  • 让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到 jdk 提供的核心类
  • 让类的加载有优先次序,保证核心类优先加载

2.5.面试题:对象引用类型分为哪几类?

  • 强引用
    • 普通变量赋值即为强引用,如 A a = new A();
    • 通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收

image.png

  • 软引用(SoftReference)
    • 例如:SoftReference a = new SoftReference(new A());
    • 如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象
    • 软引用自身需要配合引用队列来释放
    • 典型例子是反射数据

image.png

  • 弱引用(WeakReference)
    • 例如:WeakReference a = new WeakReference(new A());
    • 如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象
    • 弱引用自身需要配合引用队列来释放
    • 典型例子是 ThreadLocalMap 中的 Entry 对象

image.png

  • 虚引用(PhantomReference)
    • 例如: PhantomReference a = new PhantomReference(new A());
    • 必须配合引用队列一起使用,当虚引用引用的对象被回收时,会将虚引用对象入队,由 Reference
    • Handler 线程释放其关联的外部资源
    • 典型例子是 Cleaner 释放 DirectByteBuffer 占用的直接内存

image.png

虚引用事例:

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 {
        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) {
                        // 断开与GC Root对象关系,垃圾回收,将会释放内存
                        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();
        }
    }
}

2.6.面试题:finalize 的理解

将资源释放和清理放在 finalize 方法中非常不好,非常影响性能,严重时甚至会引起 OOM,从 Java9 开始就被标注为 @Deprecated,不建议被使用了

两个重要队列

  • unfinalized 队列
    • 当重写了 finalize 方法的对象,在构造方法调用之时,JVM 都会将其包装成一个 Finalizer 对象,并加入 unfinalized 队列中(静态成员变量、双向链表结构)
  • ReferenceQueue 队列
    • 第二个重要的队列,也是 Finalizer 类中一个静态成员变量,名为 queue(是一个单向链表结构),刚开始它是空的。当狗对象可以被当作垃圾回收时,就会把这些狗对象对应的
      Finalizer 对象加入这个队列

image.png

真正回收时机

  • 即使 Dog 对象没人引用,垃圾回收时也没法立刻回收它,因为 Finalizer 还在引用它嘛,为的是【先别着急回收啊,等我调完 finalize 方法,再回收】
  • 查看 FinalizerThread 线程内的代码,这个线程从 ReferenceQueue 中逐一取出每个 Finalizer 对象,把它们从链表断开,这样没谁能引用到它,以及其对应的狗对象,所以下次 gc 时就可以被回收了

image.png

为什么 finalize 方法非常不好,非常影响性能

  • 非常不好

    • FinalizerThread 是守护线程,代码很有可能没来得及执行完,线程就结束了,造成资源没有正确释放
    • 异常被吞掉这个就太糟了,你甚至不能判断有没有在释放资源时发生错误
  • 影响性能

    • 重写了 finalize 方法的对象在第一次被 gc 时,并不能及时释放它占用的内存,因为要等着 FinalizerThread 调用完 finalize,把它从第一个 unfinalized 队列移除后,第二次 gc 时才能真正释放内存
    • 可以想象 gc 本就因为内存不足引起,finalize 调用又很慢(两个队列的移除操作,都是串行执行的,用来释放连接类的资源也应该不快),不能及时释放内存,对象释放不及时就会逐渐移入老年代,老年代垃圾积累过多就会容易 full gc,full gc 后释放速度如果仍跟不上创建新对象的速度,就会 OOM
  • 面试

    面试造航母,上班拧螺丝。多面试,少加班。

    325 引用 • 1395 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖 • 1 关注
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 2 关注

相关帖子

回帖

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...