JVM 精华

参考 https://www.xiaolincoding.com/interview/jvm.html

文章目录

内存模型

JVM 内存模型类似 c的程序地址空间

(JDK 7 对常量池进行了部分调整,将部分内容从元空间移动到堆中)

  • 程序计数器:存储当前线程正在执行的 Java 方法的 JVM 指令地址。(如果线程执行的是 Native 方法,计数器值为 null。是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域,生命周期与线程相同。)
  • Java 虚拟机栈:每个线程都有自己独立的 Java 虚拟机栈,每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。存储的往往是堆上对象的引用(生命周期与线程相同)
  • 本地方法栈:主要为虚拟机使用到的 Native 方法服务
  • Java 堆:在虚拟机启动时创建,用于存放对象实例。被所有线程共享。
  • 元空间(方法区,永久代,类似代码段):使用本地内存(而不是堆中,解决了内存溢出问题)。用于存储已被虚拟机加载的类信息、常量、静态变量等数据。
  • 运行时常量池(已初始化数据):存放编译期生成的各种字面量和符号引用
  • 直接内存:不属于 JVM 运行时数据区的一部分,通过 NIO 类引入,是一种堆外内存,可以显著提高 I/O 性能。

堆分为哪几部分

  • 新生代(Young Generation)
    新生代分为 Eden SpaceSurvivor Space 0/1。大多数新创建的对象首先存放在Eden区,满时触发Minor GC(新生代垃圾回收),存活下来的对象会被移动到其中一个Survivor空间。两个Survivor区域轮流充当对象的中转站,帮助区分短暂存活的对象和长期存活的对象。
  • 老年代(Old Generation/Tenured Generation)
    存放过一次或多次Minor GC仍存活的对象会被移动到老年代。老年代中的对象生命周期较长,因此Major GC(也称为Full GC,涉及老年代的垃圾回收)发生的频率相对较低,但其执行时间通常比Minor GC长。老年代的空间通常比新生代大,以存储更多的长期存活对象。(大对象可能直接分配到老年代,以避免因频繁的年轻代晋升而导致的内存碎片化问题。)

直接调用 System.gc() 或 Runtime.getRuntime().gc() 方法时,JVM会尝试执行Full GC (不保证立即执行)

引用类型

  • 强引用:如 a = new A() 这种普遍的赋值。永远不会被GC回收
  • 软引用 SoftReference : 系统在发生内存溢出前会对这类引用的对象进行回收。
  • 弱引用 WeakReference:弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。
  • 虚引用 PhantomReference:必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存。

假设我们有一个缓存系统,我们使用弱引用来维护缓存中的对象:

(对需要的弱引用 返回强引用,外部使用时就不会被回收了!)

java 复制代码
import Java.lang.ref.WeakReference;
import Java.util.HashMap;
import Java.util.Map;

public class CacheExample {

    private Map<String, WeakReference<MyHeavyObject>> cache = new HashMap<>();

    public MyHeavyObject get(String key) {
        WeakReference<MyHeavyObject> ref = cache.get(key);
        if (ref != null) {
            return ref.get();
        } else {
            MyHeavyObject obj = new MyHeavyObject();
            cache.put(key, new WeakReference<>(obj));
            return obj;
        }
    }

    // 假设MyHeavyObject是一个占用大量内存的对象
    private static class MyHeavyObject {
        private byte[] largeData = new byte[1024 * 1024 * 10]; // 10MB data
    }
}

类初始化和类加载

  • 类加载检查:JVM 运行是按需加载的,当程序首次使用某个类(new等),会先检查常量池 能否定位对应目标类的符号引用,若未加载,类加载器会从磁盘读取对应的 .class 文件解析。
  • 分配内存
  • 解析阶段: 会把符号引用转换为直接引用
    • 符号引用:编译时生成的逻辑描述,例如com/example/MyClass,用于动态链接阶段。
    • 直接引用:解析阶段将符号引用转换为具体内存地址(如方法区中类元数据的指针、方法表的偏移量等)。
  • 初始化零值:分配到的内存空间都初始化为零值(不包括对象头)
  • 对象头等进行必要设置:例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象头中。
  • 执行 init 方法:执行类的构造函数
  • 使用
  • 卸载:1.该类所有的实例都已经被回收 2.加载该类的类加载器ClassLoader 已经被回收。3.类对应的Java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

类加载器

这些类加载器之间的关系形成了双亲委派模型 ,其核心思想是当一个类加载器收到类加载的请求时,首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(除非父加载器找不到相应类型),每一层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。 ------ 保证不会重复加载

  • 保证安全性:Java核心库被启动类加载器加载,而启动类加载器只加载信任的类路径中的类,这样可以防止不可信的类假冒核心类,增强了系统的安全性。(例如,恶意代码无法自定义一个Java.lang.System类并加载到JVM中,因为这个请求会被委托给启动类加载器,而启动类加载器只会加载标准的Java库中的类。)

垃圾回收 GC

Garbage Collection

回收堆和方法区的无用内存。

通过new关键字创建的对象由 GC 回收,但部分资源需要手动关闭,例如:

  • 文件 / 网络连接:FileInputStream、Socket等,需调用close()方法。
  • 数据库连接:Connection、Statement等,需调用close()或使用try-with-resources。
  • 系统资源:如java.nio.channels.FileChannel需手动关闭。

这些资源通常封装在对象中,但 GC 无法直接释放它们持有的操作系统资源(如文件句柄、网络套接字)。

判断垃圾的方法:

引用计数法

Reference Counting

原理: 为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。当计数器为0时,表示对象不再被任何变量引用,可以被回收。

有循环引用问题

可达性分析算法

Reachability Analysis

原理: 从一组称为GC Roots(垃圾收集根)的对象出发,向下追溯它们引用的对象,以及这些对象引用的其他对象。如果一个对象到GC Roots没有任何引用链相连(即从GC Roots到这个对象不可达),那么这个对象就被认为是不可达的,可以被回收。

垃圾回收算法:

  • 标记-清除算法
    首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。【慢】【产生内存碎片】
  • 复制算法
    将内存分成两块,每次申请内存时都使用其中的一块。当内存不够时,将这一块内存中所有存活的复制到另一块上,然后将然后再把已使用的内存整个清理掉。【申请内存只能使用一半的内存空间】
  • 标记-整理算法
    标记后不直接清理,而是先把所有存活对象都移动到内存的一端,移动结束后直接清理掉剩余部分。
    分代回收算法
    将内存划分成了新生代和老年代。依据对象经历过的 GC 次数,15次后就会进入老年代。
    (后两个算法通常会结合使用)
stop the world

STW

标记-复制算法应用在 CMS新生代(ParNew是CMS默认的新生代垃圾回收器)和 G1垃圾回收器中。

  • 标记阶段,即从GC Roots集合开始,标记活跃对象;
  • 转移阶段,即把活跃对象复制到新的内存地址上;
  • 重定位阶段,因为转移导致对象的地址发生了变化,在重定位阶段所有指向对象旧地址的指针都要调整到对象新的地址上。

以 G1 为例:

标记阶段停顿分析

  • 初始标记阶段:初始标记阶段是指从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段【耗时非常短】。
  • 并发标记阶段:并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析,找出存活对象。【并发】
  • 再标记阶段:重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。

清理阶段停顿分析

  • 清理阶段清点出有存活对象的分区和没有存活对象的分区。该阶段是STW的。(该阶段不会清理垃圾对象,也不会执行存活对象的复制)内存分区数量少,【耗时也较短】。

复制阶段停顿分析

  • 复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是STW的。内存分配耗时短,但是复制耗时较长。

垃圾回收器:

  • Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
  • Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
CMS 与 G1
  • CMS收集器 (Concurrent Mark-Sweep) (标记-清除算法):老年代并行收集器,以获取【最短回收停顿时间】为目标的收集器。具有高并发、低停顿的特点,追求最短GC回收停顿时间。
    • 低延迟需求
    • 针对老年代的垃圾回收
    • 容易出现内存碎片,可能需要定期进行Full GC来压缩内存空间。
  • G1收集器 (Garbage First) (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器。G1回收的范围是整个Java堆(包括新生代,老年代),而其他大部分收集器回收的范围仅限于新生代或老年代。
    • 适用于管理大内存堆:能够有效处理数GB以上的堆内存。
    • 比较平衡的性能
    • 对内存碎片敏感,降低了碎片化对性能的影响。

区别:

  • CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
  • STW时间:G1收集器可预测垃圾回收 (opens new window)的停顿时间,以避免应用雪崩现象。
  • CMS 会产生内存碎片
  • 回收过程不同:
  • CMS会产生浮动垃圾:并发清除时,垃圾回收线程和用户线程同时工作会产生浮动垃圾,意味着CMS垃圾回收器必须预留一部分内存空间用于存放浮动垃圾。浮动垃圾过多时会退化为 serial old。
    而 G1没有浮动垃圾,G1的筛选回收是多个垃圾回收线程并行gc的。清理时用户线程也会同时产生一部分可回收对象,但是这部分只能在下次执行清理时再回收。预留内存不足就会出现'Concurrent Mode Failure',一旦出现此错误时便会切换到 serial old。
相关推荐
程序猿20231 小时前
MAT(memory analyzer tool)主要功能
jvm
期待のcode4 小时前
Java虚拟机的非堆内存
java·开发语言·jvm
jmxwzy8 小时前
JVM(java虚拟机)
jvm
Maỿbe8 小时前
JVM中的类加载&&Minor GC与Full GC
jvm
人道领域9 小时前
【零基础学java】(等待唤醒机制,线程池补充)
java·开发语言·jvm
小突突突9 小时前
浅谈JVM
jvm
饺子大魔王的男人11 小时前
远程调试总碰壁?局域网成 “绊脚石”?Remote JVM Debug与cpolar的合作让效率飙升
网络·jvm
天“码”行空21 小时前
java面向对象的三大特性之一多态
java·开发语言·jvm
独自破碎E1 天前
JVM的内存区域是怎么划分的?
jvm
期待のcode1 天前
认识Java虚拟机
java·开发语言·jvm