一、小林-JVM面试题
1、创建对象的过程?
在Java中创建对象的过程包括以下几个步骤:
- 类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
- 分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
- 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
- 进行必要设置,比如对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
- 执行 init 方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始——构造函数,即class文件中的方法还没有执行,所有的字段都还为零,对象需要的其他资源和状态信息还没有按照预定的意图构造好。所以一般来说,执行 new 指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全被构造出来。
2、对象的生命周期
对象的生命周期包括创建、使用和销毁三个阶段:
- 创建:对象通过关键字new在堆内存中被实例化,构造函数被调用,对象的内存空间被分配。
- 使用:对象被引用并执行相应的操作,可以通过引用访问对象的属性和方法,在程序运行过程中被不断使用。
- 销毁:当对象不再被引用时,通过垃圾回收机制自动回收对象所占用的内存空间。垃圾回收器会在适当的时候检测并回收不再被引用的对象,释放对象占用的内存空间,完成对象的销毁过程。
3、讲一下类加载过程?
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括以下 7 个阶段:
-
加载:通过类的全限定名(包名 + 类名),获取到该类的.class文件的二进制字节流,将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构,在内存中生成一个代表该类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口
-
连接:验证、准备、解析 3 个阶段统称为连接。
- 验证:确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全。验证阶段大致会完成以下四个阶段的检验动作:文件格式校验、元数据验证、字节码验证、符号引用验证
- 准备:为类中的静态字段分配内存,并设置默认的初始值,比如int类型初始值是0。被final修饰的static字段不会设置,因为final在编译的时候就分配了
- 解析:解析阶段是虚拟机将常量池的「符号引用」直接替换为「直接引用」的过程。符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候可以无歧义地定位到目标即可。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用是和虚拟机实现的内存布局相关的。如果有了直接引用, 那引用的目标必定已经存在在内存中了。
-
初始化:初始化是整个类加载过程的最后一个阶段,初始化阶段简单来说就是执行类的构造器方法,要注意的是这里的构造器方法()并不是开发者写的,而是编译器自动生成的。
-
使用:使用类或者创建对象
-
卸载:如果有下面的情况,类就会被卸载:1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。2. 加载该类的ClassLoader已经被回收。 3. 类对应的Java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
4、讲一下类的加载和双亲委派原则
我们把 Java 的类加载过程分为三个主要步骤:加载、链接、初始化。
首先是加载阶段(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。
加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
第二阶段是链接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程中。这里可进一步细分为三个步骤:
- 验证(Verification),这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
- 准备(Preparation),创建类或接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。
- 解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。
最后是初始化阶段(initialization),这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
再来谈谈双亲委派模型,简单说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。
5、什么是Java里的垃圾回收?如何触发垃圾回收?
垃圾回收(Garbage Collection, GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。垃圾回收可以通过多种方式触发,具体如下:
- 内存不足时:当JVM检测到堆内存不足,无法为新的对象分配内存时,会自动触发垃圾回收。
- 手动请求:虽然垃圾回收是自动的,开发者可以通过调用
System.gc()或Runtime.getRuntime().gc()建议 JVM 进行垃圾回收。不过这只是一个建议,并不能保证立即执行。 - JVM参数:启动 Java 应用时可以通过 JVM 参数来调整垃圾回收的行为,比如:
-Xmx(最大堆大小)、-Xms(初始堆大小)等。 - 对象数量或内存使用达到阈值:垃圾收集器内部实现了一些策略,以监控对象的创建和内存使用,达到某个阈值时触发垃圾回收。
6、判断垃圾的方法有哪些?
在Java中,判断对象是否为垃圾(即不再被使用,可以被垃圾回收器回收)主要依据两种主流的垃圾回收算法来实现:引用计数法和可达性分析算法。
引用计数法(Reference Counting)
- 原理:为每个对象分配一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1。当计数器为0时,表示对象不再被任何变量引用,可以被回收。
- 缺点:不能解决循环引用的问题,即两个对象相互引用,但不再被其他任何对象引用,这时引用计数器不会为0,导致对象无法被回收。
可达性分析算法(Reachability Analysis)
Java虚拟机主要采用此算法来判断对象是否为垃圾。
- 原理:从一组称为GC Roots(垃圾收集根)的对象出发,向下追溯它们引用的对象,以及这些对象引用的其他对象,以此类推。如果一个对象到GC Roots没有任何引用链相连(即从GC Roots到这个对象不可达),那么这个对象就被认为是不可达的,可以被回收。GC Roots对象包括:虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、本地方法栈中JNI(Java Native Interface)引用的对象、活跃线程的引用等。
7、垃圾回收算法是什么,是为了解决了什么问题?
JVM有垃圾回收机制的原因是为了解决内存管理的问题。在传统的编程语言中,开发人员需要手动分配和释放内存,这可能导致内存泄漏、内存溢出等问题。而Java作为一种高级语言,旨在提供更简单、更安全的编程环境,因此引入了垃圾回收机制来自动管理内存。
垃圾回收机制的主要目标是自动检测和回收不再使用的对象,从而释放它们所占用的内存空间。这样可以避免内存泄漏(一些对象被分配了内存却无法被释放,导致内存资源的浪费)。同时,垃圾回收机制还可以防止内存溢出(即程序需要的内存超过了可用内存的情况)。
通过垃圾回收机制,JVM可以在程序运行时自动识别和清理不再使用的对象,使得开发人员无需手动管理内存。这样可以提高开发效率、减少错误,并且使程序更加可靠和稳定。
8、垃圾回收算法有哪些?
- 标记-清除算法:标记-清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记-清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。
- 复制算法:为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足。
- 标记-整理算法:复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了“标记-整理算法”。标记-整理算法的“标记”过程与“标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。
- 分代回收算法:分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。
9、垃圾回收器有哪些?
- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
- Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代
10、垃圾回收算法哪些阶段会stop the world?
标记-复制算法应用在CMS新生代(ParNew是CMS默认的新生代垃圾回收器)和G1垃圾回收器中。标记-复制算法可以分为三个阶段:
- 标记阶段,即从GC Roots集合开始,标记活跃对象;
- 转移阶段,即把活跃对象复制到新的内存地址上;
- 重定位阶段,因为转移导致对象的地址发生了变化,在重定位阶段,所有指向对象旧地址的指针都要调整到对象新的地址上。
下面以G1为例,通过G1中标记-复制算法过程(G1的Young GC和Mixed GC均采用该算法),分析G1停顿耗时的主要瓶颈。G1垃圾回收周期如下图所示:
G1的混合回收过程可以分为标记阶段、清理阶段和复制阶段。
标记阶段停顿分析
- 初始标记阶段:初始标记阶段是指从GC Roots出发标记全部直接子节点的过程,该阶段是STW的。由于GC Roots数量不多,通常该阶段耗时非常短。
- 并发标记阶段:并发标记阶段是指从GC Roots开始对堆中对象进行可达性分析,找出存活对象。该阶段是并发的,即应用线程和GC线程可以同时活动。并发标记耗时相对长很多,但因为不是STW,所以我们不太关心该阶段耗时的长短。
- 再标记阶段:重新标记那些在并发标记阶段发生变化的对象。该阶段是STW的。
清理阶段停顿分析
- 清理阶段清点出有存活对象的分区和没有存活对象的分区,该阶段不会清理垃圾对象,也不会执行存活对象的复制。该阶段是STW的。
复制阶段停顿分析
- 复制算法中的转移阶段需要分配新内存和复制对象的成员变量。转移阶段是STW的,其中内存分配通常耗时非常短,但对象成员变量的复制耗时有可能较长,这是因为复制耗时与存活对象数量与对象复杂度成正比。对象越复杂,复制耗时越长。
四个STW过程中,初始标记因为只标记GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。转移阶段要处理所有存活的对象,耗时会较长。
因此,G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW。
11、minorGC、majorGC、fullGC的区别,什么场景触发full GC
在Java中,垃圾回收机制是自动管理内存的重要组成部分。根据其作用范围和触发条件的不同,可以将GC分为三种类型:Minor GC(也称为Young GC)、Major GC(有时也称为Old GC)、以及Full GC。以下是这三种GC的区别和触发场景:
Minor GC (Young GC)
- 作用范围:只针对年轻代进行回收,包括Eden区和两个Survivor区(S0和S1)。
- 触发条件:当Eden区空间不足时,JVM会触发一次Minor GC,将Eden区和一个Survivor区中的存活对象移动到另一个Survivor区或老年代(Old Generation)。
- 特点:通常发生得非常频繁,因为年轻代中对象的生命周期较短,回收效率高,暂停时间相对较短。
Major GC
- 作用范围:主要针对老年代进行回收,但不一定只回收老年代。
- 触发条件:当老年代空间不足时,或者系统检测到年轻代对象晋升到老年代的速度过快,可能会触发Major GC。
- 特点:相比Minor GC,Major GC发生的频率较低,但每次回收可能需要更长的时间,因为老年代中的对象存活率较高。
Full GC
-
作用范围:对整个堆内存(包括年轻代、老年代以及永久代/元空间)进行回收。
-
触发条件:
- 直接调用
System.gc()或Runtime.getRuntime().gc()方法时,虽然不能保证立即执行,但JVM会尝试执行Full GC。 - Minor GC(新生代垃圾回收)时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发Full GC,对整个堆内存进行回收。
- 当永久代(Java 8之前的版本)或元空间(Java 8及以后的版本)空间不足时。
- 直接调用
-
特点:Full GC是最昂贵的操作,因为它需要停止所有的工作线程(Stop The World),遍历整个堆内存来查找和回收不再使用的对象,因此应尽量减少Full GC的触发。
12、垃圾回收器 CMS 和 G1的区别?
区别一:使用的范围不一样:
- CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
- G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
区别二:STW的时间:
- CMS收集器以最小的停顿时间为目标的收集器。
- G1收集器可预测垃圾回收 (opens new window)的停顿时间(建立可预测的停顿时间模型)
区别三: 垃圾碎片
- CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
- G1收集器使用的是“标记-整理”算法,进行了空间整合,没有内存空间碎片。
区别四: 垃圾回收的过程不一样
注意这两个收集器第四阶段得不同
区别五: CMS会产生浮动垃圾
- CMS产生浮动垃圾过多时会退化为serial old,效率低,因为在上图的第四阶段,CMS清除垃圾时是并发清除的,这个时候,垃圾回收线程和用户线程同时工作会产生浮动垃圾,也就意味着CMS垃圾回收器必须预留一部分内存空间用于存放浮动垃圾
- 而G1没有浮动垃圾,G1的筛选回收是多个垃圾回收线程并行gc的,没有浮动垃圾的回收,在执行‘并发清理’步骤时,用户线程也会同时产生一部分可回收对象,但是这部分可回收对象只能在下次执行清理是才会被回收。如果在清理过程中预留给用户线程的内存不足就会出现‘Concurrent Mode Failure’,一旦出现此错误时便会切换到SerialOld收集方式。
13、什么情况下使用CMS,什么情况使用G1?
CMS适用场景:
- 低延迟需求:适用于对停顿时间要求敏感的应用程序。
- 老生代收集:主要针对老年代的垃圾回收。
- 碎片化管理:容易出现内存碎片,可能需要定期进行Full GC来压缩内存空间。
G1适用场景:
- 大堆内存:适用于需要管理大内存堆的场景,能够有效处理数GB以上的堆内存。
- 对内存碎片敏感:G1通过紧凑整理来减少内存碎片,降低了碎片化对性能的影响。
- 比较平衡的性能:G1在提供较低停顿时间的同时,也保持了相对较高的吞吐量。
14、G1回收器的特色是什么?
G1 的特点:
- G1最大的特点是引入分区的思路,弱化了分代的概念。
- 合理利用垃圾收集各个周期的资源,解决了其他收集器、甚至 CMS 的众多缺陷
G1 相比较 CMS 的改进:
- 算法: G1 基于标记--整理算法, 不会产生空间碎片,在分配大对象时,不会因无法得到连续的空间,而提前触发一次 FULL GC 。
- 停顿时间可控: G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象。
- 并行与并发:G1 能更充分的利用 CPU 多核环境下的硬件优势,来缩短 stop the world 的停顿时间。
15、GC只会对堆进行GC吗?
JVM 的垃圾回收器不仅仅会对堆进行垃圾回收,它还会对方法区进行垃圾回收。
- 堆(Heap): 堆是用于存储对象实例的内存区域。大部分的垃圾回收工作都发生在堆上,因为大多数对象都会被分配在堆上,而垃圾回收的重点通常也是回收堆中不再被引用的对象,以释放内存空间。
- 方法区(Method Area): 方法区是用于存储类信息、常量、静态变量等数据的区域。虽然方法区中的垃圾回收与堆有所不同,但是同样存在对不再需要的常量、无用的类信息等进行清理的过程。
二、代码随想录-SpringBoot面试题
1、SpringBoot常用注解
2、SpringBoot自动装配
3、RPC框架
3.1 Dubbo流程
3.2 RPC使用了什么技术
4、Redis是单线程吗?如何支持高并发
5、缓存穿透,缓存雪崩,缓存击穿
6、MQ消息如何保证不丢失
7、MQ消息的顺序性
三、代码随想录-MySQL面试题
1、锁的划分
2、日志
2.1 undo和redo
2.2 binlog
2.3 为什么需要两阶段提交
3、幻读
4、事务如何实现MVCC
5、脏页
6、空洞count
四、面试指北-Netty面试题
1、BIO,NIO,AIO区别
2、为什么用Netty
3、Netty应用场景
4、Netty核心组件
4.1 Bytebuf
4.2 Channel
4.3 EventLoop
1. Channel(通道)
- 作用:Netty 中所有 I/O 操作(如读写数据)都通过
Channel进行,它是对网络连接的抽象(类似 Java NIO 的SocketChannel)。 - 关键方法:
read()/write():异步读写数据。close():关闭通道。config():获取/设置通道配置参数。
- 类型:
NioSocketChannel(TCP 客户端)、NioServerSocketChannel(TCP 服务端)等。
2. EventLoop(事件循环)
- 作用:单线程处理 I/O 事件(如连接建立、数据读写),通过多路复用技术(如 NIO 的
Selector)高效管理多个通道。 - 核心特性:
- 非阻塞:一个线程可处理多个通道事件。
- 任务队列:通过
EventLoopExecutor执行耗时操作(如编解码)。
- 关联类:
EventLoopGroup:管理多个EventLoop(如服务端通常用两个线程组:bossGroup处理连接,workerGroup处理数据)。
3. ChannelPipeline(管道)
- 作用:数据在
Channel中流动时需经过一系列处理,ChannelPipeline是一个链表结构,每个节点称为ChannelHandler。 - 关键概念:
- 入站处理:
ChannelInboundHandler(如SimpleChannelInboundHandler)处理接收到的数据。 - 出站处理:
ChannelOutboundHandler(如ChannelOutboundBuffer)处理要发送的数据。 - 动态添加/删除 Handler:运行时可通过
pipeline.addLast()或remove()修改处理逻辑。
- 入站处理:
4. ChannelHandlerContext(上下文)
- 作用:
ChannelHandler在ChannelPipeline中的上下文,用于:- 访问相邻的 Handler(如
ctx.fireChannelRead()触发下一个入站处理器)。 - 获取
Channel、EventLoop等组件。 - 执行异步操作(如
ctx.writeAndFlush())。
- 访问相邻的 Handler(如
5. Bootstrap & ServerBootstrap
- 作用:简化客户端/服务端的启动配置。
- 区别:
- Bootstrap:客户端使用,配置
ChannelInitializer初始化管道。 - ServerBootstrap:服务端使用,额外配置
childHandler处理子节点(客户端连接)。
- Bootstrap:客户端使用,配置
6. ByteBuf(字节缓冲区)
- 作用:Netty 自带的动态数组,替代 Java NIO 的
ByteBuffer,支持快速读写和零拷贝。 - 核心特性:
- 池化:通过
ByteBufAllocator可复用内存,减少 GC 压力。 - 操作链:
readInt()、writeBytes()等方法链式调用。 - 引用计数:自动管理内存释放(通过
ReferenceCounted)。
- 池化:通过
7. Handler(处理器)
- 分类:
- 入站处理器(
ChannelInboundHandler):处理接收到的数据(如channelRead())。 - 出站处理器(
ChannelOutboundHandler):处理待发送的数据(如write())。 - 通用处理器(如
ChannelInitializer初始化管道)。
- 入站处理器(
8. 组件协作流程(以服务端为例)
- ServerBootstrap 启动,绑定端口并监听连接。
- bossGroup 的 EventLoop 接收客户端连接请求,创建新
Channel。 - 新
Channel交给workerGroup的 EventLoop 处理。 - 数据通过
ChannelPipeline的 Handler 链(入站→业务逻辑→出站)处理。 - 最终通过
EventLoop异步写入网络。
9. 总结
| 组件 | 核心职责 | 典型场景 |
|---|---|---|
| Channel | 网络 I/O 操作抽象 | TCP/UDP 通信 |
| EventLoop | 事件循环与线程模型 | 多路复用、非阻塞 I/O |
| ChannelPipeline | 数据处理流水线 | 编解码、业务逻辑链式处理 |
| ByteBuf | 高效字节缓冲区管理 | 减少内存拷贝、提升性能 |