JVM组成
JVM
JVM 就是 Java 程序的运行环境,它通过 类加载、字节码执行、内存管理、GC、线程调度 等机制,让 Java 实现了 跨平台、自动内存管理和高效执行。
它是一个抽象的计算机,能执行以 字节码(.class 文件) 为单位的指令。
好处:
-
一次编写,到处运行
-
自动内存管理,垃圾回收机制
JVM由那些部分组成,运行流程是什么?
JVM 主要由 类加载器、运行时数据区、执行引擎、垃圾回收器、本地接口 组成。
运行流程是:类加载器加载字节码 → 执行引擎解释/编译执行 → 内存区域分配对象与方法栈帧 → GC 自动回收。
从图中可以看出 JVM 的主要组成部分
-
ClassLoader(类加载器)
-
Runtime Data Area(运行时数据区,内存分区)
-
Execution Engine(执行引擎)
-
Native Method Library(本地库接口)
运行流程:
(1)类加载器(ClassLoader)把Java代码转换为字节码
(2)运行时数据区(Runtime Data Area)把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是有执行引擎运行
(3)执行引擎(Execution Engine)将字节码翻译为底层系统指令,再交由CPU执行去执行,此时需要调用其他语言的本地库接口(Native Method Library)来实现整个程序的功能。
更具体:
JVM 的运行流程
一个 Java 程序从
.java
文件到运行的完整流程:
编译阶段:
.java
→ 编译器(javac) →.class
字节码文件。类加载阶段:JVM 的类加载器加载
.class
文件到内存,放入 方法区。执行阶段:节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是有执行引擎运行
JVM 创建主线程,分配 虚拟机栈 和 PC 寄存器。
执行引擎从 方法区 取字节码指令,解释或 JIT 编译成机器码执行。
运行时内存分配
程序运行过程中需要创建对象,存入 堆内存。
方法调用时创建 栈帧,保存局部变量和操作数栈。
垃圾回收
GC 检测堆内存中的对象,回收不可达对象。
JVM内存分配
JVM 内存分配分为 线程私有区(随线程销毁而释放) 和 线程共享区(需 GC 管理)。
-
对象实例分配在堆,
-
引用存放在栈;
-
类元信息在方法区;
-
线程运行状态保存在计数器和栈。
-
GC 主要负责堆和方法区的垃圾回收。
什么是程序计数器?
程序计数器(PC 寄存器)
-
保存当前线程执行的字节码行号,线程切换时能恢复执行位置。
-
每个线程独享(私有),不存在内存回收问题。
javap -verbose xx.class 打印堆栈大小,局部变量的数量和方法的参数。
程序计数器的作用
JVM对于多线程是通过线程轮流切换并且分配线程执行时间。
在任何的一个时间点上,一个处理器只会处理执行一个线程,如果当前被执行的这个线程它所分配的执行时间用完了,就会挂起。处理器会切换到另外的一个线程上来进行执行。并且这个线程的执行时间用完了,接着处理器就会又来执行被挂起的这个线程。
那么现在有一个问题就是,当前处理器如何能够知道,对于这个被挂起的线程,它上一次执行到了哪里?
那么这时就需要从程序计数器中来回去到当前的这个线程他上一次执行的行号,然后接着继续向下执行。
程序计数器是JVM规范中唯一一个不会出现内存溢出(OOM)的内存空间,所以这个空间也不会进行GC。
虚拟机栈(VM Stack)
每个线程运行时所需要的内存,称为虚拟机栈,先进后出
虚拟机栈包括 局部变量表、操作数栈、方法出口 等。方法调用时分配,方法结束后回收。
若栈深度过大可能抛 StackOverflowError。
-
每个线程运行时所需要的内存,称为虚拟机栈,先进后出
-
每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存
-
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
常见问题:
(1)垃圾回收是否涉及栈内存?
-
垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放
(2)栈内存分配越大越好吗?
-
未必,默认的栈内存通常为1024k
-
栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半
(4)方法内的局部变量是否线程安全?
-
如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
-
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
-
比如以下代码:
栈内存溢出情况
-
栈帧过多导致栈内存溢出,典型问题:递归调用
-
栈帧过大导致栈内存溢出
你能给我详细的介绍Java堆吗?
堆(Heap)存放 对象实例和数组,是 GC 管理的主要区域。
-
堆进一步划分为:
-
新生代(Young Generation):Eden + Survivor0 + Survivor1,用于存放新对象。
-
老年代(Old Generation):存放生命周期长的对象。
-
-
JDK8 之后,堆外新增 Metaspace(元空间)。
堆属于线程共享的区域:主要用来保存对象实例,数组等
-
当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
-
年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到老年代区间。
-
老年代主要保存生命周期长的对象,一般是一些老的对象
-
元空间保存的类信息、静态变量、常量、编译后的代码
为了避免方法区出现OOM,所以在java8中将堆上的方法区【永久代】给移动到了本地内存上,重新开辟了一块空间,叫做元空间。那么现在就可以避免掉OOM的出现了。
元空间(MetaSpace)介绍
元空间的本质和永久代(方法区在 JDK7 之前叫 永久代(PermGen))类似,都是对 JVM 规范中方法区的实现。
不过元空间与永久代之间最大的区别在于:
-
元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
在 HotSpot JVM 中,永久代( ≈ 方法区)中用于存放类和方法的元数据以及常量池,比如Class 和 Method。每当一个类初次被加载的时候,它的元数据都会放到永久代中。
永久代是有大小限制的,因此如果加载的类太多,很有可能导致永久代内存溢出,即OutOfMemoryError,为此不得不对虚拟机做调优。
那么,Java 8 中 PermGen 为什么被移出 HotSpot JVM 了?
官网给出了解释:http://openjdk.java.net/jeps/122
This is part of the JRockit and Hotspot convergence effort. JRockit customers do not need to configure the permanent generation (since JRockit does not have a permanent generation) and are accustomed to not configuring the permanent generation.
移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
1)由于 PermGen 内存经常会溢出,引发OutOfMemoryError,因此 JVM 的开发者希望这一块内存可以更灵活地被管理,不要再经常出现这样的 OOM。
2)移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代。
准确来说,Perm 区中的字符串常量池被移到了堆内存中是在 Java7 之后,Java 8 时,PermGen 被元空间代替,其他内容比如类元信息、字段、静态属性、方法、常量等都移动到元空间区。比如 java/lang/Object 类元信息、静态属性 System.out、整型常量等。
能不能解释一下方法区?
概述
方法区(Method Area)是各个线程共享的内存区域,主要存储类的信息、运行时常量池
虚拟机启动的时候创建,关闭虚拟机时释放
如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspace
常量池
可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
可以通过查看字节码结构(类的基本信息、常量池、方法定义)javap -v xx.class
理解
下图,左侧是main方法的指令信息,右侧constant pool 是常量池
main方法按照指令执行的时候,需要到常量池中查表翻译找到具体的类和方法地址去执行
运行时常量池
常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
你听过直接内存吗?
不受 JVM 内存回收管理,是虚拟机的系统内存,常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受 JVM 内存回收管理
NIO 操作:Java NIO 中的
ByteBuffer.allocateDirect(int capacity)
方法会分配直接内存,用于高效的 IO 操作(如FileChannel
读写文件、SocketChannel
网络通信)。
举例:
需求,在本地电脑中的一个较大的文件(超过100m)从一个磁盘挪到另外一个磁盘
使用传统的IO的时间要比NIO操作的时间长了很多了,也就说NIO的读性能更好。
这个是跟我们的JVM的直接内存是有一定关系,如下图,是传统阻塞IO的数据传输流程
下图是NIO传输数据的流程,在这个里面主要使用到了一个直接内存,不需要在堆中开辟空间进行数据的拷贝,jvm可以直接操作直接内存,从而使数据读写传输更快。
堆栈的区别是什么?
1、栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的。
2、堆会GC垃圾回收,而栈不会。
3、栈内存是线程私有的,而堆内存是线程共有的。
4,、两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常。
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.lang.OutOfMemoryError。
类加载器
什么是类加载器,类加载器有哪些?(高频)
类加载器的作用:负载将的class文件加载到java虚拟机中,并为之创建一个Class对象
类加载器根据各自加载范围的不同,划分为四种类加载器:
-
启动类加载器(BootStrap ClassLoader):
-
该类并不继承ClassLoader类,其是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。
-
-
扩展类加载器(ExtClassLoader):
-
该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。
-
-
应用类加载器(AppClassLoader):
-
该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。
-
-
自定义类加载器:
-
开发者自定义类继承ClassLoader,实现自定义类加载规则。
-
Java的双亲委托机制是什么?(高频)
我们的应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,还可以加入自定义的类加载器。这些类加 载器之间的层次关系一般会如下图所示:
上图所展示的类加载器之间的这种层次关系,就称之为类加载器的双亲委派模型。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器(逻辑继承)。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传说到顶层的启动类加载器中,只有当父类加载器返回自己无法完成这个加载请求(它的搜索 返回中没有找到所需的类)时,子类加载器才会尝试自己去加载
说一下类装载的执行过程?
JVM 类加载过程分为 加载 → 链接(验证、准备、解析) → 初始化。加载阶段读取字节码并生成 Class 对象,链接阶段完成验证安全性、分配静态变量内存和符号引用解析,最后初始化阶段执行 <clinit>()
给静态变量赋值并运行静态代码块。
类加载的执行过程
加载(Loading)
通过类加载器读取
.class
文件(可能来自磁盘、网络等)。把字节码加载到 JVM 内存,并在 方法区(元空间 Metaspace) 生成类的运行时数据结构。
在堆中生成一个 Class 对象,作为访问方法区中类元数据的入口。
链接(Linking)
链接分为三个小步骤:
验证(Verification):确保字节码符合 JVM 规范,保证不会危害虚拟机安全。
比如校验文件格式、元数据、字节码指令合法性。
准备(Preparation):为类的 静态变量分配内存 并设置 默认值(不是初始值)。
例如:
public static int a = 10;
在这一步只会分配空间并赋 0,真正赋值 10 在初始化阶段完成。解析(Resolution):把常量池中的 符号引用(Symbolic Reference) 转换为 直接引用(Direct Reference)。
例如方法、字段的引用地址解析成真正的内存地址。
初始化(Initialization)
执行类构造器
<clinit>()
方法,完成静态变量的赋初始值 & 执行静态代码块。多线程下,JVM 会保证类初始化的线程安全
垃圾收回
简述Java垃圾回收机制?(GC是什么?为什么要GC)
GC 是 JVM 的自动内存管理机制,通过 可达性分析 判断对象是否存活。
常用的回收算法有 标记-清除、复制、标记-整理、分代收集。它的作用是自动释放无用对象内存,避免内存泄漏,提高系统稳定性和开发效率。
有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成。
在进行垃圾回收时,不同的对象引用类型,GC会采用不同的回收时机。
当然,除了Java语言,C#、Python等语言也都有自动的垃圾回收机制。
如何判断一个对象是否为垃圾?(高频)
简单一句就是:如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法
引用计数法
堆中每个对象实例都有一个引用计数。
一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收
但是当对象间出现了循环引用的话,则引用计数法就会失效
优点:
-
实时性较高,无需等到内存不够的时候,才开始回收,运行时根据对象的计数器是否为0,就可以直接回收。
-
在垃圾回收过程中,应用无需挂起。如果申请内存时,内存不足,则立刻报OOM错误。
-
区域性,更新对象的计数器时,只是影响到该对象,不会扫描全部对象。
缺点:
-
浪费CPU资源,即使内存够用,仍然在运行时进行计数器的统计。
-
无法解决循环引用问题,会引发内存泄露。(最大的缺点)
可达性分析算法
现在的虚拟机采用的都是通过可达性分析算法来确定哪些内容是垃圾。
会存在一个根节点【GC Roots】,引出它下面指向的下一个节点,再以下一个节点节点开始找出它下面的节点,依次往下类推。直到所有的节点全部遍历完毕。
根对象是那些肯定不能当做垃圾回收的对象,就可以当做根对象:局部变量,静态方法,静态变量,类信息
核心是:判断某对象是否与根对象有直接或间接的引用,如果没有被引用,则可以当做垃圾回收
-
以 GC Roots 为起点,向下搜索引用链(Reference Chain)。
-
如果一个对象能通过引用链与 GC Roots 相连,则判定为“可达”。
-
如果对象不可达,第一次会被标记为“可回收对象”。
-
JVM 可能会执行一次
finalize()
方法给予“自救”机会。-
如果在
finalize()
里重新建立引用链,对象会“复活”。 -
否则,下一次 GC 会真正清理。
-
X,Y这两个节点是可回收的,但是并不会马上的被回收!! 对象中存在一个方法【finalize】。当对象被标记为可回收后,当发生GC时,
首先会判断这个对象是否执行了finalize方法,
如果这个方法还没有被执行的话,那么就会先来执行这个方法,接着在这个方法执行中,可以设置当前这个对象与GC ROOTS产生关联,
那么这个方法执行完成之后,GC会再次判断对象是否可达,如果仍然不可达,则会进行回收,如果可达了,则不会进行回收。
finalize方法对于每一个对象来说,只会执行一次。如果第一次执行这个方法的时候,设置了当前对象与RC ROOTS关联,那么这一次不会进行回收。 那么等到这个对象第二次被标记为可回收时,那么该对象的finalize方法就不会再次执行了。
JVM 垃圾回收算法有哪些?
JVM 常见的垃圾回收算法有:标记-清除、复制、标记-整理、分代收集。
新生代常用复制算法(因为大部分对象很快死亡),
老年代常用标记-清除/整理(避免内存碎片),
而实际 JVM 垃圾回收器普遍采用 分代收集 策略。
标记-清除算法
标记清除算法,是将垃圾回收分为2个阶段,分别是标记和清除。
1.根据可达性分析算法得出的垃圾进行标记
2.对这些标记为可回收的内容进行垃圾回收
可以看到,标记清除算法解决了引用计数算法中的循环引用的问题,没有从root节点引用的对象都会被回收。
优点:实现简单;速度快。
同样,标记清除算法也是有缺点的:
-
效率较低,标记和清除两个动作都需要遍历所有的对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的。
-
(重要)通过标记清除算法清理出来的内存,碎片化较为严重,因为被回收的对象可能存在于内存的各个角落,所以清理出来的内存是不连贯的。
标记-整理算法
标记压缩算法是在标记清除算法的基础之上,做了优化改进的算法。和标记清除算法一样,也是从根节点开始,对对象的引用进行标记,在清理阶段,并不是简单的直接清理可回收对象,而是将存活对象都向内存另一端移动,然后清理边界以外的垃圾,从而解决了碎片化的问题。
-
流程:
-
标记存活对象。
-
将存活对象压缩到一端,清理边界以外的空间。
-
-
优点:避免了内存碎片。
-
缺点:移动对象成本高,效率低于复制算法。
-
应用场景:老年代(存活对象多,不适合复制算法)。
复制算法
将有用的复制走,将剩下的全部删掉
复制算法的核心就是,将原有的内存空间一分为二(From 区 和 To 区),每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收。
如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。
-
优点:没有内存碎片,分配效率高(指针碰撞)。在垃圾对象多的情况下,效率较高
-
缺点:浪费一半内存。
-
应用场景:新生代(大部分对象很快死亡,适合复制算法)
分代收集—— 实际应用最广
-
思想:根据对象生命周期长短划分区域,采用不同算法:
-
新生代:对象存活率低 → 采用 复制算法。
-
老年代:对象存活率高 → 采用 标记-清除 或 标记-整理。
-
-
优点:结合多种算法优势,提高整体效率。
-
现代 JVM(HotSpot) 默认采用分代收集。
分代回收器有两个分区:老生代和新生代
-
新生代默认的空间占比总空间的 1/3,
-
老生代的默认占比是2/3。
新生代使用的是复制算法,新生代里有3个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,
它的执行流程如下:
当年轻代中的Eden区分配满的时候,就会触发年轻代的GC(Minor GC)。具体过程如下:
1、在Eden区执行了第一次GC之后,存活的对象会被移动到其中一个Survivor分区(以下简称to)
2、From区中的对象根据对象的年龄值决定去向,达到阈值15移动到老年代,没有达到复制到to区域(复制算 法)
3、在把Eden和to区中的对象清空掉
JVM的永久代中会发生垃圾回收么?
永久代会触发垃圾回收的,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。 注:
Java 8 中已经移除了永久代,新加了一个叫做元数据区(Metaspace)的内存区。
说一下 JVM 有哪些垃圾回收器?
在JVM中,实现了多种垃圾收集器,包括:
-
串行垃圾收集器
-
并行垃圾收集器
-
CMS(并发)垃圾收集器
-
G1垃圾收集器
🔹 新生代收集器
Serial 收集器
单线程,回收和用户线程互斥(STW,Stop The World),适合单 CPU、小内存环境。
ParNew 收集器
Serial 的多线程版本。
常和 CMS(老年代收集器)搭配使用。
Parallel Scavenge 收集器(吞吐量优先)
多线程,追求高吞吐量(用户代码运行时间 / 总时间)。
适合后台计算类应用。
🔹 老年代收集器
Serial Old 收集器
Serial 的老年代版本,单线程,标记-整理算法。
主要作为 CMS 的备用方案。
Parallel Old 收集器
Parallel Scavenge 的老年代版本,多线程,标记-整理算法。
适合高吞吐量场景。
CMS(Concurrent Mark-Sweep)收集器
以最短停顿时间为目标。
并发标记、并发清除,减少 STW 时间。
缺点:会产生内存碎片,回收过程耗 CPU。
串行垃圾收集器
Serial和Serial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑
-
Serial 作用于新生代,采用复制算法
-
Serial Old 作用于老年代,采用标记-整理算法
垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
并行垃圾收集器
Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器
-
Parallel New作用于新生代,采用复制算法
-
Parallel Old作用于老年代,采用标记-整理算法
垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
CMS(并发)垃圾收集器
CMS全称 Concurrent Mark Sweep),是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行。
详细聊一下G1垃圾回收器
G1 GC 是 JDK9 之后的默认垃圾收集器,把堆划分为多个 Region,优先回收垃圾最多的 Region,通过 并发标记 + 混合回收 + 复制压缩 来实现 高吞吐 + 可预测低延迟,非常适合大堆和低停顿场景。
概述: G1是一个分代的,并行与并发的"标记-整理"垃圾回收器。
它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。
相比于CMS:
1. G1垃圾回收器使用的是"标记-整理",因此其回收得到的空间是连续的。
2. G1回收器的内存与CMS回收器要求的内存模型有极大的不同。G1将内存划分一个个固定大小的region,每个 region可以是年轻代、老年代的一个。内存的回收是以region作为基本单位的;
1.G1 的设计目标
-
面向 大堆内存(几十 GB) 的场景,追求:
-
高吞吐量(充分利用多核 CPU 并行回收)。
-
低延迟(可预测的停顿时间)。
-
2. 内存结构(Region 化)
-
传统分代(新生代 / 老年代) → G1 把整个堆划分为 若干个等大小的 Region(分区)。
-
每个 Region 可以动态扮演:
-
Eden 区
-
Survivor 区
-
Old 区
-
-
特殊:有 Humongous Region 专门存放大对象(超过一个 Region 一半大小)。
好处:避免了传统分代带来的固定划分,更灵活。
3. G1 的核心机制
-
Mixed GC(混合收集):
-
不再是单纯的新生代 / 老年代 GC,而是根据需要选择部分 Region 回收。
-
-
记忆集(Remembered Set, RSet):
-
记录其他 Region 对本 Region 的引用,避免全堆扫描。
-
-
回收优先级:
-
G1 会根据 Region 的垃圾比例排序,优先回收垃圾最多的 Region,这也是名字 Garbage First 的由来。
-
4. G1 的工作流程
-
初始标记(Initial Mark)
-
标记 GC Roots 直接可达的对象。
-
停顿(STW)。
-
-
并发标记(Concurrent Mark)
-
在整个堆里做可达性分析,标记存活对象。
-
与用户线程并发执行。
-
-
最终标记(Remark)
-
修正并发标记阶段用户线程继续运行导致的标记变动。
-
停顿(STW)。
-
-
筛选回收(Cleanup/Copying)
-
计算每个 Region 的回收价值(垃圾比例)。
-
按优先级选择部分 Region 进行复制回收(存活对象复制到空 Region,原 Region 清空)。
-
5. G1 的优缺点
优点:
-
可预测的停顿时间:用户可指定期望的最大停顿时间(如 200ms)。
-
大内存场景表现优异(几十 GB 以上)。
-
避免内存碎片:采用复制算法回收,天然整理内存。
缺点:
-
实现复杂,对 JVM 内部消耗较大。
-
在小堆或低配置机器上性能不如 Parallel GC。
Java中都有哪些引用类型?(高频)
Java 有 强、软、弱、虚 四种引用:
-
强引用:最普通,GC 不回收。
-
软引用:内存不足时才回收,常用于缓存。
-
弱引用:只要 GC 就回收,常用于 ThreadLocal 等。
-
虚引用:对象回收时得到通知,用于堆外内存管理
强引用
Java中默认声明的就是强引用
最常见的引用方式,比如:
Object obj = new Object();
特点:
-
只要存在强引用,GC 永远不会回收对象。
-
就算内存不足,宁可抛
OutOfMemoryError
也不会回收。
适用场景:日常开发中的普通对象。
软引用
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收
使用 SoftReference
类创建:
SoftReference<Object> softRef = new SoftReference<>(new Object());
特点:
-
在 内存不足时,GC 才会回收软引用的对象。
-
适合实现 内存敏感的缓存(如图片缓存)。
适用场景:缓存系统(Ehcache 就用软引用实现)。
弱引用
使用 WeakReference
类创建:
WeakReference<Object> weakRef = new WeakReference<>(new Object());
特点:
-
只要发生 GC,就会回收弱引用的对象(不管内存是否足够)。
-
回收后
weakRef.get()
返回null
。
适用场景:
-
ThreadLocal 的
ThreadLocalMap
key 就是弱引用,防止内存泄漏。
虚引用
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,
在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是 说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
使用 PhantomReference
类创建,需要配合 ReferenceQueue
:
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
特点:
-
不能通过
get()
获取对象。 -
主要作用是 在对象被回收时收到系统通知,用于资源回收(如堆外内存)。
适用场景:
-
管理堆外内存(Direct Memory)。
-
更灵活地监控 GC 行为。
JVM调优
JVM调优
JVM 调优的核心目标是 减少 GC 对业务的影响、避免内存泄漏、提升性能。
常见手段包括:调整内存参数、选择合适 GC 收集器、分析 GC 日志、使用监控工具、优化代码逻辑。
例如在大堆低延迟场景可以用 G1 GC,在吞吐量优先场景可以用 Parallel GC。
调优命令有哪些?
JVM 调优常用命令有:
-
jps(查看进程),
-
jstat(GC/内存监控),
-
jmap(导出堆快照),
-
jstack(线程堆栈分析),
-
jconsole / jvisualvm(可视化监控)。
它们配合使用能快速定位 内存泄漏、GC 频繁、死锁 等问题。
你知道哪些JVM性能调优参数?(高频)
对于JVM调优,主要就是调整年轻代、年老代、元空间的内存空间大小及使用的垃圾回收器类型。
JVM 调优常用参数分为三类:
-
内存参数:
-Xms
,-Xmx
,-Xmn
,-Xss
-
-Xms:设置堆的初始化大小
-
-Xmx:设置堆的最大大小
-
-
GC 参数:
-XX:+UseG1GC
,-XX:NewRatio
,-XX:SurvivorRatio
,-XX:MaxTenuringThreshold
-
-XX:NewSize:新生代大小
-
-XX:NewRatio 新生代和老生代占比
-
-XX:NewSize:新生代大小
-
--XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
-
-
日志参数:
-XX:+PrintGCDetails
,-Xloggc:gc.log
根据业务场景(高吞吐 / 低延迟 / 大堆内存)选择合适的组合,才能达到最佳性能。
JVM 调优的参数可以在哪里设置参数值?
tomcat的设置vm参数
修改TOMCAT_HOME/bin/catalina.sh文件,如下图
JAVA_OPTS="-Xms512m -Xmx1024m"
springboot项目jar文件启动
通常在linux系统下直接加参数启动springboot项目
nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod &
nohup : 用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行
参数 & :让命令在后台执行,终端退出后命令仍旧执行。
你用过哪些性能调优工具?(高频)
1、jdk自带监控工具:
-
jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制 台,用于对JVM中内存,线程和类等的监控
-
-
jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
2、第三方 :
MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工 具,它可以帮助我们查找内存泄漏和 减少内存消耗 GChisto,一款专业分析gc日志的工具
你都有哪些手段用来排查内存溢出?(高频)
内存溢出包含很多种情况,我在平常工作中遇到最多的就是堆溢出。
有一次线上遇到故障,重新启动后,使用jstat 命令,发现Old区在一直增长。
我使用 jmap命令,导出了一份线上堆栈,然后使用MAT进行分析。通过对GC Roots的分析,我发现了一个非常大的 HashMap对象,这个原本是有位同学做缓存用的,但是一个无界缓存,造成了堆内存占用一直上升。后来,将这个缓存改成 Guava Cache,并设置了弱引用,故障就消失了。
Java 内存泄漏的排查思路是:监控发现问题 → 导出堆快照 → 使用 MAT/VisualVM 分析对象引用链 → 定位可疑代码(静态集合、ThreadLocal、缓存、未关闭资源等) → 修复并验证。