JVM 基础

内存模型

JVM 内存模型是什么?

(1)JVM 内存模型共分为5个区:Java虚拟机栈、本地方法栈、堆、程序计数器、方法区(元空间)

(2)各个区各自的作用:

a.程序计数器:它是一块很小的内存空间,主要用来记录当前线程执行的字节码的地址,也就是记录当前线程执行的位置。 是虚拟机中没有规定会OutofMemoryError的区域。

b.Java 虚拟机栈:每个方法执行时会在栈中创建一个栈帧,执行完成被销毁,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。(栈里面存的是地址引用,实际指向的是堆里面的对象)

c.本地方法栈:与Java虚拟机栈类似,用于管理本地方法的调用。

d.方法区(Java8叫元空间):用于存放已被虚拟机加载的类信息,常量,静态变量等全局数据信息

e.堆:Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例 都在这里分配内存;

(3)线程私有、公有

a.线程私有:每个线程在开辟、运行的过程中会单独创建这样的一份内存,有多少个线程可能有多少个内存 Java虚拟机栈、本地方法栈、程序计数器是线程私有的

b.线程全局共享的 堆和方法区

(4)栈虽然方法运行完毕了之后被清空了,但是堆上面的还没有被清空,所以引出了GC(垃圾回收),不能立马删除,因为不知道是否还有其它的也是引用了当前的地址来访问的

强引用、软引用、弱引用、虚引用

  1. 强引用
  • 实现: 最常见的引用,当我们新建一个对象时就创建了一个具有强引用的对象 例如 Object obj = new Object();
  • 回收时机: 只要一个对象具有强引用,垃圾收集器就不会回收它,即使抛出OutOfMemoryError 也不会回收
  • 用途: 绝大多数普通对象的引用
  1. 软引用
  • 实现: java.lang.ref.SoftReference
  • 回收时机: 一个对象只具有软引用,当内存不够的时候,就会回收这些对象的内存,回收后内存依然不够就会抛出 OutOfMemoryError
  • 用途: 实现内存敏感的缓存(如图片缓存、大量数据缓存),既能利用缓存提升性能,又能在内存紧张时自动释放。
Java 复制代码
SoftReference<Object> softRef = new SoftReference<>(new Object());
Object obj = softRef.get(); // 可能返回 null(若已被回收)
  1. 弱引用
  • 实现: java.lang.ref.WeakReference
  • 回收时机: 比软引用具有更短的生命周期,在垃圾回收时,垃圾收集器一旦发现只具有弱引用的对象,不管当前内存是否足够,都会回收它的内存。
  • 用途: 弱引用通常用于实现对象缓存,但不希望缓存的对象影响垃圾回收的情况。
Java 复制代码
WeakReference<Object> weakRef = new WeakReference<>(new Object());
Object obj = weakRef.get(); // 下次 GC 后极可能返回 null
  1. 虚引用
  • 实现: java.lang.ref.PhantomReference
  • 回收时机: 是最弱的引用类型,如果一个对象仅持有虚引用,这就和没有任何引用一样,在任何时候都可能被垃圾收集器回收。
    但在对象被回收前,虚引用会被放入一个队列之中,供程序员进行处理。
  • 用途: 虚引用主要用于跟踪对象被垃圾回收的时机,进行一些必要的清理或记录
Java 复制代码
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
// phantomRef.get() 永远为 null
// 当对象被回收后,phantomRef 会被加入 queue,可以从中取出并执行清理

可能追问的问题

  1. 什么是引用队列(ReferenceQueue)?

    • 软、弱、虚引用可以关联一个 ReferenceQueue。当引用的对象被回收后,引用本身会被 JVM 添加到该队列,允许我们执行后续清理(例如从缓存中移除无效的键值对)。

类加载机制

JVM 类加载的生命周期包括加载、连接、初始化、使用和卸载,其中连接又分为验证、准备和解析。

加载阶段

  • 类加载器根据类的全限定名获取字节码加载到内存
  • 将其转换为 JVM 方法区中的运行时数据结构
  • 并在堆中生成对应的 Class 对象。

验证阶段检查 Class 文件格式、元数据和字节码是否合法。防止恶意或者错误的字节码危害 JVM。

准备阶段 为 static 类变量分配内存并赋默认值,例如 static int age = 18,准备阶段通常先赋值为 0,真正赋值为 18 要等到初始化阶段。编译期可确定的 static final 常量比较特殊,可能直接赋常量值。

解析阶段把常量池中的符号引用转换成直接引用。

初始化阶段

  • 执行编译器生成的 类构造器<clinit> 方法,主要包括静态变量的显式赋值和静态代码块。
    (类构造器是构造类信息的,不是构造该类对象的构造器,它会按顺序收集类中所有类变量的赋值动作,和静态代码块中的语句,然后合并产生)
  • 初始化子类前,JVM会先初始化父类,而且同一个类在一个类加载器下通常只初始化一次。
  • 虚拟机会保证一个类的<clinit> 方法在多线程环境中被正确加锁和同步(非重点)

例如执行 new Child() 时,JVM会先完成 Child 的加载、验证、准备和解析,然后先初始化 Parent,再初始化 Child,最后才创建对象并调用 Child 的构造方法。

什么时候会发生类初始化(加载、链接、初始化)

类的主动引用(一定会发生类的加载、链接、初始化)
  1. 虚拟机启动时,会先初始化main方法所在的类
  2. new 对象
  3. 调用类的静态成员(除了final常量)和静态方法
  4. 用反射
  5. 初始化子类时,父类会先被加载
类的被动引用(可能会加载,但不会触发初始化)
  1. 访问 final 常量
java 复制代码
System.out.println(MyClass.CONST);

如果是:

java 复制代码
static final int CONST = 10;

不会触发类初始化(可能连加载都不会发生)

  1. 通过数组定义类引用,不会触发此类的初始化
java 复制代码
MyClass[] arr = new MyClass[10];

只会加载 [LMyClass; 数组类,不会初始化 MyClass

  1. 引用类但不使用
java 复制代码
MyClass m;

不会触发加载


final 常量什么时候初始化

编译器常量

满足以下条件的 static final 变量:

  • 基本类型 / String
  • 直接赋值常量表达式
  • 不需要运行期计算(调用方法、new对象 就是运行期计算)
java 复制代码
static final int A = 10;
static final String S = "hello";

在编译期就确定值

运行时不参与初始化

运行期final常量

不是编译期常量,例如

java 复制代码
static final int A = new Random().nextInt();
static final Integer B = Integer.valueOf(10);
static final String S = new String("hello");
  • 准备阶段赋予默认值
  • 类初始化阶段(Initialization)执行 <clinit> 方法时初始化

什么是Java里的垃圾回收?如何触发垃圾回收?

垃圾回收(Garbage Collection, GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。垃圾回收可以通过多种方式触发,具体如下:

  • 内存不足时:当JVM检测到堆内存不足,无法为新的对象分配内存时,会自动触发垃圾回收。
  • 手动请求 :虽然垃圾回收是自动的,开发者可以通过调用 System.gc()Runtime.getRuntime().gc() 建议 JVM 进行垃圾回收。不过这只是一个建议,并不能保证立即执行。
  • JVM参数 :启动 Java 应用时可以通过 JVM 参数来调整垃圾回收的行为,比如:-Xmx(最大堆大小)、-Xms(初始堆大小)等。
  • 对象数量或内存使用达到阈值:垃圾收集器内部实现了一些策略,以监控对象的创建和内存使用,达到某个阈值时触发垃圾回收。

内存泄漏: 内存泄漏是指在运行过程中不再使用的对象仍然被引用,而无法被垃圾收集器回收,从而导致内存组件减少的现象。虽然在Java中,垃圾回收机制会自动回收不再使用的对象,但如果有对象仍被不再使用的引用持有,垃圾收集器无法回收这些内存,最终可能导致程序的内存使用不断增加。

内存溢出: 虽然在Java中,垃圾回收机制会自动回收不再使用的对象,但如果有对象仍被不再使用的引用持有,垃圾收集器无法回收这些内存,最终可能导致程序的内存使用不断增加。

判断垃圾的方法有哪些?

在 Java 中,判断对象是否为垃圾(即不再被使用,可以被垃圾回收器回收)主要依据两种主流的垃圾回收算法来实现:引用计数法和可达性分析算法

引用计数法(Reference Counting)
  • 原理:为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加 1;当引用失效时,计数器减 1。当计数器为 0 时,表示对象不再被任何变量引用,可以被回收。
  • 缺点:不能解决循环引用的问题,即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为 0,导致对象无法被回收。
可达性分析算法(Reachability Analysis)

Java 虚拟机主要采用此算法来判断对象是否为垃圾。

  • 原理:从一组称为 GC Roots(垃圾收集根)的对象出发,向下追溯它们引用的对象,以及这些对象引用的其他对象,以此类推。如果一个对象到 GC Roots 没有任何引用链相连(即从 GC Roots 到这个对象不可达),那么这个对象就被认为是不可达的,可以被回收。GC Roots 对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、本地方法栈中 JNI(Java Native Interface)引用的对象、活跃线程的引用等。

垃圾回收算法是什么,是为了解决了什么问题?

JVM有垃圾回收机制的原因是为了解决内存管理的问题。在传统的编程语言中,开发人员需要手动分配和释放内存,这可能导致内存泄漏、内存溢出等问题。而Java作为一种高级语言,旨在提供更简单、更安全的编程环境,因此引入了垃圾回收机制来自动管理内存。

垃圾回收机制的主要目标是自动检测和回收不再使用的对象,从而释放它们所占用的内存空间。这样可以避免内存泄漏(一些对象被分配了内存却无法被释放,导致内存资源的浪费)。同时,垃圾回收机制还可以防止内存溢出(即程序需要的内存超过了可用内存的情况)。

通过垃圾回收机制,JVM可以在程序运行时自动识别和清理不再使用的对象,使得开发人员无需手动管理内存。这样可以提高开发效率、减少错误,并且使程序更加可靠和稳定。

垃圾回收算法有哪些?

  • 标记-清除算法:标记-清除算法分为"标记"和"清除"两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记-清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。
  • 复制算法:为了解决碎片空间的问题,出现了"复制算法"。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足。
  • 标记-整理算法:复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了"标记-整理算法"。标记-整理算法的"标记"过程与"标记-清除算法"的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。
  • 分代回收算法 :分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对象还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。

有哪些垃圾回收器

1. 新生代垃圾收集器
  • Serial 收集器(复制算法):是新生代单线程收集器,优点是简单高效,算是最基本、发展历史最悠久的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。
  • ParNew 收集器(复制算法):是新生代并行收集器,其实就是 Serial 收集器的多线程版本。
  • Parallel Scavenge 收集器:是新生代并行收集器,追求高吞吐量,高效利用 CPU。

2. 老年代垃圾收集器
  • Serial Old:是Serial收集器的老年代版本,它同样是一个单线程(串行)收集器,使用标记整理算法。
  • Parallel Old:是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理"算法。
  • CMS 收集器:是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

CMS收集器是基于"标记-清除"算法实现的,它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为4个步骤:

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

3. 新生代和老年代垃圾收集器
  • G1收集器(标记整理算法):Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于"标记-整理"算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代

G1收集器的优势

  1. 独特的分代垃圾回收器,分代GC:分代收集器,同时兼顾年轻代和老年代
  2. 使用分区算法,不要求eden、年轻代或老年代的空间都连续
  3. 并行性:回收期间,可由多个线程同时工作,有效利用多核cpu资源
  4. 空间整理:回收过程中,会进行适当对象移动,减少空间碎片
  5. 可预见性:G1可选取部分区域进行回收,可以缩小回收范围,减少全局停顿

G1收集器的阶段分以下几个步骤

  1. 初始标记(它标记了从GC Root开始直接可达的对象)
  2. 并发标记(从GC Roots开始对堆中对象进行可达性分析,找出存活对象)
  3. 最终标记(标记那些在并发标记阶段发生变化的对象,将被回收)
  4. 筛选回收(首先对各个Region的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)
相关推荐
天选之子1231 天前
DuckDB学习
jvm·学习
春卷同学1 天前
032-关系型数据库在记账应用中的建模与查询优化
java·jvm·数据库
orient2 天前
JVM 调优实战:从监控告警到参数配置,一套完整方法论
jvm
orient2 天前
大白话 JVM:对象到底存在哪?GC 又是怎么运行的?
jvm
炸薯条!2 天前
从零开始学C++ (内存管理)
java·jvm·c++
凤山老林2 天前
从“无法创建本地线程“异常说起:深度解析线程栈与JVM线程优化指南
java·开发语言·jvm
csdn2015_3 天前
JVM 常用命令
jvm
fīɡЙtīиɡ ℡3 天前
JVM从入门到精通
jvm
wear工程师3 天前
volatile 这题别只答「可见性」:面试官真正想听的是这条 happens-before
java·jvm