1. JVM组成
1.1 JVM由哪些部分组成?运行流程?(讲一下JVM)⭐️
难易程度:☆☆☆ 出现频率:☆☆☆☆
Java Virtual Machine:Java 虚拟机,Java程序的运行环境(java二进制字节码的运行环境)
好处:一次编写,到处运行;自动内存管理,垃圾回收机制程序运行之前,需要先通过编译器将 Java 源代码文件编译成 Java 字节码文件;
程序运行时,JVM 会对字节码文件进行逐行解释,翻译成机器码指令,并交给对应的操作系统去执行。
好处:一次编写,到处运行;自动内存管理,垃圾回收机制
JVM <—> 操作系统(windows、linux)<—> 计算机硬件(cpu、内存条)
java跨平台是因JVM屏蔽了操作系统的差异,真正运行代码的不是操作系统
JVM 主要由四个部分组成: 运行流程:
Java 编译器(javac)将 Java 代码转换为字节码(.class 文件)1. 类加载器(ClassLoader)
负责加载 .class 文件,将 Java 字节码加载到内存中,并交给 JVM 执行
2. 运行时数据区(Runtime Data Area)
管理JVM使用的内存。主要包括:
方法区(Method Area):存储类的元数据、常量、静态变量等。
堆(Heap):存储所有对象和数组,垃圾回收器主要回收堆中的对象。
栈(Stack):每个线程都有一个栈,用于存储局部变量、方法调用等信息。
程序计数器(PC Register):每个线程有一个程序计数器,指示当前线程正在执行的字节码指令地址。
本地方法栈(Native Method Stack):支持本地方法的调用(通过 JNI)。
其中方法区
和堆
是线程共享的,虚拟机栈
、本地方法栈
和程序计数器
是线程私有的。3. 执行引擎(Execution Engine)
负责执行字节码,包含:
解释器:逐条解释执行字节码。
JIT 编译器:将热点代码编译为机器码,提高执行效率。
垃圾回收器:回收堆中的不再使用的对象,释放内存。
4. 本地库接口(Native Method Library)
允许 Java 程序通过 java本地接口JNI(Java Native Interface)调用本地方法(如 C/C++ 编写的代码),与底层系统或硬件交互。
1.2 什么是程序计数器?
难易程度:☆☆☆ 出现频率:☆☆☆☆
程序计数器:线程私有的,每个线程一份,内部保存字节码的行号。用于记录正在执行的字节码指令的地址。
每个线程都有自己的程序计数器,确保线程切换时能够继续执行未完成的任务。
1.3 你能给我详细的介绍Java堆吗?⭐️⭐️
难易程度:☆☆☆ 出现频率:☆☆☆☆
Java堆是 JVM 中用于存储所有对象和数组的内存区域。线程共享的区域。当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
它被分为:
年轻代(存储新创建的对象),被划分为三部分:
Eden区:大多数新对象的分配区域;
S0 和 S1(两个大小严格相同的Survivor区):Eden 空间经过 GC 后存活下来的对象会被移到其中一个 Survivor 区域;
老年代:在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到老年代区间。
永久代:JDK 7 及之前,JVM 的方法区(也称永久代),保存的类信息、静态变量、常量、编译后的代码;
元空间:JDK 8 及之后,永久代被 Metaspace(元空间)取代,移除了永久代,把数据存储到了本地内存的元空间中,且其大小不再受 JVM 堆的限制,防止内存溢出。
1.4 什么是虚拟机栈
难易程度:☆☆☆ 出现频率:☆☆☆☆
Java Virtual machine Stacks (java 虚拟机栈)
每个线程在 JVM 中私有的一块内存区域,称为虚拟机栈,先进后出,用于存储方法的局部变量和方法调用信息;
每个栈由多个栈帧(frame)组成,当线程执行方法时,为该方法分配一个栈帧(Stack Frame);
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法;
垃圾回收是否涉及栈内存?
垃圾回收主要指就是堆内存,
栈内存中不会有垃圾回收的概念,因为栈内存是由 JVM 自动管理的,方法执行完成时,栈帧弹栈,内存就会释放;
栈内存分配越大越好吗?
未必,默认的栈内存通常为1024k;
栈内存过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的线程数减半;
方法内的局部变量是否线程安全?
方法内的局部变量 本身是线程安全的,因为它们存储在每个线程独立的栈中,不会被其他线程共享。
但如果局部变量是引用类型,并且该引用指向的对象逃离了方法作用范围(例如被返回或传递到外部),则需要考虑该对象的线程安全性。
如果对象是可变的,并且被多个线程访问,可能会引发线程安全问题。
栈内存溢出情况(StackOverflowError)
栈帧过多导致栈内存溢出;
典型问题:递归调用会在栈中创建新的栈帧,如果递归深度过大,可能会导致栈空间耗尽,从而抛出
栈帧过大导致栈内存溢出
堆栈的区别是什么?
栈内存用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的。
堆会GC垃圾回收,而栈不会;
栈内存是线程私有的,而堆内存是线程共有的;
两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常
栈空间不足:java.lang.StackOverFlowError
堆空间不足:java.lang.OutOfMemoryError
1.5 运行时数据区中包含哪些区域? ⭐️⭐️⭐️⭐️⭐️
JVM 运行时数据区包括方法区、堆、栈、程序计数器和本地方法栈。
方法区存储类的元数据、常量池、静态变量和 JIT 编译后的代码。
类的结构信息:每个类的信息,如类名、父类名、接口、方法、字段的名称和描述等。常量池:存储常量值,如字符串常量、类常量等。
静态变量:属于类的变量,而不是某个实例的变量。
JIT编译后的代码是指 Jvm在运行时将热点代码从字节码编译为本地机器代码。
方法区在 JDK 7 之前被称为 “永久代”,从 JDK 8 开始,永久代被移除,改为使用元空间
堆是 JVM 中最大的内存区域,负责存储所有的对象实例和数组,并进行垃圾回收。
虚拟机栈存储每个线程的局部变量、方法调用信息和返回地址等。
程序计数器是每个线程私有的,用于存储当前线程正在执行的字节码指令的地址。
本地方法栈支持 JNI 本地方法调用,线程私有。
专门为本地方法调用而设计。它用于执行本地代码时所需的栈空间。
在 JDK 1.8 时 JVM 的内存结构主要有两点不同,
一个是方法区(Method Area)在 JDK 1.8 被替换为元空间(Metaspace),且元空间使用本地内存。
另一个是运行时常量池(Runtime Constant Pool)在 JDK 1.7 属于方法区的一部分,而在 JDK 1.8 变成元空间的一部分。
哪些线程私有?哪些线程共享?
线程私有的:线程计数器、虚拟机栈、本地方法栈
线程共享的:堆、方法区
哪些区域可能会出现 OutOfMemoryError?
堆(Heap): 当堆内存不足时,会抛出OutOfMemoryError。
方法区(Method Area): 当方法区内存不足时,会抛出OutOfMemoryError,通常是由于加载的类太多或常量池中的数据过多。
1.6 能不能解释一下方法区?
难易程度:☆☆☆ 出现频率:☆☆☆
方法区 是 JVM 运行时数据区的一部分,主要用于存储类的信息、常量、静态变量以及 JIT 编译后的代码。
在 JDK 7 之前,这部分内存称为永久代(PermGen),而在 JDK 8 以后,永久代被移除,取而代之的是元空间(Metaspace),它位于本地内存中,不再受堆内存限制。虚拟机启动的时候创建,关闭虚拟机时释放。
方法区的内存由 JVM 管理,并在类卸载时进行垃圾回收。
如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspac。
方法区和永久代以及元空间是什么关系呢? ⭐️
方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,
方法区是java虚拟机规范中的一个概念,而永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。
永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。
元空间和方法区的区别?
元空间(Metaspace)和方法区的主要区别在于:
内存存储位置:方法区在JVM的堆内存中,而元空间使用的是本地内存(Native Memory)。
内存管理:方法区受JVM管理,可能导致
OutOfMemoryError
;元空间由操作系统管理,避免了这个问题。JVM版本差异:方法区在JDK 7及之前存在,JDK 8后被元空间取代。
内存溢出:方法区可能因为空间不足引发
OutOfMemoryError
,而元空间则避免了这个问题,因为它不依赖堆内存。
元空间有什么作用?为什么要有元空间?
元空间(Metaspace)的作用是存储JVM类的元数据,它在JDK 8及之后的版本中取代了之前的永久代(PermGen)。
与永久代不同,元空间不再使用堆内存,而是直接使用本地内存(Native Memory)。这是为了提高性能和避免永久代内存溢出的风险。
主要有两个原因:
解决内存溢出问题:JDK 7 及之前的永久代(PermGen)存储类元数据,容易出现 OutOfMemoryError。而元空间(Metaspace)使用本地内存,仅受系统内存限制,避免了该问题。
性能优化:元空间使用本地内存而非堆内存存储类元数据,提升了类加载性能,减少了垃圾回收(GC)的压力,因为元空间的回收由 JVM 和操作系统共同管理,无需频繁触发 GC。
Java对象的创建过程?⭐️⭐️⭐️⭐️⭐️
类加载检查,当程序执行到 new 指令时,JVM 会先检查对应的类是否已经被加载、解析和初始化过。如果类尚未加载,JVM 会按照类加载机制(加载、验证、准备、解析、初始化)完成类的加载过程。这一步确保了类的元信息(如字段、方法等)已经准备好,为后续的对象创建奠定基础。
内存的分配,JVM 会为新对象分配内存空间。对象所需的内存大小在类加载完成后就可以确定,因此分配内存的过程就是从堆中划分一块连续的空间,主要有两种方式:①一种是通过指针碰撞,如果堆中的内存是规整的(已使用和空闲区域之间有明确分界),JVM 可以通过移动指针来分配内存。
②另一种是通过空闲列表,如果堆中的内存是碎片化的,JVM 会维护一个空闲列表,记录可用的内存块,并从中分配合适的区域。
此外,为了保证多线程环境下的安全性,JVM 还会采用两种策略避免内存分配冲突,一种是通过 CAS 操作尝试更新分配指针,如果失败则重试;另一种是每个线程在堆中预先分配一小块专属区域,避免线程间的竞争。零值初始化,JVM 会对分配的内存空间进行初始化,将其所有字段设置为零值(如 int 为 0,boolean 为 false,引用类型为 null)。这一步确保了对象的实例字段在未显式赋值前有一个默认值,从而避免未初始化的变量被访问。
设置对象头,其中包含Mark Word、Klass Pointer和数组长度。Mark Word 用于存储对象的哈希码、GC 分代年龄、锁状态标志等信息。Klass Pointer 指向对象所属类的元数据(即 Person.class 的地址)。
执行构造方法,用<init> 方法完成对象的初始化。构造方法会根据代码逻辑对对象的字段进行赋值,并调用父类的构造方法完成继承链的初始化。这一步完成后,对象才真正可用。
Java 创建对象的四种常见方式
new 关键字(最常见)
使用 new 关键字可以直接创建对象,并调用 无参或有参构造方法 进行初始化。
反射
反射机制可以在运行时动态创建对象。
clone() 方法(对象克隆)
clone() 方法用于创建一个相同内容的新对象,不会调用构造方法。需要实现 Cloneable 接口,并重写 clone() 方法。
反序列化(Serializable)
反序列化可以将存储或传输的对象数据恢复成 Java 对象,不会调用构造方法。
对象访问定位的两种方式知道吗?优缺点?⭐️
对象访问定位是 JVM 中通过引用变量找到实际对象的过程。在 Java 中,有两种主要的对象访问定位方式:句柄和直接指针。
句柄访问:引用变量存储的是句柄的地址,句柄再指向对象。
优点:GC 时对象移动不影响引用,稳定性高;实例和类型数据分离,便于管理。
缺点:访问慢(两次跳转),内存占用大(需要句柄池)。直接指针访问:引用变量直接存储对象地址。
优点:访问快(一次跳转),内存占用小。
缺点:GC 时需要修改所有引用,开销大。
1.7 介绍一下运行时常量池?
常量池
可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池
常量池是 .class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
1.8 你听过直接内存吗?
难易程度:☆☆☆ 出现频率:☆☆☆
它不受 JVM 内存回收管理,是虚拟机的系统内存;
常见于在 NIO 中使用直接内存,不需要在堆中开辟空间进行数据的拷贝,jvm可以直接操作直接内存,从而使数据读写传输更快。但分配回收成本较高。使用传统的IO的时间要比NIO操作的时间长了很多,也就说NIO的读性能更好。
这个是跟我们的JVM的直接内存是有一定关系,如下图,传统阻塞IO的数据传输流程和NIO传输数据的流程。
2. 类加载器
2.1 什么是类加载器,类加载器有哪些?
难易程度:☆☆☆☆ 出现频率:☆☆☆
JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将.class字节码文件加载到JVM内存,生成对应的Class对象,供程序使用。
启动类加载器(BootStrap ClassLoader):
该类并不继承ClassLoader类,其是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库。扩展类加载器(ExtClassLoader):
该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。应用类加载器(AppClassLoader):
该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。自定义类加载器:
开发者自定义类继承ClassLoader,实现自定义类加载规则。
类加载器的体系并不是“继承”体系,而是委派体系,类加载器首先会到自己的parent中查找类或者资源,如果找不到才会到自己本地查找。类加载器的委托行为动机是为了避免相同的类被加载多次
2.2 什么是双亲委派模型?⭐️⭐️⭐️⭐️
难易程度:☆☆☆☆ 出现频率:☆☆☆☆
双亲委派模型要求类加载器在加载某一个类时,先委托父加载器尝试加载。
如果父加载器可以完成类加载任务,就返回成功;
只有父加载器无法加载时,子加载器才会加载。
2.3 JVM为什么采用双亲委派机制?(好处)⭐️
避免类的重复加载:父加载器加载的类,子加载器无需重复加载。
保证核心类库的安全性:为了安全,保证类库API不会被修改。如
java.lang包下的类
只能由 启动类加载器Bootstrap ClassLoader 加载,防止被篡改。
2.4 说一下类的生命周期?
一个类从被加载到虚拟机内存中开始,到从内存中卸载,它的整个生命周期包括了:
加载、验证、准备、解析、初始化、使用和卸载这7个阶段。
其中,验证、准备和解析这三个部分统称为连接(linking)。
2.5 说一下类装载的执行过程?⭐️⭐️⭐️⭐️⭐️
难易程度:☆☆☆☆☆ 出现频率:☆☆☆
类装载过程包括三个阶段:载入、连接和初始化,连接细分为 验证、准备、解析,这是标准的 JVM 类装载流程。
加载(Loading):通过类加载器找到
.class
文件读取到内存,生成Class
对象。
连接(Linking):
验证:检查字节码是否合法,防止恶意代码破坏 JVM;
准备:为类的静态变量分配内存并设置默认初始值,但不执行赋值逻辑;
解析:将常量池中的 符号引用(如类名、方法名)转为 直接引用(内存地址)。
初始化(Initialization):执行类的静态代码块和静态变量赋值。在准备阶段,静态变量已经被赋过默认初始值了,在初始化阶段,静态变量将被赋值为代码期望赋的值。比如说 static int a = 1;,在准备阶段,a 的值为 0,在初始化阶段,a 的值为 1
类装载完成后的阶段:加载完成后,类进入‘使用阶段’。当
Class
对象不再被引用时,可能触发‘卸载’。使用:JVM 通过
Class
对象创建实例、调用方法,进入正常运行阶段。
卸载:当Class
对象不再被引用时,由 GC 回收,但 JVM 核心类(如java.lang.*
)不会被卸载。
3. 垃圾回收
3.1 简述Java垃圾回收机制?(GC是什么?为什么要GC)
GC(Garbage Collection,垃圾回收)是 Java 中自动管理内存的机制,负责回收不再使用的对象,以释放内存空间。
垃圾回收是 Java 程序员不需要显式管理内存的一大优势,它由 JVM 自动进行。
GC 的主要目的是:
自动管理内存:程序运行过程中会创建大量的对象,但一些对象在使用完后不再被引用。手动管理这些对象的内存释放非常繁琐且容易出错;
防止内存泄漏:如果不及时释放无用对象的内存,系统的可用内存会越来越少,最终可能导致 OutOfMemoryError;
避免内存溢出:GC 机制能够保证内存不会因为长期积累未回收的对象而耗尽。
3.2 对象什么时候可以被垃圾器回收?如何判断对象是否死亡?⭐️
难易程度:☆☆☆☆ 出现频率:☆☆☆☆
如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
如果要定位什么是垃圾,有两种方式来确定,
1. 引用计数法:通过计数引用的数量,当引用为 0 时回收。但不能处理循环引用
这种方法通过给每个对象维护一个引用计数器。当有一个新的引用指向该对象时,引用计数加 1;当引用离开时,计数减 1。
2. 可达性分析算法:通过检查对象是否从根对象可达,无法访问的对象可以回收。是 Java 使用的主要方法。
根对象是那些肯定不能当做垃圾回收的对象,就可以当做根对象
根对象包含:虚拟机栈中引用的对象;静态变量;活动线程的引用;JNI 引用的对象。
3.3 JVM 垃圾回收算法有哪些?⭐️⭐️⭐️⭐️⭐️
难易程度:☆☆☆ 出现频率:☆☆☆☆
Java 中的垃圾回收器采用不同的算法来回收不再使用的对象。
标记-清除算法:简单,效率高,但有内存碎片;
从根对象开始,遍历所有可以访问到的对象,并标记它们。
遍历所有对象,清除那些未被标记的对象,即不可达的对象。
标记-整理算法:解决了碎片问题,但整理过程较慢,效率低;
与标记-清除算法相同,标记所有可达的对象。
清除不可达对象后,将存活的对象移动到内存的一端,消除内存碎片。
复制算法:无碎片,内存整理高效,但内存利用率低;
将堆内存分为两部分,每次只使用其中一部分。当一部分用完时,垃圾回收器将存活的对象复制到另一部分,并清空当前使用的部分。
分代收集算法
3.4 分代收集算法
分代收集算法-堆的区域划分
java8时,堆被分为了两份:新生代和老年代【1:2】,在java7时,还存在一个永久
对于新生代,内部又被分为了三个区域。
伊甸园 Eden 区,幸存者区 survivor (分成 from 和 to )【8:1:1】
分代收集算法-工作机制
新创建的对象,都会先分配到eden区
当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象
存活对象采用复制算法复制到 to 中,复制完后,伊甸园和 from 内存都得到释放(即清空
经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将存活的对象复制到from区
又来了一批数据
当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
为什么要用垃圾分代回收?
分代收集的主要目的是提高垃圾回收效率,减少GC的停顿时间。其基本思想是将堆内存分成多个区域(通常是年轻代、老年代和持久代),根据对象的存活时间来进行不同的回收策略。
年轻代(Young Generation):存放刚创建的对象。因为大部分对象都是短命的,年轻代回收主要采用复制算法,回收效率高,能够快速回收短生命周期的对象。
老年代(Old Generation):存放长时间存活的对象。老年代对象较少变化,回收时使用标记-清除或标记-整理算法,回收速度相对较慢,但减少了频繁的垃圾回收。
持久代(Permanent Generation)(JDK 7及之前)/元空间(Metaspace)(JDK 8及之后):存放类信息等元数据。虽然它不涉及对象的存活与否,但也通过分代管理避免频繁回收。
分代收集的优势:
高效回收:通过将对象按存活时间分代,年轻代的回收可以频繁进行,而老年代则较少回收,这样可以显著提高回收效率。
减少停顿时间:年轻代采用复制算法,效率高且停顿时间短,减少了GC对应用程序性能的影响。
MinorGC、 Mixed GC 、 FullGC的区别是什么?
MinorGC(年轻代垃圾回收)
只回收年轻代,频繁且代价较低,停顿时间短。
对象会在年轻代中被回收,短命对象被清理,存活的对象晋升到老年代。
因为年轻代内存较小,回收的代价通常较低,停顿时间短,回收效率高。Mixed GC(混合垃圾回收)
回收年轻代 + 部分老年代,G1 回收器特有,减少了老年代Full GC的频率,停顿时间比Minor GC长,但比Full GC短。
Mixed GC会回收年轻代的对象,同时也回收老年代的一部分。具体回收哪些老年代区域,由JVM根据内存压力和区域分布决定。
这种回收方式能减少老年代的Full GC触发次数。
Mixed GC比Minor GC涉及的内存范围大,因此会导致更长的停顿时间。相比Full GC,它的停顿时间仍然较短,但比Minor GC长。FullGC(完全垃圾回收)
回收整个堆(年轻代、老年代、永久代/元空间),停顿时间最长(STW),对性能影响最大,应该尽量避免。
Full GC会检查整个堆内存,进行垃圾回收。它不仅回收年轻代,还回收老年代和永久代(或元空间)。这是一个全局性的回收过程,因此会占用更多的时间,导致较长的停顿
STW(Stop-The-World):暂停所有应用程序线程,等待垃圾回收的完成
3.5 说下 JVM 有哪些垃圾回收器?⭐️⭐️⭐️⭐️
难易程度:☆☆☆☆ 出现频率:☆☆☆☆
在jvm中,实现了多种垃圾收集器,包括:
串行垃圾收集器
并行垃圾收集器
CMS(并发)垃圾收集器
G1垃圾收集器
串行垃圾收集器
Serial和Serial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑
Serial 作用于新生代,采用复制算法
Serial Old 作用于老年代,采用标记-整理算法
垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
并行垃圾收集器
Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器
Parallel New作用于新生代,采用复制算法
Parallel Old作用于老年代,采用标记-整理算法
垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。
CMS(并发)垃圾收集器
CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,
该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,
停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行。
3.6 详细聊一下G1垃圾回收器
难易程度:☆☆☆☆ 出现频率:☆☆☆☆
G1 在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为默认的垃圾收集器。
G1 把 Java 堆划分为多个大小相等的独立区域Region,每个区域都可以扮演新生代或老年代的角色。
同时,G1 还有一个专门为大对象设计的 Region,叫 Humongous 区。
这种区域化管理使得 G1 可以更灵活地进行垃圾收集,只回收部分区域而不是整个新生代或老年代。
G1 GC的设计目标是能够在大内存环境中提供高吞吐量,同时尽量减少垃圾回收的停顿时间。如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC。
大对象的判定规则是,如果一个大对象超过了一个 Region 大小的 50%,比如每个 Region 是 2M,只要一个对象超过了 1M,就会被放入 Humongous 中。
G1 收集器的运行过程大致可划分为这几个步骤:
初始标记: 这一步会触发一次短暂停顿(Stop-The-World,STW),标记从 GC Roots 可以直接引用的对象,即标记所有直接可达的活跃对象。
并发标记,这一阶段与应用并发运行,标记堆中所有可达的对象。 这个阶段可能会持续较长时间,具体时间取决于堆的大小和对象数量。
混合收集,在并发标记完成后,G1 会计算出哪些区域的回收价值最高,即哪些区域包含最多垃圾。然后,它优先回收这些区域,包括部分年轻代区域和老年代区域。
通过选择回收成本低而收益高的区域,G1提高了回收效率,并尽量减少了停顿时间。
可预测的停顿,在垃圾回收期间,G1仍然需要进行停顿,但它提供了预测机制。用户可以通过JVM启动参数指定期望的最大停顿时间,G1会尽量在此时间内完成垃圾回收,以确保应用性能。
3.7 JDK中有几种引用类型?分别的特点是什么?⭐️
在Java中,引用类型有不同的级别,它们控制着对象的生命周期以及垃圾回收的行为。强引用、软引用、弱引用和虚引用都是Java中引用对象的方式,它们的区别主要体现在垃圾回收器回收对象的时机和条件上。
强引用(Strong Reference)
最常见的引用类型,只要强引用存在,对象就不会被垃圾回收。
只有当引用被显式置为null
或者没有任何引用指向该对象时,垃圾回收器才会回收它
Object obj = new Object(); // 强引用
obj = null; // 显式置为 null,对象才可能被 GC
若强引用对象过多,可能导致内存泄漏或 OOM。软引用(Soft Reference)
软引用用于描述那些在内存充足时不应回收、但在内存不足时可以回收的对象。
需要配合SoftReference使用
SoftReference<Object> softRef = new SoftReference<>(new Object());弱引用(Weak Reference)
弱引用比软引用更弱,每次垃圾回收时都会被回收,无论内存是否充足。
需要配合WeakReference使用
WeakReference<Object> weakRef = new WeakReference<>(new Object());虚引用(Phantom Reference)
虚引用是最弱的引用类型,必须与引用队列(ReferenceQueue)配合使用。
虚引用的对象被回收时,会将虚引用加入引用队列,由Reference Handler线程调用虚引用相关方法来释放直接内存。
虚引用的主要作用是用来监控对象被回收的状态。
4. JVM实践(调优)
4.1 JVM 调优的参数可以在哪里设置参数值?
难易程度:☆☆ 出现频率:☆☆☆
我们当时的项目是springboot项目,可以在项目启动的时候,java -jar中加入参数就行了
tomcat的设置vm参数
war包部署在tomcat中设置
修改TOMCAT_HOME/bin/catalina.sh文件
JAVA_OPTS=”-Xms512m -Xmx1024m”
linux下是.sh结尾,windows是.bat结尾
springboot项目jar文件启动
jar包部署在启动参数设置
通常在linux系统下直接加参数启动springboot项目
java -Xms512m -Xmx1024m
nohup java -Xms512m -Xmx1024m -jar xxxx.jar –spring.profiles.active=prod &
nohup : 用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行
参数 & :让命令在后台执行,终端退出后命令仍旧执行。
4.2 用的 JVM 调优的参数都有哪些?
难易程度:☆☆☆ 出现频率:☆☆☆☆
嗯,这些参数是比较多的。我记得当时我们设置过:…。具体的指令记不太清楚。
设置堆内存大小
-Xms:初始堆大小
-Xmx:最大堆大小
设置年轻代中Eden区和两个Survivor区的大小比例
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:n 为 3 表示年轻代和年老代比值为 1:3,年轻代占总和的 1/4
-XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。如 n=3 表示 Eden 占 3, Survivor 占 2,一个 Survivor 区占整个年轻代的 1/5
设置使用哪种垃圾回收器
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行老年代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
年轻代晋升老年代阈值
虚拟机栈的设置
4.3 说一下 JVM 调优的工具?
难易程度:☆☆☆☆ 出现频率:☆☆☆☆
嗯,我们一般都是使用jdk自带的一些工具,比如
命令工具
jps
输出JVM中运行的进程状态信息。 jps
jstack
查看java进程内线程的堆栈信息。 jstack [option] <pid>
jmap
用于生成堆转存快照
jmap [options] pid 内存映像信息
jmap -heap pid 显示Java堆的信息
jmap -dump:format=b,file=heap.hprof pid
format=b表示以hprof二进制格式转储Java堆的内存
file=<filename>用于指定快照dump文件的文件名。
jhat
用于分析jmap生成的堆转存快照(一般不推荐使用,而是使用Ecplise Memory Analyzer)
jstat
是JVM统计监测工具。可以用来显示垃圾回收信息、类加载信息、新生代统计信息等。
总结垃圾回收统计:jstat -gcutil pid
可视化工具
jconsole
JDK 自带的监控工具,
用于对jvm的内存,线程,类 的监控
打开方式:java 安装目录 bin目录下 直接启动 jconsole.exe 就行
VisualVM:故障处理工具
能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈
打开方式:java 安装目录 bin目录下 直接启动 jvisualvm.exe就行
4.4 java内存泄露的排查思路?
难易程度:☆☆☆☆ 出现频率:☆☆☆☆
内存泄漏通常是指堆内存,通常是指一些大对象不被回收的情况。
通过jmap或设置jvm参数获取堆内存快照dump
通过工具,VisualVM去分析dump文件,VisualVM可以加载离线的dump文件
通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
找到对应的代码,通过阅读上下文的情况,进行修复即可
4.5 CPU飙高排查方案与思路?
难易程度:☆☆☆☆ 出现频率:☆☆☆☆
使用top命令查看占用cpu的情况
通过top命令查看后,可以查看是哪一个进程占用cpu较高,记录这个进程id
使用ps命令查看进程中的线程信息,看看哪个线程的cpu占用较高
使用jstack命令查看进程中哪些线程出现了问题,最终定位问题代码的行号
暂无评论内容