Java面试题——JVM(二)

JVM

96、请分析新生代GC、老年代GC和完全GC的特点

在Java堆内存中,分代垃圾回收是一种常用的垃圾回收方式。其中,新生代主要负责存储短期存活的对象,老年代则主要存储长期存活的对象。新生代GC、老年代GC和完全GC是三种不同的GC方式,它们的特点如表所示。

97、请分析JDK预设的垃圾回收器

JDK预设的垃圾回收器的特点如表所示。

垃圾回收器的性能评估指标如表所示。

98、请分析G1垃圾回收器

在JDK 17中,G1垃圾回收器是默认的垃圾回收器。G1垃圾回收器在JDK 7中首次被引入,并在随后的JDK版本中进行了改进和优化。G1垃圾回收器是一个高效的垃圾回收器,其特点是可以高效地管理大内存、具有可预测的停顿时间和高吞吐量。G1垃圾回收器将堆内存划分为数个大小不等的区域,每个区域都会被动态地标记为存活对象或垃圾对象。在进行GC时,G1垃圾回收器会首先选择包含垃圾对象最多的那些区域进行回收。在回收这些区域时,G1垃圾回收器采用了标记-整理算法,将存活对象移动到其他区域中,并同时清理掉垃圾对象。由于G1垃圾回收器是并行垃圾回收器,因此它在进行垃圾回收时可以充分利用多线程的优势,从而提高垃圾回收效率。

G1垃圾回收器的一个重要特点是具有可预测的停顿时间,它通过对堆内存划分和垃圾回收过程的优化,控制GC中的停顿时间。通过设置最大停顿时间,开发者可以避免在应用程序中出现过长的GC停顿时间,从而提高应用程序的响应性能和可靠性。

G1垃圾回收器适用于需要管理大规模内存且对GC停顿时间敏感的应用程序,例如金融交易、在线游戏等高并发场景。G1垃圾回收器将整个Java堆内存划分为多个动态大小的区域,G1垃圾回收器的内存区域如表所示。

99、请分析ZGC垃圾回收器

JDK 11中引入了ZGC垃圾回收器,它是一种基于无锁、分区的低延迟垃圾回收器。ZGC垃圾回收器在JDK 11中是作为实验性功能引入的,并在随后的JDK版本中进行了改进和优化。ZGC垃圾回收器的优势如表所示。

G1垃圾回收器和ZGC垃圾回收器都是JDK中的低延迟垃圾回收器,它们的区别主要体现在堆分布、垃圾回收算法、暂停时间、可靠性、应用场景等方面,具体如表所示。

100、请分析CMS垃圾回收器

CMS垃圾回收器是一种使用并发算法进行内存管理的垃圾回收器,它旨在减少应用程序中的垃圾收集暂停时间。CMS垃圾回收器采用并发进行的标记-清除算法,即CMS对堆内存的扫描和标记过程是和应用程序一起运行的,以最大限度地减少应用程序被挂起的时间。CMS垃圾回收的阶段的具体描述如表所示。

CMS垃圾回收器的优缺点如表所示。

101、请分析查看GC日志的方法

JVM的GC日志是分析JVM垃圾回收性能的重要工具,查看GC日志的方法如下。

首先,启用JVM的GC日志。可以通过设置以下JVM参数来启用GC日志。

bash 复制代码
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:<file-path>

JVM的参数如表所示。

然后,使用一些工具来对GC日志进行分析,从而了解应用程序的垃圾回收情况。通常可以使用jstat工具或VisualGC工具,如果使用jstat工具,可以使用以下命令来查看GC相关信息。

bash 复制代码
jstat -gc [pid] [interval] [count]

jstat工具的参数如表所示。

接下来,jstat将输出包含GC统计数据的表格。例如,输入以下命令,以5秒的间隔执行5次GC监控。

bash 复制代码
jstat -gc <pid> 5000 5

最后,在VisualVM中选择VisualGC视图即可显示GC日志详细信息。

102、请分析CPU缓存的特性

在Java代码执行时,CPU缓存与内存之间会发生频繁的交互。CPU缓存是一个高速缓存池,用于临时存储数据,以加快访问速度;而内存则是一个存储相对较慢的存储介质,主要用于长期存储数据。CPU缓存以缓存行为基本单位进行管理,每个缓存行的大小通常为64字节。当CPU访问特定地址的数据时,可能会将该数据及其相邻的一段内存区域加载到缓存行中。将这些数据缓存在CPU缓存中可以大大加快程序的查询和操作速度。CPU缓存的特性如表所示。

103、请分析JVM中常见的CPU指令和内存屏障

JVM中的CPU指令是计算机规定执行操作的类型和操作数的基本命令,它规定了CPU能够执行的操作。常见的CPU指令如表所示。

JVM中的内存屏障是一组特殊的CPU指令,JVM利用这些CPU指令在多线程环境下保证线程的有序性和线程间的可见性,防止出现线程之间的重排序和指令过早执行等问题。常见的内存屏障如表所示。

104、请分析内核线程和用户线程

内核线程是由操作系统内核创建和管理的,它能够独立于任何用户进程运行,并使用系统资源。与用户进程中的线程相比,内核线程通常运行在内核态,具有更高的权限和访问级别,可以直接访问系统资源。内核线程的调度由操作系统内核负责,与用户进程中的线程相比,其调度更为复杂,需要考虑多个因素,如线程优先级、阻塞状态、就绪队列等。

用户线程是在用户进程中创建和管理的,它运行在操作系统内核以外的用户空间中。与内核线程相比,用户线程运行在用户态,权限和访问级别较低,无法直接访问系统资源。用户线程的调度由用户进程负责,在不同的用户线程之间进行切换,以实现多任务并发执行。内核线程调度算法复杂,而用户线程的调度则相对简单,通常采用时间片轮转或优先级调度算法。

内核线程和用户线程的区别如表所示。

105、请分析Java线程调度的方式

Java线程调度是指通过一定的算法和策略来决定哪个线程在什么时候运行。Java线程调度方式如表所示。

抢占式线程调度算法如表所示。

106、请分析JVM即时编译

JVM即时编译是Java虚拟机中的一种优化技术,用于将Java字节码即时编译为本地机器码并执行,从而提高程序的执行效率。在Java 8中引入了基于Graal的全新即时编译器,它是一种性能强大、高度可扩展的即时编译器,能够提供更快的启动速度、更短的垃圾回收时间和更高的吞吐量,即时编译器的特性如表所示。

即时编译器可以显著提高Java程序的执行效率,特别是对于需要频繁执行的代码块,性能提升尤为明显。与传统的解释执行相比,即时编译器能够大幅减少解释执行的开销,使代码的运行速度提高数倍甚至数十倍。然而,即时编译器也存在一些缺点。例如,在程序首次运行时,由于需要即时编译字节码,可能会需要较长的启动时间;同时,即时编译器也会占用一定的内存资源。为了平衡编译时间和执行效率,JVM通常采用分层编译、延迟编译等技术进行优化。因此,在使用即时编译器时,需要根据实际情况进行权衡和调整,以充分发挥其优势并最小化其缺点。

107、请分析JVM栈上分配和逃逸分析

JVM栈上分配是指在Java虚拟机的栈空间上为局部变量(即方法内部定义的变量)分配内存空间,而不是通过new等关键字在堆上分配内存。这种分配方式在方法结束时会自动释放内存,因此可以有效地减轻垃圾回收器的负担,提高应用程序的性能。

具体来说,JVM栈上分配的实现方式是:在方法调用时,JVM会为每个线程创建一个栈帧,栈帧包含了局部变量表、操作数栈等信息,当方法内部需要使用一个局部变量时,就直接在局部变量表中快速分配内存空间。这种分配不需要垃圾回收,因为这个局部变量是一个非逃逸对象,分配的内存空间是该对象的临时内存,在方法结束时将自动释放。然而,JVM栈上分配并不适用于所有情况。比如,如果需要将一个局部变量的生命周期延长到方法外部,就必须在堆上为这个变量分配内存空间,这称为对象的逃逸。此外,栈上分配也存在栈溢出的风险,因此需要谨慎使用。

逃逸分析是一种对Java程序进行静态分析的技术,用于分析对象的作用域,确定对象是否存在逃逸行为。逃逸分析的主要目的是优化对象的内存分配,尽可能地将对象分配到栈上,以减少堆内存的占用和垃圾回收的开销。

逃逸分析的基本原理是:一个对象在方法中创建后,如果它的引用仅限于该方法内部,那么就可以将该对象分配到栈上,这样的对象称为非逃逸对象。非逃逸对象的创建和回收更加高效,因为它们不需要垃圾回收器的介入。逃逸分析通常在即时编译过程中完成,其具体实现步骤如表所示。

注意,逃逸分析也存在一定的局限性,例如静态变量、非线程本地变量等情况下,逃逸分析无法进行优化,因此需要结合实际情况使用。

108、请分析JVM方法内联

JVM方法内联是一种优化技术,其主要目的是减少函数调用和栈帧的创建、销毁等操作,从而提高程序的执行效率。这一优化技术通过将调用方法的代码直接替换为被调用方法的代码来实现。

JVM方法内联特别适用于那些被频繁调用、代码量小且逻辑简单的方法,如getter()、setter()方法。这些方法通常执行速度快,但函数调用开销相对较高。通过JVM方法内联可以将这些方法的代码直接嵌入到调用处,从而消除函数调用带来的开销,使代码更加紧凑和高效。

然而,JVM方法内联并非没有代价。JVM方法内联会增加代码的体积,可能导致编译时间延长,并且在某些情况下可能引发代码缓存不足的问题。此外,JVM并不总是能够对所有方法进行内联优化,特别是当调用的方法包含复杂控制流程(如循环、递归等)时,内联的效果可能会被削弱。因此,在实际应用中,开发者需要根据具体情况进行权衡和调整,以确保内联优化能够带来实际的性能提升。

109、请分析JVM锁消除

JVM锁消除是指在即时编译过程中,对于一些不可能存在竞争的锁进行消除的优化技术。通过静态分析代码,JVM可以判断出某些锁对象只会被单线程访问,或者根本不存在锁对象所保护的共享资源,因此可以安全地将这些锁消除掉,从而提高程序的执行效率。

JVM锁消除的实现基于逃逸分析技术。逃逸分析技术能够分析出对象的作用域和生命周期,并推断出哪些对象可能会逃逸到方法之外。如果一个对象不会逃逸出当前方法,那么它就可以被视为局部变量。此时,如果这个对象被用来进行同步,那么它就可以被视为私有对象,不需要进行加锁和解锁操作。即时编译器因此可以将volatile变量或同步块优化成非同步的代码,从而达到锁消除的目的。

JVM锁消除的优点是可以显著减少锁竞争的次数,从而提高程序的执行效率和并发性能。然而,逃逸分析过程本身也可能会带来一定的性能开销,影响程序的启动速度和编译时间。

注意,JVM锁消除是一种自动优化技术,它并不能保证一定能够消除所有无用的锁。在实际应用中,由于误判或代码变更等原因,锁消除可能不准确或产生副作用。因此,开发者需要根据具体情况进行评估,并结合其他优化技术来提高程序的性能。

110、请分析JVM锁粗化

JVM锁粗化是一种优化策略,用于减少同步操作的开销。当多个连续的操作需要对同一资源加锁时,JVM可能会将这些操作合并成一个更大的操作,只在开始和结束时加锁和解锁。这样可以避免频繁地获取和释放锁,减少上下文切换的次数,从而提高程序的执行效率。JVM锁粗化特别适用于锁持有时间短但调用频繁的情况,例如在循环中对同一对象多次加锁。然而,过度的锁粗化也可能导致其他线程等待时间过长,影响并发性能。因此,JVM会根据运行时的具体情况,如锁的竞争程度和持有锁所需的时间,智能地调整锁的粒度,以达到性能与并发性的最佳平衡。这种优化通常由JVM的即时编译器自动完成,无须开发者显式参与。JVM锁粗化的过程如表所示。

111、请分析JVM偏向锁

JVM偏向锁是Java虚拟机中的一种优化技术,其基本思想是在程序刚启动时,并没有其他线程竞争同步资源,因此为了提高程序性能,可以将锁对象标记为可偏向状态。这样,当有一个线程访问该锁时,JVM会自动将该锁对象标记为偏向锁,并将该线程的ID记录在偏向锁中。当该线程再次访问该锁时,JVM会直接将锁授予该线程,无须执行任何额外的操作。

JVM偏向锁的使用能有效减少锁的竞争,从而提升程序的性能。然而,在锁竞争非常激烈的情况下,偏向锁会失效,因此它更适用于并发度较低的场景。同时,在大多数情况下,偏向锁对程序的响应时间影响较小,因为若有其他线程竞争同一个锁,偏向锁会自动失效,以便其他线程能及时获得锁。偏向锁的主要实现机制如表所示。

112、请分析JVM轻量级锁

JVM轻量级锁是一种轻量级的互斥机制,旨在优化线程在竞争同步资源时的性能损失。它主要基于CAS操作实现,避免了传统互斥机制的系统调用和内核态上下文切换,从而在低竞争情况下显著提升了程序的性能。轻量级锁的主要实现机制如表所示。

轻量级锁也有一些局限性,例如当对象被频繁地加锁和释放时,轻量级锁可能会变得不稳定,进而导致程序性能下降。

总的来说,轻量级锁适用于竞争资源较少、线程持有锁时间较短以及对象主要由单个线程访问的场景。在高竞争场景下,轻量级锁可能并不适用,因为线程的自旋等待会占用大量CPU时间,反而可能导致程序性能下降。

113、请分析JVM守护线程的作用

JVM守护线程是一种特殊的线程,其主要用于在程序运行时提供后台服务。与普通线程不同,守护线程不会阻止JVM的退出。只要所有用户线程都已经结束,无论守护线程是否存活,JVM都会立即退出。JVM守护线程的主要作用如表所示。

在Java中,可以通过继承Thread类或实现Runnable接口来创建自定义线程。要创建一个自定义的守护线程,只需将线程的daemon属性设置为true。需注意的是,守护线程在所有非守护线程结束时会自动终止,因此它们通常用于执行后台任务或提供辅助功能,而非关键业务逻辑。

114、请分析JVM字符串去重的原理

在JVM中,字符串是被字符串常量池管理的对象。当创建一个字符串时,JVM会先检查这个字符串是否已经存在于字符串常量池中。如果已经存在,则返回字符串常量池中的对象引用;否则,创建新的字符串对象并将其添加到字符串常量池中。由于字符串的不可变性和字符串常量池的特点,JVM会对重复的字符串进行去重处理,即相同的字符串引用会指向同一个字符串对象。

字符串常量池是JVM中用于存储字符串对象的数据结构,它实际上是一个哈希表,将字符串的哈希值作为键,将具有相同哈希值的字符串对象作为值存储在一起。

JVM在启动时会创建一个默认大小(默认大小可能因JVM版本而异)的字符串常量池,这个大小可以通过命令行参数-XX:StringTableSize来进行调整。每个字符串对象在被加入到字符串常量池时,都会计算出它的哈希值和索引值。JVM在需要使用字符串对象时,会首先在字符串常量池中查找是否存在相同的字符串对象;如果存在,则返回该对象的引用;否则,会在找到的空槽中创建新的字符串对象并将其加入到字符串常量池中,然后返回新创建对象的引用。

字符串常量池的大小通常选择为一个质数,因为哈希表中的散列函数使用质数可以获得更好的分布效果。设置-XX:StringTableSize参数的大小将直接影响字符串常量池的占用内存和性能:如果字符串常量池太小,可能会导致哈希冲突增多,从而影响字符串对象的查找性能;如果字符串常量池太大,则会占用更多的内存空间,进而影响JVM的整体性能和响应速度。

当需要添加字符串对象到字符串常量池时,JVM会先计算字符串的哈希值,并根据哈希值计算出该字符串在字符串常量池中的索引值。然后,从该索引值开始向后遍历字符串常量池,直到遇到一个空槽或者相同的字符串对象为止。如果找到了相同的字符串对象,则直接返回该对象的引用;否则,在找到的空槽中创建新的字符串对象并将其加入到字符串常量池中。如果字符串常量池已经被填满,JVM可能会尝试进行字符串回收或扩容操作,以容纳更多的字符串对象。

115、请分析Java多线程死锁的原因

Java多线程中的死锁是指两个或多个线程在执行过程中,因互相等待对方释放资源而陷入阻塞的状态。Java多线程产生死锁的原因如表所示。

在编写多线程程序时,需要采取一些策略和技巧来尽量避免死锁的产生。例如,可以采用避免竞争资源的设计,或者确保所有线程以相同的顺序请求资源来避免死锁。此外,还可以利用一些工具来检测和避免死锁,如使用synchronized、ReentrantLock等同步工具进行加锁,并使用jconsole、jstack、jvisualvm等工具来监控和诊断程序运行中的死锁问题。

116、请分析Java SPI机制

Java SPI机制是一种面向接口编程的机制,它允许应用程序在运行时发现和加载某个特定接口的实现类,并调用对应的方法。这种机制使得应用程序更加灵活、可扩展和易于维护,同时也提高了代码的重用性和可移植性。

Java SPI机制的核心思想是通过在META-INF/services目录中的配置文件中声明服务接口的实现类来实现动态加载。具体来说,当应用程序需要使用某个服务接口时,会先获取该接口的所有实现类的名称列表,然后通过反射机制动态加载这些实现类,并执行对应的方法。

要使用Java SPI机制,需要遵循以下步骤:

  • (1)定义一个服务接口,通常这个接口定义在服务接口的jar包中,并由提供该服务的第三方组件实现;
  • (2)在jar包的META-INF/services目录下创建一个以服务的全限定名命名的文件;
  • (3)在该文件中列出服务接口的所有实现类的全限定名,每个实现类占一行,可以使用"#"或"!"开始的行作为注释;
  • (4)在应用程序中,通过ServiceLoader类获得该服务接口的所有实现类,并遍历这些实现类来使用对应的方法。

Java SPI机制的优缺点如表所示。

117、请分析Java中的值传递和指针传递

在Java中,参数传递方式只有值传递。无论是基本数据类型还是对象引用,实际上传递的都是副本,而不是内存地址或指针。

值传递是指将一个变量的值作为参数传递给方法,方法会创建一个新的本地变量来存储这个参数值的副本,因此在方法内部对这个本地变量的任何修改都不会影响原始变量的值。对于基本数据类型来说,传递的就是它们值的副本;而对于对象来说,传递的是对象引用的副本,即方法接收到的是对象引用的一个新副本,但这个副本仍然指向原来的对象。

指针传递是指将一个变量的内存地址作为参数传递给方法,方法可以通过这个地址直接访问和修改原始变量的值。在Java中,由于没有指向变量内存地址的指针,所以这种方式是不可行的。

虽然Java没有直接的指针传递方式,但可以通过传递对象引用来实现类似的效果。例如,当一个数组对象作为参数传递给一个方法时,方法接收到的是这个数组对象引用副本。通过这个引用副本,方法可以访问和修改数组内部的元素。然而,需要注意的是,即使在这种情况下,实质上也仍然是值传递,因为传递的是对象引用值的副本,而不是对象本身的内存地址。如果方法改变了这个对象的属性,原始对象的属性也会被修改,因为原始对象和传递给方法的对象引用副本都指向同一个对象实例。

相关推荐
CRMEB1 小时前
商城大促活动策划全流程:从定目标到复盘四阶段
java·开发语言·人工智能·ai·开源·php
denglinqings1 小时前
Java开发从入门到精通所有课程
java·开发语言
星空1 小时前
maven镜像
java·maven
程序猿阿森2 小时前
Python 闭包与装饰器:从入门到精通
开发语言·python·面试
余额瞒着我当琳2 小时前
C++--vector底层,leetcode118杨辉三角实战,对于传统reserve的坑点,迭代器失效
开发语言·c++
曹牧2 小时前
C#:字符串做20位截断
开发语言·c#
醉城夜风~2 小时前
(超详细)C++多态(Polymorphism)
开发语言·c++
这是个栗子2 小时前
【JS代码分析】前端鉴权基础:Token 的本地存储与状态重置实践
开发语言·前端·javascript
chuan.bai2 小时前
Java RAG 实战附录:qwen3 与 bge-m3 模型切换指南
java·人工智能·算法