目录
1.元空间中包含运行时常量池,都放在本地内存中,如何进行回收?
专栏简介
八股战神篇专栏是基于各平台共上千篇面经,上万道面试题,进行综合排序提炼出排序前百的高频面试题,并对这些高频八股进行关联分析,将每个高频面试题可能进行延伸的问题进行分析排序选出高频延伸八股题。面试官都是以点破面从一个面试题不断深入,目的是测试你的理解程度。本专栏将解决你的痛点,助你从容面对。本专栏已更新Java基础高频面试题、Java集合高频面试题、MySQL高频面试题、JUC Java并发高频面试题、JVM高频面试题,后续会继续更新Redis、操作系统、计算机网络、设计模式、场景题等,计划在七月前更新完毕(赶在大家高频面试前)点此链接订阅专栏“八股战神篇”。
一 请解释Java虚拟机(JVM)及其主要功能
Java虚拟机(JVM)是Java平台的核心组成部分,它是一个能够执行Java字节码的虚拟计算机。JVM的主要功能是提供一个平台无关的执行环境,使得Java程序可以在任何支持JVM的系统上运行。JVM通过将字节码转化为平台特定的机器码并执行,从而实现了Java的跨平台特性。
JVM的主要功能包括:
加载和验证类:JVM通过类加载器加载Java类文件,并验证字节码的正确性,确保代码安全性。
执行字节码:JVM通过解释器或即时编译器(JIT)将字节码转换为机器码并执行。
内存管理与垃圾回收:JVM自动管理内存分配和回收,避免开发者手动处理内存。
提供运行时环境:JVM为Java程序提供必要的运行时支持,包括堆、栈、方法区等内存区域,支持多线程、异常处理等特性。
延伸
1. JVM的基本概念
JVM(Java Virtual Machine)是一种虚拟化的计算机,它并不直接运行在硬件上,而是运行在操作系统上。JVM的设计允许Java程序跨平台运行,其背后的关键在于字节码。Java程序在编译后生成字节码文件(.class
文件),这些字节码文件并不依赖于具体平台的硬件架构,而是JVM负责将字节码转换为特定平台的机器码并执行。
字节码:Java程序编写后,由Java编译器(
javac
)生成字节码,而不是机器码。字节码是一种中间代码,不依赖于操作系统或硬件。跨平台性:不同操作系统或硬件架构下,JVM的实现是不同的,但它们都能解析相同的字节码文件,因此Java程序可以在不同平台上无缝运行。
2. JVM的主要功能
(1) 加载和验证类
JVM需要从文件系统或其他来源加载Java类文件。类加载的过程是由类加载器(ClassLoader)来完成的,类加载器负责读取.class
字节码文件并将它们加载到JVM的内存中。
类加载器:JVM中的类加载器分为不同类型,如引导类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)。它们负责加载Java标准库类、扩展库类以及用户定义的类。
类的验证:加载的类需要通过字节码验证器检查,确保没有安全漏洞。例如,验证类的结构是否符合JVM规范,检查是否有非法的内存访问或不合规的操作。
(2) 执行字节码
一旦类加载完成并验证无误,JVM就开始执行字节码。执行过程分为两种方式:
解释执行:JVM的解释器逐行读取字节码并执行。这种方式速度较慢,但可以立即执行字节码。
JIT编译:JVM通过即时编译器(JIT)将频繁使用的字节码片段编译成本地机器码,并缓存,以便后续直接执行。JIT编译极大地提升了程序的执行效率,尤其在长时间运行的应用程序中。
JIT编译例子: 当JVM执行到某段代码时,如果这段代码经常被调用,JIT编译器会将其编译成机器码,这样下次再执行时,就不需要解释执行,而是直接使用机器码执行,从而提高了性能。
(3) 内存管理与垃圾回收
JVM负责自动管理内存的分配与回收。内存管理的关键部分是垃圾回收(Garbage Collection,GC),它通过回收不再使用的对象来释放内存,避免了内存泄漏的问题。
堆内存(Heap):所有对象实例都存放在堆中。JVM的垃圾回收器主要在堆中进行内存回收。
栈内存(Stack):每个线程都有自己的栈,存储局部变量、操作数栈和方法调用的信息。栈内存的管理是由JVM自动处理的。
垃圾回收器(GC):JVM有不同的垃圾回收算法,如串行GC、并行GC、G1GC等,目的是自动回收不再使用的对象,优化内存使用。
GC的工作过程通常分为几个步骤:
1. 标记阶段:标记所有可达的对象。
标记阶段:标记所有可达的对象。
清除阶段:清除不可达的对象,回收其占用的内存。
压缩阶段:清理后的堆可能产生内存碎片,JVM会尝试压缩堆内存,减少碎片化。
(4) 提供运行时环境
JVM不仅负责执行字节码,还提供了一个运行时环境,支持多个关键功能:
线程管理:JVM支持多线程执行,提供线程调度、同步等机制。
异常处理:JVM负责抛出和捕获异常,确保程序在出现错误时能够处理异常,避免程序崩溃。
方法调用:JVM管理方法的调用过程,包括栈帧的创建、方法参数的传递和返回值的处理。
JVM通过这些功能支持了Java程序的稳定性和高效执行。
二 对象创建的过程了解吗
对象的创建过程是 Java 虚拟机(JVM)中一个非常重要的环节,它主要分为五步。
第一步是进行类加载检查,当程序执行到 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> 方法完成对象的初始化。构造方法会根据代码逻辑对对象的字段进行赋值,并调用父类的构造方法完成继承链的初始化。这一步完成后,对象才真正可用。
延伸
1.Java 创建对象的四种常见方式
(1)new 关键字(最常见)
使用 new 关键字可以直接创建对象,并调用 无参或有参构造方法 进行初始化。
示例:
Person person1 = new Person();
Person person2 = new Person("小粥", 18);
适用于大部分场景,代码直观,易于理解。
(2)反射(Class 和 Constructor 两种方式)
反射机制可以在运行时动态创建对象,主要通过 Class.newInstance() 和 Constructor.newInstance() 两种方式实现。
Class.newInstance()
通过 Class 对象的 newInstance() 方法创建实例,只能调用无参构造方法。
Person person = Person.class.newInstance();
System.out.println(person);
限制:必须有无参构造方法,否则会抛出异常。
Constructor 类提供更灵活的创建方式,可以调用 任意构造方法(包括私有构造方法)。
Constructor<Person> constructor = Person.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true); // 允许访问私有构造方法
Person person = constructor.newInstance("小粥", 18);
System.out.println(person);
对比:
Class.newInstance() 只能调用无参构造方法,Constructor.newInstance() 可调用任意构造方法。
Constructor.newInstance() 可以通过 setAccessible(true) 访问私有构造方法。
(3)clone() 方法(对象克隆)
clone() 方法用于创建一个相同内容的新对象,不会调用构造方法。需要实现 Cloneable 接口,并重写 clone() 方法。
public class Person implements Cloneable {
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
}
Person person1 = new Person("小粥", 18);
Person person2 = person1.clone();
System.out.println(person1 == person2); // false
注意:clone() 只进行 浅拷贝,对象内部的引用类型变量仍然指向同一块内存。如果要实现 深拷贝,需要让所有引用类型的成员变量也实现 Cloneable 接口,并重写 clone() 方法。
(4)反序列化(Serializable)
反序列化可以将存储或传输的对象数据恢复成 Java 对象,不会调用构造方法。
Person person1 = new Person("小粥", 18);
byte[] bytes = SerializationUtils.serialize(person1);
Person person2 = (Person) SerializationUtils.deserialize(bytes);
System.out.println(person1 == person2); // false
特点:对象必须实现 Serializable 接口,否则无法序列化;反序列化创建的对象是全新的,与原对象无关;性能开销较大,适用于数据存储或网络传输,而不是频繁对象创建。
三 什么是双亲委派模型
双亲委派模型是 Java 类加载机制中的核心概念,它定义了类加载器之间的层次关系和加载规则。通过这种模型,Java 能够保证类的唯一性和安全性,同时避免重复加载类的问题。接下来我会详细讲述双亲委派模型的定义、层次结构和工作流程。
首先说一下什么是双亲委派模型,它其实是一种类加载机制,其规定了当一个类加载器收到类加载请求时,不会立即尝试自己去加载这个类,而是先将请求委托给父类加载器完成。只有当父类加载器无法加载该类(例如在父类的搜索范围内找不到对应的类)时,子类加载器才会尝试自己加载。这种机制确保了类的加载过程具有层次性,并且优先使用高层级的类加载器来加载核心类库。
接下来说一下类加载器的层次结构,主要分为四层,
第一层是启动类加载器(Bootstrap ClassLoader),它负责加载 JVM 核心类库(如 rt.jar 中的类),位于最顶层,通常由本地代码实现。
第二层是扩展类加载器(Extension ClassLoader),它负责加载 $JAVA_HOME/lib/ext 目录下的扩展类库。
第三层是应用程序类加载器(Application ClassLoader),它负责加载用户类路径(ClassPath)上的类,也称为系统类加载器。
第四层是自定义类加载器,开发者可以通过继承 ClassLoader 类实现自己的类加载器,用于加载特定需求的类。
这些类加载器之间形成了一个树状的层次结构,每个类加载器都有一个父加载器。
最后说一下双亲委派模型的工作流程,主要分为四步,
第一步是检查缓存,当前类加载器会先检查是否已经加载过目标类,如果已加载,则直接返回对应的 Class 对象。
第二步是委派父加载器,如果没有加载过,当前类加载器会将加载请求委派给父加载器处理。
第三步是递归向上,父加载器继续将请求委派给它的父加载器,直到到达 Bootstrap ClassLoader。
第四步是尝试加载,如果父加载器无法加载目标类,则子加载器会尝试自己加载。
延伸
1.双亲委派机制的作用:
1,保证类加载的安全性,通过双亲委派机制让顶层的类加载器去加载核心类,避免恶意代码去替换jdk中的核心类库。确保核心类库的安全性和完整性。
2,避免同一个类被多次加载,上层的类加载器如果加载过类,直接返回给类避免重复加载。
2.双亲委派模型的核心思想:
每个类加载器都拥有一个父加载器,在加载类时,都会先将加载请求委派给父加载器。
通过这种方式,Java 确保了核心类库不会被覆盖或篡改。比如
java.lang.String
类总是由Bootstrap ClassLoader
加载,不可能被应用程序的类加载器所替代。
3.双亲委派模型的优势
类的唯一性:通过双亲委派模型,同一个类只会被一个类加载器加载一次,从而避免了重复加载的问题。
安全性:核心类库(如 java.lang.String)由 Bootstrap ClassLoader 加载,防止用户自定义的恶意类冒充核心类库。
模块化管理:不同层级的类加载器负责加载不同范围的类,便于实现模块化和隔离性。
四 JVM的内存区域
JVM 的内存区域可以分为线程共享和线程私有两部分,每个部分都有明确的职责和作用,保障 Java 程序的高效运行。JDK1.7 和 1.8 时内存结构略有不同,接下来我先讲解一下 JDK1.7 时 JVM 的内存结构,然后再说一下 JDK1.8 时发生了哪些变动。
首先是线程共享的部分,一共有两个,
一个是堆(Heap),所有对象实例和数组都在这里分配内存,垃圾回收器(GC)会管理其中的对象回收。堆中还包含了字符串常量池(String Constant Pool),用于存储字符串字面量和常量。
另一个是方法区(Method Area):用于存储类元信息、常量、方法字节码等。其中运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期生成的各种字面量和符号引用。
然后是线程私有的部分,一共有三个,
第一个是虚拟机栈(VM Stack),每个线程启动时都会创建一个虚拟机栈,它存储方法调用过程中产生的栈帧,包括局部变量、操作数栈、方法返回地址等,每个方法调用都会创建一个新的栈帧,方法执行结束后栈帧出栈。
第二个是本地方法栈(Native Method Stack),专门用于存储本地方法(Native Method)的调用信息,与虚拟机栈类似,但用于 JNI(Java Native Interface)调用。
第三个是程序计数器(Program Counter Register),记录当前线程正在执行的字节码指令地址。它是 JVM 运行时最小的内存区域,每个线程都有一个独立的程序计数器。
最后是本地内存,
里面包含直接内存(Direct Memory),由 NIO(New Input/Output)直接分配,不受 JVM 堆的管理,通常用于高性能数据传输,如缓冲区(Buffer)。这个内存结构保证了 JVM 在执行 Java 代码时能够高效管理对象、执行方法调用,并支持多线程并发。
在 JDK 1.8 时 JVM 的内存结构主要有两点不同,
一个是方法区(Method Area)在 JDK 1.8 被替换为元空间(Metaspace),且元空间使用本地内存。
另一个是运行时常量池(Runtime Constant Pool)在 JDK 1.7 属于方法区的一部分,而在 JDK 1.8 变成元空间的一部分。
延伸
1.元空间中包含运行时常量池,都放在本地内存中,如何进行回收?
运行时常量池与元空间:运行时常量池是JVM用于存储编译期生成的常量(如字符串字面量、方法和字段的符号引用等)的地方。在Java 8之前,常量池是存放在永久代(PermGen)中的,而Java 8之后,永久代被移除,运行时常量池被放到了元空间(Metaspace)中。
元空间的内存管理:元空间使用的是本地内存(Native Memory),而不是堆内存。它的内存分配和回收由操作系统管理,而非JVM的垃圾回收器(GC)。元空间中的内存一般是较难回收的,因为类的元数据信息和运行时常量池数据一旦加载,很少会被卸载,除非对应的类被卸载。
回收问题:虽然元空间的内存管理依赖操作系统,但在某些情况下,内存可能会出现膨胀,导致应用程序占用过多的内存。如果元空间持续增长且没有及时进行类卸载操作,那么内存就会持续被占用。JVM提供了一些选项(如-XX:MaxMetaspaceSize)来限制元空间的大小,以防止内存过度使用。
2.什么是直接内存?
直接内存(Direct Memory)是指不属于JVM堆内存管理的一部分,而是通过Java的nio
(Non-blocking I/O)库来直接操作的内存区域。直接内存由操作系统直接管理,它允许Java程序绕过JVM堆内存进行数据的读写操作,从而提高性能,特别是在需要频繁进行I/O操作的场景下。直接内存的分配通常使用DirectByteBuffer
来实现,且不经过垃圾回收(GC)机制的管理。
3. 直接内存与堆内存的区别
堆内存:是JVM管理的内存区域,所有的Java对象(如
new
创建的对象)都存储在堆内存中。堆内存受JVM垃圾回收器(GC)管理,会自动清理不再使用的对象。直接内存:由操作系统直接管理,JVM不直接控制。它通常用于高效的数据读写(如NIO网络编程、文件操作等)。直接内存不经过GC处理,因此可以避免GC的干扰,减少内存管理的开销。
五 垃圾回收算法
垃圾回收(Garbage Collection,简称 GC)是 Java 虚拟机(JVM)中自动管理内存的重要机制,它通过一系列算法来识别和回收不再使用的对象,从而释放堆内存。接下来我会详细讲述常见的四种垃圾回收算法及其工作原理。
第一个是标记-清除算法(Mark-Sweep),它是最基础的垃圾回收算法,主要分为两个阶段,一个是标记阶段,从根对象(GC Roots)开始,递归遍历所有可达对象,并标记为“存活”; 另一个是清除阶段,遍历整个堆内存,回收未被标记的对象所占用的空间。
此算法主要存在两个问题,一个是内存碎片化,回收后的内存可能会产生大量不连续的碎片,导致大对象无法分配内存;另一个是效率较低,需要两次遍历堆内存,耗时较长。
第二个是复制算法(Copying),它通过将内存划分为两块(From 和 To),每次只使用其中一块,解决了标记-清除算法的内存碎片化问题,主要分为两个阶段,一个是复制阶段,当一块内存用完时,将存活的对象复制到另一块内存中,并按顺序排列;另一个是清理阶段,直接清空原来的内存块,无需额外的标记或清除操作。
此算法的优点是效率高且不会产生内存碎片,但缺点是需要双倍的内存空间。
第三个是标记-整理算法(Mark-Compact),它是对标记-清除算法的改进,它在标记阶段完成后,会将所有存活对象向一端移动,从而避免内存碎片化。主要分为两个阶段,一个是标记阶段,与标记-清除算法相同,标记所有存活对象;另一个是整理阶段,将存活对象移动到内存的一端,清理边界外的内存。
此算法适合老年代(Old Generation),因为老年代中的对象存活率较高,复制成本较大。
第四个是分代收集算法(Generational Collection),它是目前主流 JVM 的垃圾回收策略,它基于对象的生命周期将堆内存划分为新生代(Young Generation)和老年代(Old Generation)。
对于新生代,大多数对象朝生夕灭,采用复制算法进行垃圾回收。新生代进一步划分为 Eden 区和两个 Survivor 区(From 和 To);对于老年代,存活时间较长的对象存储在此,采用标记-清除或标记-整理算法进行垃圾回收。
这种算法结合了不同算法的优点,针对不同代的特点选择合适的回收策略,从而提升整体性能。
延伸
1.垃圾回收算法的标准
java垃圾回收的过程都会由单独的GC线程完成,不管哪一个回收算法,都会有部分阶段暂停所有的用户进程。这个过程称为Stop The World 简称为STW,这个时间进行回收垃圾,如果STW的时间过程就会影响用户的使用。
2.判断一个垃圾回收算法是否优秀,一般从三个方面进行判断:
吞吐量
吞吐量指的是CPU用于执行用户代码的时间 / CPU总执行时间。即 吞吐量 = 执行用户代码时间 / (执行用户时间 + GC时间)。吞吐量越大执行效率越高。
最大暂停时间
最大暂停时间指的是在所有垃圾回收过程中的STW时间的最大值。STW时间越大用户使用系统的影响就越大。
堆使用效率
不同的垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法可以使用完整的堆内存,而复制算法会将堆内存一分为二,每次只能使用一半的内存。从堆使用的效率来看,标记清除算法要由于复制算法。
上面的三种评价标准:吞吐量、最大暂停时间、堆使用效率,不可兼得。一般来说,堆内存越大,最大暂停时间越长,想要减少暂停时间就必须降低吞吐量。不同的垃圾回收算法使用与不同的场景。
3.如何判断是不是垃圾?
在 Java 中,垃圾回收(GC)主要通过 ** 和 GC Root Tracing(可达性分析)** 来判断对象是否可回收。
引用计数法:每个对象维护一个 引用计数器,当有新的引用指向该对象时,计数器加一;当引用失效时,计数器减一。如果计数器变为 0,表示该对象不再被使用,可以被回收。
但是这样会出现一个问题,当栈中没有引用指向堆中的内存,而堆中的对象互相引用。这样造成堆中的对象无法被回收。
这就是循环引用。
例如:AB实例对象在栈上已经没有变量引用了,由于计数器还是1无法回收,出现了内存泄漏
可达性分析法:JVM 采用 可达性分析作为主要的垃圾判断算法,通过一组称为 GC Roots 的对象作为起点,查找所有可达对象。如果某个对象无法从 GC Roots 访问,则认为它是垃圾,需回收。接下来会详细讲述GC Roots、对象的引用类型和生存状态。
第一,GC Roots 主要包括:
JVM 栈中的引用(方法的局部变量、参数等)。
静态变量引用(存储在方法区的类变量)。
运行时常量池中的引用(如字符串常量 String)。
JNI 本地方法引用(即 Native 方法中的引用)。
第二,对象的引用类型(可达性判断)
根据引用强度,Java 将对象引用分为以下四种类型:
强引用(StrongReference):使用 new 关键字创建的对象,不会被回收。
软引用(SoftReference):在内存不足时会被回收,适用于 缓存。
弱引用(WeakReference):不论内存是否足够,只要发生 GC,弱引用的对象都会被回收。
虚引用(PhantomReference):无法通过虚引用访问对象,主要用于 跟踪对象的回收状态。
第三,对象的生存状态
可达:对象能被 GC Roots 访问,说明仍然存活。
可回收(第一次标记):对象已经不可达,但 可能复活(如 finalize() 方法中重新引用 GC Roots)。
不可复活(第二次标记):若对象在 finalize() 之后仍然不可达,则被认定为垃圾,等待回收。
六 垃圾回收器
Java 的垃圾回收机制通过标记垃圾对象和回收无用内存,提升内存利用率,降低程序停顿时间。垃圾回收器是具体实现算法的工具,最常用的两种收集器是 CMS 和 G1 ,分别适用于不同的场景,接下来我会分别进行讲述。
第一个是 CMS 收集器,CMS(Concurrent Mark Sweep)是以最小化停顿时间为目标的垃圾收集器,适用于需要高响应的应用场景(如 Web 应用)。其基于“标记-清除算法”,回收流程包括以下阶段:
首先停止所有用户线程,启用一个GC线程进行初始标记(Stop The World),标记 GC Roots 能直接引用的对象,停顿时间短。
其次由用户线程和 GC 线程并发执行,进行并发标记,用户线程和 GC 线程并发执行,完成从 GC Roots 开始的对象引用分析。
然后,启动多个GC 线程进行重新标记(Stop The World),修正并发标记期间用户线程对对象引用的变动,停顿时间稍长但可控。
最后,启动多个用户线程和一个GC 线程,进行并发清除,清理不可达对象,清理完成后把GC线程进行重置。
CMS 的优点是以响应时间优先,停顿时间短,但也有两个缺点,一个是由于CMS采用“标记-清除”,会导致内存碎片积累,另一个是由于在并发清理过程中仍有用户线程运行,可能生成新的垃圾对象,需在下次 GC 处理。
第二个是 G1 收集器,G1(Garbage-First)收集器以控制 GC 停顿时间为目标,兼具高吞吐量和低延迟性能,适用于大内存、多核环境。其基于“标记-整理”和“标记-复制算法”,回收流程包括以下阶段:
首先,停止所有用户线程,启用一个GC线程进行初始标记(Stop The World),标记从 GC Roots 可达的对象,时间短。
其次,让用户线程和一个GC 线程并发工作,用GC 线程进行并发标记,分析整个堆中对象的存活情况。
然后,停止所有用户线程,让多个GC 线程进行最终标记(Stop The World),修正并发标记阶段产生的引用变动,识别即将被回收的对象。
最后,让多个GC 线程进行筛选回收,根据收集时间预算,优先回收回收价值最高的 Region。回收完成后把GC线程进行重置。这是 G1 的核心优化,基于堆分区,将回收工作集中于垃圾最多的区域,避免全堆扫描。
G1 具有三个优点,
其一,将堆内存划分为多个 Region,可分别执行标记、回收,提升效率。
第二,采用“标记-整理”和“标记-复制”,实现内存紧凑化。
第三,方便控制停顿时间,通过后台维护的优先队列,动态选择高价值 Region,极大减少了全堆停顿的频率。
但G1缺点是:调优复杂,对硬件资源要求较高。
延伸
1.除了CMS和G1外,还有哪些垃圾回收器
除了 CMS(并发标记清除)和 G1(Garbage First)之外,JVM 还提供了其他几种垃圾回收器,主要包括 Serial、Parallel、ZGC等。
(1)Serial 垃圾回收器
特点:单线程回收,适用于 单核 CPU 以及 小型 Java 应用。在 GC 过程中会 Stop-The-World(STW),即暂停所有应用线程,影响响应时间。
适用场景:适用于单 CPU 机器,堆内存较小(如 100MB~2GB)。主要用于桌面应用或测试环境。
优缺点: 实现简单,额外开销小;单线程垃圾回收,STW 时间较长,不适用于高并发环境。
使用参数:-XX:+UseSerialGC
(2)Parallel 垃圾回收器(吞吐量优先)
特点:多线程并行进行垃圾回收,提高 GC 效率。吞吐量优先:适用于批量任务处理,GC 期间仍然会 STW。适用于 多核 CPU 环境,可以最大化 CPU 资源利用率。
适用场景:高吞吐量需求的应用(如大数据计算、离线任务)。适用于 堆内存中等(几 GB 级别)的服务器端应用。
优缺点:多线程回收,吞吐量高,适合 批处理任务。GC 期间仍然会导致 较长的暂停时间,不适用于 低延迟应用。
使用参数:-XX:+UseParallelGC
(3)ZGC(低延迟垃圾回收器)
特点:目标是超低延迟,最大 GC 停顿时间 <1ms。支持超大堆内存(最大 16TB),适用于大规模服务。并发执行大部分 GC 任务,减少 STW 时间。
适用场景:适用于低延迟应用(如金融、游戏、交易系统)。大堆内存(数百 GB 级别) 的应用。
优缺点:GC 过程 几乎无感知,适用于 高响应要求场景。相比 G1,CPU 开销稍高,仍在优化中。
使用参数:-XX:+UseZGC
七 什么是指令重排序?
指令重排序(Instruction Reordering)是指在程序执行过程中,处理器根据一定的优化规则,改变指令的执行顺序,但不改变指令的执行结果。指令重排序通常发生在编译器、处理器或 JVM 的优化过程中,它的目的是提高程序执行效率,如减少 CPU 等资源的等待时间。
在 Java 的并发编程中,指令重排序是一个重要的概念,因为它可能导致多线程环境下的程序行为不符合预期,进而引发 竞态条件、数据不一致 等问题。
延伸
1.指令重排序的原因
编译优化:为了提高程序的性能,编译器可以对指令顺序进行优化。例如,编译器可能会将某些不相干的指令交换位置,以提高 CPU 的流水线效率。
硬件优化:现代 CPU 具有指令流水线(Pipeline)和乱序执行(Out-of-Order Execution)等优化特性,这使得它们在执行指令时可能会改变指令的顺序,尽管逻辑顺序未被改变。
JVM 优化:在 Java 中,JVM 在执行字节码时,也可能进行一些优化,比如将某些指令重新排序,以提高执行效率。
2.指令重排序有哪些类型?解释一下过程?
指令重排序主要有两种类型:编译器重排序和处理器重排序。
编译器重排序:编译器在生成机器代码时可能会对指令进行重排序,目的是为了优化代码的执行效率。它主要发生在程序的编译阶段,在不改变程序最终行为的前提下,调整指令的顺序。
处理器重排序:处理器会在执行过程中对指令进行乱序执行,以提高CPU的执行效率。这通常发生在硬件层面,尤其是为了利用指令流水线和并行处理能力。
3.如何阻止指令重排序?
阻止指令重排序的方法主要有两种:使用 volatile
关键字和使用 synchronized
关键字。
使用
volatile
关键字:声明一个变量为volatile,可以防止该变量的值在不同线程之间发生重排序。volatile确保对该变量的写操作对所有线程都是可见的,并且禁止对该变量的重排序。使用
synchronized
关键字:通过synchronized
来同步代码块或方法,保证在一个线程中执行完同步代码后,其他线程才能执行相同的同步代码,从而防止指令重排序。