楼主: I4n2w67Z61QH
131 0

[其他] Java常见面试题及答案汇总(2025最新版) [推广有奖]

  • 0关注
  • 0粉丝

等待验证会员

小学生

42%

还不是VIP/贵宾

-

威望
0
论坛币
0 个
通用积分
0
学术水平
0 点
热心指数
0 点
信用等级
0 点
经验
50 点
帖子
4
精华
0
在线时间
0 小时
注册时间
2018-6-23
最后登录
2018-6-23

楼主
I4n2w67Z61QH 发表于 2025-11-22 07:10:27 |AI写论文

+2 论坛币
k人 参与回答

经管之家送您一份

应届毕业生专属福利!

求职就业群
赵安豆老师微信:zhaoandou666

经管之家联合CDA

送您一个全额奖学金名额~ !

感谢您参与论坛问题回答

经管之家送您两个论坛币!

+2 论坛币

一、Java基础语法与核心特性

1. Java的核心特性有哪些?

Java语言具备多个关键特性,使其在企业级开发中广泛应用:

  • 跨平台性(Write Once, Run Anywhere):通过JVM实现。Java源码编译为字节码后,可在任何安装了JVM的操作系统上运行,无需重新编译。
  • 面向对象编程(OOP):支持封装、继承和多态三大基本特征,有助于构建模块化、可维护的代码结构。
  • 安全性机制:提供沙箱执行环境、字节码验证以及细粒度权限控制(如文件读写限制),保障程序运行安全。
  • 健壮性设计:通过自动垃圾回收(GC)防止内存泄漏;结合强类型检查与完善的异常处理机制,降低运行时错误概率。
  • 分布式支持:内置对RMI(远程方法调用)和HTTP协议的支持,便于构建跨网络的应用系统。
  • 多线程能力:语言层面集成多线程API,简化并发编程模型,提升程序性能与响应速度。

2. 基本数据类型与包装类的区别

对比维度 基本数据类型(如int、float) 包装类(如Integer、Float)
本质 原始值类型,不具有对象属性 引用类型,直接或间接继承自Object类
默认值 存在明确初始值(例如int为0,boolean为false) 默认为null,表示未初始化状态
使用场景 适用于局部变量、简单运算等高性能需求场合 用于集合类(如List)、泛型操作或需要表达“无值”语义的情境
缓存机制 无缓存功能 部分类型拥有常量池优化,如Integer[-128~127]、Byte、Short等范围内的值会被复用

关键知识点说明:

  • 自动装箱与拆箱:从Java 5开始引入的语言特性,编译器能自动完成基本类型与对应包装类之间的转换。例如将int赋给Integer时触发装箱;反之则发生拆箱。
    int i = new Integer(10)

    Integer j = 10
  • 缓存陷阱示例
    • 使用==比较两个位于[-128,127]区间内的Integer对象时返回true,因为它们共享同一缓存实例。
      Integer a = 127; Integer b = 127;

      a == b
    • 超出该范围的数值会创建新对象,导致==比较结果为false,此时应使用equals()方法进行内容比对。
      Integer c = 128; Integer d = 128;

      c == d
    • 推荐始终使用equals()来比较包装类的值是否相等。
      equals()

3. String、StringBuffer、StringBuilder三者有何区别?

主要差异体现在可变性线程安全性两个方面:

  • String:不可变类,底层由final修饰的字符数组(JDK8以前是char[],JDK9起改为byte[])构成。每次修改都会生成新的对象,频繁拼接效率低下。
  • StringBuffer:可变字符串容器,所有公共方法均被synchronized关键字修饰,保证线程安全,适合多线程环境下使用。
  • StringBuilder:同样可变,但方法未加同步锁,因此性能优于StringBuffer,适用于单线程中的高效字符串操作。

底层原理补充:

  • String的不可变性源于其内部数组被声明为final,且没有暴露任何修改接口,确保数据一致性。
    private final char value[]
  • StringBuffer与StringBuilder共同继承自AbstractStringBuilder,内部采用动态扩容的char数组存储内容。
  • 默认初始容量为16,当当前长度不足时,新容量计算公式为:原容量 × 2 + 2;若仍不够,则直接扩展至所需大小。

4. final关键字的三种用途解析

  • 修饰类:表示该类不能被继承,典型例子包括String和Math类,防止其行为被篡改。
  • 修饰方法:该方法不允许子类重写,保护核心逻辑不被覆盖。
  • 修饰变量:一旦赋值后不可更改。对于基本类型,意味着值恒定;对于引用类型,仅保证引用地址不变,但其所指向的对象内容仍可修改。

常见误区提醒:

  • 声明一个final数组后,虽然不能改变其引用地址,但可以修改其中元素的内容——这是合法操作。
    final int[] arr = {1,2,3}; arr[0] = 4;
  • 试图将final变量重新指向另一个对象则是非法的,会导致编译错误。
    arr = new int[5];

5. 接口与抽象类的主要区别

比较维度 接口(Interface) 抽象类(Abstract Class)
继承方式 支持多实现,一个类可同时实现多个接口 仅支持单继承,每个类最多只能继承一个抽象类
成员变量 只能定义public static final类型的常量 可包含普通成员变量、静态变量及常量
成员方法 JDK8前仅允许抽象方法;JDK8起支持default和static方法;JDK9+还允许private方法 可包含抽象方法、具体实现方法以及静态方法
构造方法 不存在构造器 可以定义构造方法,虽不能直接实例化,但可供子类调用以完成初始化
设计目的 侧重于定义行为契约,促进解耦,例如List接口规定集合操作规范 强调共性抽取,作为模板复用代码,例如HttpServlet封装通用请求处理流程

典型应用场景:

  • 当多个无关类需要遵循相同的行为规范但各自独立实现时,优先选择接口。
    Runnable
  • 当一组类属于同一类别并共享大量公共逻辑时,适合使用抽象类进行抽象封装。
    AbstractList

6. Java异常体系的核心结构

Java的异常体系以Throwable为顶层父类,其下分为两大分支:

Throwable

  • Error:代表严重的系统级问题,如OutOfMemoryError或StackOverflowError,通常无法恢复,程序无需捕获处理。
    Error
  • Exception:表示应用程序可能遇到并应妥善处理的异常情况,进一步划分为:
    Exception
    • 受检异常(Checked Exception):必须在编译期显式处理,否则无法通过编译,典型如IOException、SQLException。
    • 非受检异常(Unchecked Exception):即运行时异常,继承自RuntimeException,如NullPointerException、ArrayIndexOutOfBoundsException,不要求强制捕获。
      RuntimeException

常用异常处理关键字:

  • try:包裹可能抛出异常的代码块。
    try
  • catch:用于捕获特定类型的异常并进行处理,多个catch块应按子类到父类顺序排列,避免屏蔽。
    catch
  • finally:无论是否发生异常都会执行的代码段,常用于释放资源,如关闭流或数据库连接。
    finally
  • throw:主动抛出一个异常实例,可用于业务校验失败等情况。
    throw
  • throws:声明方法可能抛出的异常类型,提示调用者进行相应处理。
    throw new IllegalArgumentException("参数非法")
  • 完整示例演示异常传递与处理机制。
    throws

二、Java集合框架

1. 集合框架的核心接口与继承关系?

答案:

Java集合框架主要分为两大体系,所有核心类均位于java.util包中。

单列集合(Collection):用于存储单一元素,其核心子接口包括:

  • List:有序且允许重复元素,典型实现有ArrayList、LinkedList和Vector;
  • Set:无序且元素不可重复,常见实现如HashSet、TreeSet和LinkedHashSet。

双列集合(Map):用于存储键值对(key-value),主要实现类包括HashMap、TreeMap、LinkedHashMap以及ConcurrentHashMap。

关键特性说明:

  • List支持通过索引访问元素,可使用get(index)方法获取指定位置的元素;
    List
    get(int index)
  • Set依赖hashCode()equals()方法来确保元素的唯一性;
    Set
    equals()
    hashCode()
  • Map中key不允许重复(若重复则覆盖原有value),value可以重复;从JDK8开始,Map接口提供了forEach()computeIfAbsent()等便捷操作方法。
    Map
    forEach()
    computeIfAbsent()
java.util
Collection
List
Set
Map

2. ArrayList与LinkedList的区别?

答案:

比较维度 ArrayList(基于动态数组) LinkedList(基于双向链表)
底层结构 Object[] 数组 每个节点包含prev、next和value的双向链表
访问效率 随机访问快(O(1)),可通过索引直接定位 随机访问慢(O(n)),需逐个遍历查找
增删效率 尾部增删较快(O(1)),中间位置增删较慢(需移动后续元素,O(n)) 任意位置增删快(O(1),仅修改指针),但尾部操作仍需遍历至末尾(O(n),可通过last引用优化)
内存占用 连续内存空间,开销小(无额外指针) 非连续内存,每个节点有prev和next两个指针,内存开销较大
线程安全 不安全 不安全

应用场景建议:

  • ArrayList:适用于读多写少的场景,例如数据展示列表;
  • LinkedList:适合频繁在中间插入或删除元素的场景,也常用于实现栈或队列(如消息队列)。
Throwable
AutoCloseable

3. HashMap的底层实现原理(JDK1.7 vs JDK1.8)?

答案:

HashMap是基于哈希表的Map实现,采用“数组 + 链表/红黑树”的结构设计,旨在兼顾查询与修改性能。

JDK1.7 实现方式:

  • 底层结构:Entry数组 + 单向链表;
  • 存储流程:
    1. 调用key的hashCode()方法获取哈希码;
    2. 经过哈希扰动函数处理以减少冲突;
      hashCode() ^ (hashCode() >>> 16)
    3. 通过(n - 1) & hash计算出数组下标(n为数组长度);
    4. 若对应位置为空,则直接插入;否则发生哈希冲突,采用“头插法”将新节点插入链表头部。

JDK1.8 主要优化点:

  • 底层结构升级为Node数组 + 单向链表 + 红黑树;
    • 当链表长度≥8且数组长度≥64时,链表转化为红黑树;
    • 当红黑树节点数≤6时,退化回链表。
  • 哈希扰动逻辑保持一致,但在处理冲突时改用“尾插法”,避免了JDK1.7中因头插法导致的扩容期间链表成环问题;
  • 扩容机制:默认初始容量为16,负载因子为0.75;当元素数量超过容量 × 负载因子时触发扩容,新容量为原容量的两倍;
  • 支持null作为key或value,其中null key的hash值固定为0,存放于数组索引0处。

哈希冲突解决方案:

  • 哈希扰动:增强高位参与运算,提升散列均匀性;
  • 链地址法:同一桶位上的冲突元素以链表或红黑树形式组织。

线程安全性说明:

HashMap本身非线程安全,在多线程环境下可能出现:

  • JDK1.7:扩容过程中头插法引发链表循环,造成死循环;
  • JDK1.8:并发put可能导致数据覆盖。

推荐替代方案:ConcurrentHashMapCollections.synchronizedMap()

ConcurrentHashMap
Collections.synchronizedMap(new HashMap<>())

4. ConcurrentHashMap的线程安全实现(JDK1.7 vs JDK1.8)?

答案:

ConcurrentHashMap是HashMap的线程安全版本,其核心改进在于锁策略的演进。

JDK1.7 实现机制:

  • 底层结构:由Segment数组、HashEntry数组和链表组成;
  • 锁机制:采用“分段锁”技术,Segment类继承自ReentrantLock;
    • 每个Segment独立加锁,不同Segment之间互不影响;
    • 默认Segment数量为16,理论上支持最多16个线程并发写入。

JDK1.8 重大重构:

  • 底层结构:Node数组 + 链表 + 红黑树,与HashMap基本一致;
  • 锁机制:摒弃分段锁,转而采用“CAS + synchronized”组合方案:
    • 无竞争时使用CAS进行原子性更新;
    • 出现哈希冲突时,使用synchronized锁定当前链表或红黑树的头节点,粒度更细,并发性能更高;
  • 新增功能:支持computeIfAbsent()forEach()等原子性操作,整体性能优于JDK1.7版本。
    computeIfAbsent()
    forEach()

5. HashSet的实现原理?

答案:

HashSet内部基于HashMap实现,本质上是一个Key-only的Map结构。

  • 添加元素时,实际上是将该元素作为key存入HashMap,value则统一使用一个静态的Object对象(如PRESENT);
  • 由于HashMap保证key的唯一性,因此HashSet自然具备不可重复的特性;
  • 其操作性能与HashMap一致,平均时间复杂度为O(1);
  • 不保证元素顺序(除非使用LinkedHashSet);
  • 允许存储null值一次。

综上,HashSet是利用哈希表实现的Set接口典型实现,兼顾高效存取与去重能力。

HashSet 的底层实现基于 HashMap,其核心原理如下:

在创建 HashSet 时,其构造方法会内部初始化一个 HashMap 实例。当进行元素添加操作时:

add(E e)
  • 实际调用的是 HashMap 的 put 方法
  • put(e, PRESENT)
  • 其中传入的 value 是一个静态的空 Object 对象,仅作占位符使用,不存储任何实际数据
  • PRESENT

元素的唯一性由 HashMap 中 key 的不可重复特性来保证,该机制依赖于两个关键方法的协同工作:

equals()
hashCode()

HashSet 具备以下特性:无序性、元素不可重复、非线程安全。在没有哈希冲突的情况下,增删查操作的时间复杂度为 O(1)。

关键考点解析

当将自定义对象作为 HashSet 的元素时,必须重写以下两个方法以确保元素唯一性的正确判断:

equals()
hashCode()

若未重写这两个方法,则默认使用 Object 类提供的实现,即比较对象的内存地址,这通常无法满足业务场景下的逻辑相等需求。

重写规范如下:

  • 如果两个对象的 equals 方法返回 true,则它们的 hashCode 值必须相同;
  • equals()
    hashCode()
  • 反之,若两个对象的 hashCode 相同(哈希冲突),equals 方法不一定返回 true。

三、Java 多线程与并发编程

1. 创建线程的三种方式

方式一:继承 Thread 类

通过继承 Thread 类并重写其 run() 方法来定义线程执行体,随后调用 start() 方法启动线程。该方法底层通过 native 的 JNI 调用创建操作系统级别的线程。

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running");
    }
}
// 启动线程
new MyThread().start();
Thread
run()
start()
start0()

方式二:实现 Runnable 接口

实现 Runnable 接口并重写 run() 方法,然后将该实例传递给 Thread 构造函数进行启动。这种方式避免了单继承限制,更利于资源共享。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable running");
    }
}
// 启动线程
new Thread(new MyRunnable()).start();
Runnable
run()
Thread

方式三:实现 Callable 接口

Callable 接口允许线程任务有返回值,并能抛出异常。需配合 FutureTask 使用,最终仍通过 Thread 启动。

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Callable result";
    }
}
// 启动线程
FutureTask<String> future = new FutureTask<>(new MyCallable());
new Thread(future).start();
String result = future.get(); // 阻塞等待结果返回
Callable
call()
FutureTask
Thread

对比总结:

  • 继承 Thread:简单直接,但占用继承名额,无法再继承其他类;
  • 实现 Runnable 或 Callable:更灵活,支持多继承场景下的资源共用;其中 Callable 支持返回值和异常处理,适用于需要获取执行结果的场景。

2. 线程的生命周期及状态转换

Java 中线程的状态由 Thread.State 枚举定义,共包含六种状态:

Thread.State
  • NEW(新建):线程对象已创建,但尚未调用 start() 方法;
  • NEW
    start()
  • RUNNABLE(可运行):线程已启动,处于就绪或运行中状态,具体由操作系统调度决定;
  • RUNNABLE
  • BLOCKED(阻塞):线程尝试进入同步块但未能获取 monitor 锁,例如 synchronized 块竞争失败;
  • BLOCKED
    synchronized
  • WAITING(无限等待):线程主动调用 wait()、join() 或 LockSupport.park() 进入无限期等待,直到被其他线程显式唤醒;
  • WAITING
    Object.wait()
    Thread.join()
    LockSupport.park()
  • TIMED_WAITING(限时等待):调用 sleep(long)、wait(long)、join(long) 等带有超时参数的方法后进入此状态,超时后自动恢复;
  • TIMED_WAITING
    Thread.sleep(ms)
    Object.wait(ms)
    Thread.join(ms)
  • TERMINATED(终止):线程任务执行完毕或因异常退出。
  • TERMINATED

核心状态转换路径:

  • NEW → RUNNABLE:调用 start() 方法;
  • NEW
    RUNNABLE
    start()
  • RUNNABLE → TERMINATED:线程正常结束或异常终止;
  • TERMINATED
  • RUNNABLE → BLOCKED:尝试获取同步锁失败;BLOCKED → RUNNABLE:成功获取锁;
  • RUNNABLE
    BLOCKED
    RUNNABLE
  • RUNNABLE → WAITING/TIMED_WAITING:调用相应等待方法;WAITING/TIMED_WAITING → RUNNABLE:被唤醒或超时。
  • WAITING/TIMED_WAITING
    RUNNABLE

3. synchronized 与 Lock 的区别

对比维度 synchronized(内置锁) Lock(显式锁,如 ReentrantLock)
锁的实现层级 JVM 层面实现(基于 C++) JDK 层面实现(纯 Java 编写)
获取与释放方式 进入同步代码块自动获取,退出或异常时自动释放 需手动调用 lock() 获取,unlock() 释放(建议在 finally 块中执行)
锁类型支持 可重入、非公平锁(默认),JDK 6+ 引入偏向锁、轻量级锁、重量级锁升级机制 可重入,构造时可指定公平或非公平模式
功能扩展性 功能较基础,仅提供基本同步控制 支持中断响应、超时获取锁、多个条件变量等高级特性

4. volatile关键字的作用?

volatile 是Java中提供的一种轻量级同步机制,主要具备两个核心功能:

  • 保证可见性: 当一个线程修改了被volatile修饰的变量后,该变更会立即刷新到主内存中,其他线程在读取该变量时能直接获取最新值。这是通过禁止CPU缓存优化、强制变量读写操作作用于主内存来实现的。
  • 防止指令重排序: 编译器和处理器为了优化性能可能会对指令进行重排,而volatile通过插入内存屏障(Memory Barrier)阻止这种重排序行为。典型应用如双重校验锁(DCL)单例模式中,使用volatile修饰实例字段可避免对象未完全初始化就被其他线程引用的问题。

局限性说明:

  • 不保证原子性:例如自增操作(i++),涉及“读取→修改→写入”三个步骤,并非原子操作,在多线程环境下仍可能出现数据竞争问题,需结合synchronized或AtomicInteger等工具保障原子性;
  • 不能替代锁机制:适用于“单写多读”或状态标志位场景,比如用作线程运行控制开关。
volatile boolean flag = false;

经典应用场景 —— 双重校验锁(DCL)单例模式:

public class Singleton {
    // 使用volatile确保instance的写操作不会发生指令重排
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查:无锁快速返回
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查:防止并发创建多个实例
                    instance = new Singleton(); 
                    // volatile保证以下三步不会重排序:
                    // 1. 分配内存空间
                    // 2. 初始化对象
                    // 3. instance指向内存地址
                }
            }
        }
        return instance;
    }
}
volatile int i = 0; i++;

5. 线程池的核心参数与工作流程?

Java中的线程池由 ThreadPoolExecutor 类实现,基于“池化技术”管理线程资源,有效降低频繁创建和销毁线程带来的系统开销,提升高并发下的执行效率。

ThreadPoolExecutor

核心构造参数如下:

参数名 说明
corePoolSize 核心线程数量,即使空闲也不会被回收(除非设置allowCoreThreadTimeOut)
maximumPoolSize 最大线程总数,包括核心线程与临时线程
keepAliveTime 超出核心线程数的临时线程,在空闲超过此时间后将被终止
unit keepAliveTime的时间单位(如秒、毫秒等)
workQueue 阻塞队列,用于存放待处理任务,当核心线程满负荷时新任务进入队列等待
threadFactory 线程工厂接口,可用于自定义线程名称、优先级、是否为守护线程等属性
handler 拒绝策略,当线程池饱和(最大线程数已达上限且队列已满)时如何处理新提交的任务

线程池的工作流程:

  1. 当有新任务提交时,若当前运行线程数小于corePoolSize,则创建新的核心线程执行任务;
  2. 若核心线程已满,则将任务加入workQueue等待执行;
  3. 若队列也已满,但总线程数小于maximumPoolSize,则创建临时线程处理任务;
  4. 若线程数达到maximumPoolSize且队列已满,则触发拒绝策略;
  5. 当临时线程空闲时间超过keepAliveTime,会被自动销毁以释放资源。

常见的拒绝策略:

  • AbortPolicy
    AbortPolicy
    :默认策略,直接抛出 RejectedExecutionException 异常
    RejectedExecutionException
  • CallerRunsPolicy
    CallerRunsPolicy
    :由调用者所在的线程(即提交任务的线程)直接执行该任务;
  • DiscardPolicy
    DiscardPolicy
    :静默丢弃新任务,不抛异常;
  • DiscardOldestPolicy
    DiscardOldestPolicy
    :丢弃队列中最老的一个任务,然后尝试提交新任务。

Executors工具类提供的常见线程池类型:

  • newFixedThreadPool
    Executors.newFixedThreadPool(n)
    :固定大小线程池,corePoolSize = maximumPoolSize = n,使用无界队列LinkedBlockingQueue;
  • newCachedThreadPool
    Executors.newCachedThreadPool()
    :缓存线程池,corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,空闲线程存活60秒,使用SynchronousQueue;
  • newSingleThreadExecutor
    Executors.newSingleThreadExecutor()
    :单线程池,保证任务按顺序执行,corePoolSize=1,maximumPoolSize=1,使用无界队列;
  • newScheduledThreadPool
    Executors.newScheduledThreadPool(n)
    :支持定时及周期性任务调度的线程池,底层使用DelayedWorkQueue。

注意事项:

根据《阿里巴巴Java开发手册》建议,不应使用Executors创建线程池,原因在于:

  • FixedThreadPool 和 SingleThreadExecutor
    newFixedThreadPool
    /
    newSingleThreadExecutor
    :采用无界队列,可能导致大量任务积压,引发OutOfMemoryError(OOM);
  • CachedThreadPool
    newCachedThreadPool
    :最大线程数无限制,极端情况下可能创建过多线程,耗尽系统资源。

因此推荐方式是:直接通过 ThreadPoolExecutor 构造函数手动配置参数,明确指定队列容量、线程边界和拒绝策略,提高系统的可控性和稳定性。

ThreadPoolExecutor

构造方法应设置合理的参数,例如将核心线程数设定为CPU核心数±1,并优先采用有界队列作为任务队列。

ThreadLocal 的实现机制与内存泄漏问题

ThreadLocal 是一种线程隔离的变量存储机制,它为每个线程提供独立的变量副本,从而避免多线程环境下共享变量带来的并发冲突。

底层原理

每个 Thread 对象内部维护一个 ThreadLocalMap 实例(ThreadLocal 的静态内部类),该 Map 用于存储当前线程的本地变量。

  • 键(key):ThreadLocal 实例本身,使用弱引用(WeakReference)方式保存;
  • 值(value):对应线程中的变量副本,以强引用形式存在。
ThreadLocalMap

核心方法说明

set(T value):获取当前线程的 ThreadLocalMap,若不存在则创建,并将当前 ThreadLocal 实例作为 key,传入的 value 作为值存入。

set(T value)

get():获取当前线程的 ThreadLocalMap,根据当前 ThreadLocal 实例查找对应的值;若未找到,则调用 initialValue() 方法进行初始化并返回初始值。

get()

remove():从当前线程的 ThreadLocalMap 中删除与此 ThreadLocal 实例相关的键值对,释放资源。

initialValue()
remove()

内存泄漏成因及解决方案

由于 ThreadLocalMap 的 key 使用的是弱引用,当外部不再持有 ThreadLocal 实例的强引用时(如设为 null),key 会被垃圾回收,此时 entry 的 key 变为 null,但 value 仍被强引用指向。如果线程长期运行(如线程池中的核心线程),这些 value 将无法被 GC 回收,造成内存泄漏。

应对策略:

  • 每次使用完 ThreadLocal 后主动调用 remove() 方法清除数据;
  • 避免声明静态 ThreadLocal,因其生命周期过长,更容易引发内存泄漏;
  • 在线程池场景中,确保任务执行完毕后清理 ThreadLocal 变量,防止累积泄露。
WeakReference<ThreadLocal<?>>

典型应用场景

  • 保存线程上下文信息,如用户登录状态、数据库连接对象、事务控制实例等;
  • 减少方法间参数传递的复杂度,例如 Spring 框架中通过 ThreadLocal 存储 HttpServletRequest 上下文对象,实现便捷访问。
RequestContextHolder

JVM 核心机制

1. JVM 内存模型(运行时数据区)

JVM 在 JDK 8 版本下的运行时数据区域主要划分为五个部分:

程序计数器(Program Counter Register)
记录当前线程所执行的字节码指令地址(即行号),在线程切换时可恢复执行位置。
特点:线程私有,不会出现 OutOfMemoryError,是唯一不抛出 OOM 异常的区域。

虚拟机栈(VM Stack)
用于存放方法调用过程中的栈帧,每个栈帧包含局部变量表、操作数栈、方法出口等信息。
特点:线程私有,方法调用即入栈,方法返回即出栈,反映方法执行生命周期。
异常情况:
- 栈深度超过限制 → 抛出 StackOverflowError(常见于无限递归);
- 动态扩展时内存不足 → 抛出 OutOfMemoryError。

StackOverflowError
OutOfMemoryError

本地方法栈(Native Method Stack)
功能类似于虚拟机栈,专用于支持 Native 方法(如 Thread.start0())的执行。
特点:线程私有,也可能抛出 StackOverflowError 和 OutOfMemoryError。

堆(Heap)
用于存储对象实例和数组,是 JVM 中最大的内存区域,也是垃圾回收的主要场所。
特点:线程共享,可通过以下参数配置:
- -Xms:设置初始堆大小;
- -Xmx:设置最大堆大小。

-Xms
-Xmx

逻辑分区结构:

  • 年轻代(Young Generation):新创建的对象首先分配在此。默认划分为 Eden 区、Survivor0(S0)、Survivor1(S1),比例通常为 8:1:1;
  • 老年代(Old Generation):经过多次 Minor GC 仍存活的对象会晋升至老年代;
  • 元空间(Metaspace,JDK 8+):取代永久代,用于存储类的元数据(如类名、方法签名、字段信息),使用本地内存,默认无上限,可通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 调整。
-XX:MetaspaceSize
-XX:MaxMetaspaceSize

异常情况:
- 堆内存不足 → 抛出 OutOfMemoryError;
- 元空间内存不足 → 抛出 Metaspace 相关的 OutOfMemoryError。

OutOfMemoryError: Java heap space
OutOfMemoryError: Metaspace

方法区(Method Area)
用于存储类的结构信息,包括类元数据、运行时常量池(String 常量池自 JDK 7 起移至堆中)、静态变量以及 JIT 编译后的代码。
特点:线程共享,在 JDK 8 之前称为“永久代”(PermGen),之后由“元空间”替代。

2. 垃圾回收(GC)基本原理

GC 是 JVM 自动管理堆内存的机制,旨在识别并回收不再使用的对象,释放内存空间,防止内存泄漏。

(1)垃圾判定算法

引用计数法
为每个对象维护一个引用计数器,每当有一个引用指向该对象时计数加一,引用失效则减一。当计数为零时视为可回收。
局限性:无法处理循环引用问题(如 A 引用 B,B 同时引用 A,导致计数均不为零而无法回收)。

可达性分析算法(JVM 实际采用)
以一组被称为“GC Roots”的对象为起点,向下搜索所有可达对象,未被访问到的对象被视为不可达,即为垃圾对象。
常见的 GC Roots 来源包括:
- 虚拟机栈中局部变量表引用的对象;
- 本地方法栈中 JNI 引用的对象;
- 方法区中静态成员引用的对象;
- 运行时常量池中的常量引用;
- 正在活跃的线程所持有的引用。

(2)常用垃圾回收算法

标记-清除算法(Mark-Sweep)
流程:先标记所有需要回收的对象,然后统一清除。
优点:实现简单,效率较高。
缺点:会产生大量内存碎片,可能导致后续大对象分配失败。

复制算法(Copying)
将可用内存按容量分为两块,每次只使用其中一块。当这一块用尽时,将存活对象复制到另一块上,再清空已使用过的区域。
优点:解决内存碎片问题,适合存活对象少的场景(如年轻代)。
缺点:内存利用率较低,需预留一半空间。

垃圾回收算法与常见收集器:

复制算法(Copying):
将内存划分为两个区域(例如 Eden 与 S0/S1),首先标记出存活的对象,随后将其复制到另一个空闲区域中,最后清空原区域。
优点:避免了内存碎片问题,内存分配效率高;
缺点:内存使用率仅为50%,存在空间浪费,适用于年轻代(该区域中大多数对象生命周期短、存活率低)。

<clinit>()

标记-整理算法(Mark-Compact):
首先标记所有存活对象,然后将这些对象向内存的一端进行移动,使得存活对象连续排列,最后清理边界以外的垃圾区域。
优点:无内存碎片,内存利用率高;
缺点:涉及对象移动操作,开销较大,适合用于老年代(此区域中多数对象长期存活)。

rt.jar

分代收集算法(Generational GC,JVM实际采用机制):
根据对象的生命周期将其划分到不同的代中,针对不同代的特点选用最合适的GC策略:
- 年轻代:对象死亡率高、存活少,采用复制算法;
- 老年代:对象存活多,通常采用标记-清除或标记-整理算法。

jre/lib/ext

常见的GC收集器:

  • Serial 收集器:单线程执行GC,年轻代使用复制算法,老年代使用标记-整理算法。适用于单核CPU环境,如客户端应用。
  • Parallel Scavenge 收集器:多线程并行GC,专注于提升吞吐量(定义为:用户代码运行时间 / (用户代码运行时间 + GC时间)),常用于服务器端对吞吐性能要求高的场景。
  • ParNew 收集器:是Parallel Scavenge的多线程版本,可与CMS配合工作,主要用于支持并发收集的老年代GC方案。
  • CMS 收集器(Concurrent Mark Sweep):作用于老年代,基于标记-清除算法,强调低停顿时间,允许GC线程与用户线程并发执行,适合对响应速度敏感的应用(如Web服务)。但其主要缺点包括产生内存碎片以及较高的并发资源消耗。
  • G1 收集器(Garbage-First):从JDK 9起成为默认GC收集器,基于标记-整理思想,将堆内存划分为多个大小相等的Region,优先回收垃圾对象较多的Region,兼顾低延迟和高吞吐量,支持数十GB级的大堆内存管理。
  • ZGC / Shenandoah 收集器:新一代超低延迟GC实现,能够将GC暂停时间控制在毫秒级别以下,适用于TB级堆内存和极端低延迟需求的系统。
ClassLoader

类加载机制与双亲委派模型

类加载过程概述:
类加载是指将 .class 字节码文件加载进JVM,并在内存中创建对应的 Class 对象的过程,整个流程分为五个阶段:

  1. 加载(Loading):通过类加载器读取 .class 文件内容,生成二进制字节流,并在堆中创建相应的 Class 类实例。
  2. 验证(Verification):确保字节码的安全性和合法性,包括格式检查、语法语义分析、符号引用验证等,防止恶意或错误代码破坏JVM运行环境。
  3. 准备(Preparation):为类的静态变量分配内存空间,并设置初始默认值(如 int 为 0,boolean 为 false),不涉及实例字段。
  4. 解析(Resolution):将符号引用(如类名、方法签名)转换为直接指向内存地址的直接引用。
  5. 初始化(Initialization):执行类构造器 <clinit> 方法,完成静态变量赋值和静态代码块的运行。初始化顺序遵循:先父类后子类,先静态变量再静态代码块。
findClass()

类加载器类型:

  • 启动类加载器(Bootstrap ClassLoader):由C++编写,负责加载JVM核心类库(如 java.lang.* 等),位于最高层级,没有父类加载器。
  • 扩展类加载器(Extension ClassLoader):Java语言实现,用于加载 JDK 扩展目录(如 jre/lib/ext)下的类库。
  • 应用程序类加载器(Application ClassLoader):也称系统类加载器,负责加载 classpath 中的应用程序类,是默认使用的类加载器。
  • 自定义类加载器(Custom ClassLoader):开发者继承 ClassLoader 类并重写 findClass() 方法,可用于实现特殊需求,如热部署、加密类加载等。
java.lang.String

双亲委派模型(Parent Delegation Model):
核心机制:当一个类加载器收到类加载请求时,不会立即自行加载,而是先委托给其父类加载器去尝试加载,只有当父级无法完成加载时,才由自己处理。
典型路径:应用程序类加载器 → 扩展类加载器 → 启动类加载器。若顶层无法加载,则反向逐层尝试。
优势:
- 防止重复加载同一个类(例如 java.lang.String 只能由启动类加载器加载一次);
- 保障核心类库安全,防止被篡改或替换。

破坏双亲委派的典型场景:

  • 热部署/模块化框架(如OSGi):需要在同一JVM中加载同一类的不同版本,因此必须打破标准委派机制。
  • JNDI、SPI 机制(如JDBC驱动加载):核心接口由启动类加载器加载,而具体实现类位于应用classpath中,需通过线程上下文类加载器(Thread Context ClassLoader)绕过双亲委派完成加载。

Spring 核心框架

Spring IoC 的原理与实现:
IoC(Inversion of Control,控制反转)是 Spring 框架的核心理念之一,即将对象的创建及依赖关系的维护从程序主动控制转交至Spring容器统一管理,从而降低组件间的耦合度。

关键概念:

  • IoC 容器:Spring 的核心运行时组件(如 ApplicationContext、BeanFactory),负责Bean的创建、配置、生命周期管理和依赖注入。
  • Bean:由IoC容器所管理的Java对象,通常是Service、DAO等业务组件。
  • 依赖注入(DI):IoC的具体实现方式,指容器在创建Bean实例时自动为其注入所依赖的其他Bean,无需手动 new 或查找。
ApplicationContext
BeanFactory

依赖注入的三种常用方式:

  1. 构造函数注入:通过构造方法传入依赖对象,适合强制依赖项。
  2. Setter 方法注入:通过Setter方法设置依赖,灵活性较高,适用于可选依赖。
  3. 字段注入(基于注解):直接在字段上使用 @Autowired 等注解注入,代码简洁但不利于测试和解耦,推荐谨慎使用。
new

依赖注入的三种方式

构造方法注入:通过Bean的构造函数传入所依赖的对象。该方式适用于强制依赖项,能够有效防止空指针异常,是Spring官方推荐的方式之一。从Spring 4.3版本开始,若类中只有一个构造方法,@Autowired注解可省略。

@Service
public class UserService {
    private final UserDao userDao;

    // 构造方法注入(@Autowired可省略)
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}

Setter方法注入:通过Setter方法设置依赖关系,适合用于可选的、非必需的依赖。需要配合@Autowired注解使用,Spring容器会自动调用对应的Setter方法完成注入。

@Service
public class UserService {
    private UserDao userDao;

    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

字段注入:直接在成员变量上使用@Autowired注解进行注入,写法简洁,但不被推荐。因为这种方式绕过了构造器初始化,无法保证依赖的完整性,也不利于单元测试和依赖校验。

@Service
public class UserService {
    @Autowired
    private UserDao userDao;
}
@Autowired

IoC容器初始化流程

  1. 加载配置源,例如XML文件或基于注解的配置类;
  2. 解析配置信息,扫描并识别带有@Component、@Service、@Controller等注解的类,将其元数据注册到BeanDefinitionRegistry中;
  3. @Configuration
    @Component
    @Service
    @Repository
  4. 根据Bean定义进行实例化,默认采用单例模式(懒加载除外);
  5. 执行依赖注入(DI),利用BeanPostProcessor等后置处理器机制,自动装配所需的依赖对象;
  6. 初始化Bean:依次调用由@PostConstruct标注的方法、实现InitializingBean接口的afterPropertiesSet()方法,以及自定义的init-method;
  7. @PostConstruct
    afterPropertiesSet()
  8. 此时Bean已准备就绪,可供应用程序正常使用;
  9. 当容器关闭时,触发销毁逻辑:执行@PreDestroy注解标记的方法、DisposableBean接口的destroy()方法,以及配置的destroy-method。
  10. @PreDestroy
    destroy()

Spring AOP 的原理与应用场景

AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架的重要特性之一,旨在将横切关注点(如日志、事务、安全控制等)与核心业务逻辑分离,提升代码的模块化程度和复用性。

核心概念解析

  • 切面(Aspect):一个封装了横切功能的类,比如日志记录、事务管理等,通常包含通知和切入点定义;
  • 通知(Advice):指切面在特定连接点上执行的具体操作,分为以下五种类型:
    • @Before
      前置通知,在目标方法执行前运行;
    • @After
      最终通知,无论方法是否抛出异常都会执行;
    • @AfterReturning
      返回通知,仅在方法成功返回后触发;
    • @AfterThrowing
      异常通知,当目标方法抛出异常时执行;
    • @Around
      环绕通知,包裹整个方法调用过程,可控制是否继续执行目标方法,常用于事务控制;
  • 切入点(Pointcut):用于指定哪些连接点应被织入切面逻辑,通常通过execution表达式等方式精确匹配方法签名;
  • 连接点(JoinPoint):程序执行过程中可以插入切面的特定位置,如方法调用、异常抛出等;
  • 织入(Weaving):将切面逻辑整合进目标对象的过程。Spring AOP默认在运行期通过动态代理实现织入。

实现机制

Spring AOP 主要依赖两种动态代理技术来实现织入:

JDK 动态代理:

  • 适用条件:目标类实现了至少一个接口;
  • 实现原理:通过java.lang.reflect.Proxy类生成代理对象,代理类实现与目标类相同的接口,并在方法调用中嵌入切面逻辑;
  • 局限性:只能代理接口方法,无法对没有接口的类进行增强。
  • java.lang.reflect.Proxy

CGLIB 动态代理:

  • 适用条件:目标类未实现接口;
  • 实现原理:借助CGLIB库生成目标类的子类,在子类中重写方法以插入切面逻辑;
  • 优势:支持对任意类进行代理,无需依赖接口,且运行时性能较高;
  • 注意:由于是继承实现,final类或final方法无法被代理。

典型应用场景

  • 日志记录:在方法调用前后记录参数、返回值及执行耗时;
  • 事务管理:声明式事务通过AOP实现,自动控制事务的开启、提交与回滚;
  • 权限校验:在关键业务方法执行前验证用户身份与权限;
  • 异常统一处理:捕获并处理特定方法的异常,避免重复代码。

示例:日志切面实现

以下是一个基于@AspectJ风格的日志切面示例:

// 切面类
@Aspect
@Component
public class LogAspect {

    // 定义切入点:拦截com.example.service包下所有public方法
    @Pointcut("execution(public * com.example.service..*(..))")
    public void servicePointcut() {}

    // 环绕通知:记录方法执行时间
    @Around("servicePointcut()")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed(); // 执行目标方法
        long duration = System.currentTimeMillis() - startTime;
        System.out.println("Method " + joinPoint.getSignature() + " executed in " + duration + "ms");
        return result;
    }
}
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    // 获取方法名和参数
    String methodName = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    System.out.println("方法" + methodName + "调用,参数:" + Arrays.toString(args));

    // 记录开始时间,执行目标方法
    long start = System.currentTimeMillis();
    Object result = joinPoint.proceed();

    // 方法执行完成后,计算耗时并输出结果
    long cost = System.currentTimeMillis() - start;
    System.out.println("方法" + methodName + "返回值:" + result + ",执行时间:" + cost + "ms");
    return result;
}

Spring事务管理的核心原理

Spring事务管理主要分为两种方式:声明式事务(基于AOP实现)和编程式事务(通过编码手动控制)。其中,声明式事务因其简洁性和非侵入性,成为开发中的主流选择。

1. 事务的ACID特性

  • 原子性(Atomicity):事务是最小执行单元,所有操作要么全部成功提交,要么在发生异常时全部回滚。
  • 一致性(Consistency):事务前后数据库状态保持一致,例如转账操作中,总金额不会发生变化。
  • 隔离性(Isolation):多个并发事务之间相互独立,避免彼此干扰。
  • 持久性(Durability):一旦事务提交,其对数据的修改将永久保存到存储介质中。

2. Spring支持的事务隔离级别

Spring允许配置不同的事务隔离级别,以平衡并发性能与数据一致性:

DEFAULT
:默认级别,具体由底层数据库决定(如MySQL默认为REPEATABLE READ);
READ_UNCOMMITTED
:读未提交,最低级别,可能导致脏读、不可重复读和幻读;
READ_COMMITTED
:读已提交,可防止脏读,但不可重复读和幻读仍可能发生(Oracle默认);
REPEATABLE_READ
:可重复读,避免脏读和不可重复读,但在某些情况下可能出现幻读(MySQL默认);
SERIALIZABLE
:串行化,最高级别,彻底解决并发问题,但会显著降低系统性能。

3. 事务传播行为(用于处理嵌套事务)

Spring定义了7种事务传播机制,常见如下:

REQUIRED
:若当前存在事务,则加入该事务;否则创建新事务(默认行为);
REQUIRES_NEW
:无论当前是否有事务,均开启一个新的独立事务,原事务被挂起;
SUPPORTS
:若当前有事务则加入,没有则以非事务方式运行;
NOT_SUPPORTED
:始终以非事务方式执行,若当前存在事务则将其暂停;
NEVER
:非事务方式执行,若当前存在事务则抛出异常。

4. 声明式事务的实现机制(基于AOP)

声明式事务通过注解驱动,核心流程如下:

  • 启用方式:使用 @EnableTransactionManagement 注解开启事务支持(Spring Boot项目通常自动启用);
  • 关键注解:@Transactional 可标注在类或方法上,类级别注解作用于所有公共方法;
  • AOP原理:@Transactional 被解析为切面,切入点为目标方法;
  • 通知逻辑:代理对象在方法前开启事务,正常执行后提交,出现异常则回滚;
  • 事务管理器:Spring通过 PlatformTransactionManager 接口统一管理不同数据访问技术;
@EnableTransactionManagement
:启用事务管理;
@Transactional
:标注事务方法;
@Transactional
:AOP切面处理流程;
PlatformTransactionManager
:事务管理器接口;
DataSourceTransactionManager
:适用于JDBC或MyBatis的数据源事务管理;
HibernateTransactionManager
:适配Hibernate的事务实现。

5. 常见事务失效场景及原因

  • 方法访问修饰符非 public —— @Transactional 仅对 public 方法有效;
  • 内部方法调用 —— 同一类中方法直接调用绕过代理,导致事务不生效;
  • 异常类型不匹配 —— 默认仅对 RuntimeExceptionError 回滚,检查型异常需显式声明;
  • 使用 try-catch 捕获异常但未重新抛出 —— 事务切面无法感知异常,导致无法触发回滚;
  • 传播行为设置不当 —— 如使用 NOT_SUPPORTEDNEVER 会导致事务被挂起或拒绝;
  • 数据源未正确绑定事务管理器 —— DataSourceTransactionManager 未被Spring容器管理,导致事务配置无效。
RuntimeException
:运行时异常;
Error
:错误类型;
rollbackFor
:用于指定回滚的异常类型;
try-catch
:捕获异常代码块;
throw
:未抛出异常语句;
PlatformTransactionManager
:事务管理器未注册到Spring上下文中。

六、数据库与MyBatis

1. JDBC的核心操作步骤

JDBC(Java Database Connectivity)是Java程序连接和操作数据库的标准API,基本流程包括:

  1. 加载数据库驱动:JDK 8及以上版本无需手动加载,DriverManager可自动发现驱动;
  2. 获取数据库连接:通过 DriverManager.getConnection() 建立与数据库的连接;
  3. 创建执行对象:使用 Connection 创建 StatementPreparedStatement 实例;
  4. 执行SQL语句:
    • 查询操作使用 executeQuery() 方法;
    • 增删改操作调用 executeUpdate() 方法;
DriverManager.getConnection(url, username, password)
:建立数据库连接;
executeQuery()
:执行查询语句;
executeUpdate()
:执行增删改操作。

数据库操作流程主要包括以下几个步骤:

  1. 加载驱动:针对 MySQL 8.0 及以上版本,需加载的驱动类为 com.mysql.cj.jdbc.Driver。
  2. 建立连接:通过 JDBC URL、用户名和密码获取数据库连接对象 Connection。
  3. 创建 PreparedStatement:使用预编译 SQL 语句,支持参数占位符(?),可有效防止 SQL 注入攻击。
  4. 执行查询:调用 executeQuery() 方法发送 SQL 请求并返回结果集 ResultSet。
  5. 遍历处理结果集:通过 while 循环调用 rs.next() 逐行读取数据,并使用 getXXX 方法提取字段值。
  6. 资源释放:在 finally 块中按逆序关闭 ResultSet、PreparedStatement 和 Connection,防止资源泄漏。

示例代码如下:

public void queryUser() {
    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    try {
        // 加载 MySQL 驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        
        // 建立数据库连接
        String url = "jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC";
        conn = DriverManager.getConnection(url, "root", "123456");

        // 定义带参数的 SQL 查询语句
        String sql = "SELECT id, name FROM user WHERE id = ?";
        pstmt = conn.prepareStatement(sql);
        pstmt.setInt(1, 1); // 绑定参数

        // 执行查询并获取结果集
        rs = pstmt.executeQuery();

        // 遍历结果集并输出信息
        while (rs.next()) {
            int id = rs.getInt("id");
            String name = rs.getString("name");
            System.out.println("id: " + id + ", name: " + name);
        }
    } catch (ClassNotFoundException | SQLException e) {
        e.printStackTrace();
    } finally {
        // 按顺序关闭资源,避免内存泄漏
        try {
            if (rs != null) rs.close();
            if (pstmt != null) pstmt.close();
            if (conn != null) conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

SELECT * FROM user WHERE name = '" + name + "'"

关于 SQL 注入问题,若使用 Statement 拼接字符串构造 SQL,当传入的参数如 name 包含恶意内容

' OR '1'='1
时,最终生成的 SQL 将变为
SELECT * FROM user WHERE name = '' OR '1'='1'
,可能导致所有用户数据被泄露。而 PreparedStatement 使用参数绑定机制,在SQL语法解析前完成参数设置,从根本上杜绝了此类风险。

与 Statement 相比,PreparedStatement 的优势包括:

  • 支持预编译,提升多次执行时的性能;
  • 提供参数化查询接口,增强安全性;
  • 自动处理特殊字符,减少转义错误。

MyBatis 核心组件及其工作原理

MyBatis 是一个优秀的持久层框架,它简化了传统 JDBC 编码过程,允许开发者通过 XML 文件或注解方式配置 SQL 语句,无需手动管理连接、结果集等资源。

核心组件说明:

SqlSessionFactory
SqlSessionFactory 是 MyBatis 的核心工厂类,由
SqlSessionFactoryBuilder
解析全局配置文件 mybatis-config.xml 后构建而成,通常以单例形式存在,线程安全。

SqlSession
SqlSession 表示一次数据库会话,封装了底层 Connection 和事务控制逻辑。由于其内部状态不具线程安全性,建议每个请求独立创建实例。

Mapper接口
Mapper 接口 是用户自定义的数据访问接口,MyBatis 利用动态代理技术自动生成其实现类,将接口方法调用映射到对应的 SQL 执行。

Mapper.xml
Mapper 映射文件 或注解中定义了具体的 SQL 语句、输入参数类型及结果集映射规则,实现 Java 对象与数据库记录之间的转换。

Configuration
Configuration 对象保存 MyBatis 的全部配置信息,包括数据源、事务管理器、缓存设置以及注册的所有 Mapper。

工作流程概述:

  1. 配置加载阶段
    SqlSessionFactoryBuilder
    负责读取 mybatis-config.xml 和各个 Mapper.xml 文件,解析出数据库连接信息、SQL 定义、映射关系等元数据。
  2. 创建 SqlSessionFactory:根据解析后的 Configuration 构建 SqlSessionFactory 实例,一般在整个应用生命周期中仅创建一次。
  3. 获取 SqlSession:通过 SqlSessionFactory 调用
    openSession()
    方法创建 SqlSession,默认情况下事务不会自动提交。
  4. 获取 Mapper 代理对象:调用 SqlSession 的
    getMapper(Mapper接口.class)
    方法,获得指定 Mapper 接口的代理实现。
  5. 执行 SQL 操作:调用 Mapper 接口中的方法时,MyBatis 自动查找对应 SQL 并执行 JDBC 操作。
  6. 结果集映射:根据 resultType 或 resultMap 的配置,将 ResultSet 自动转换为所需的 Java 实体对象。
  7. 事务管理:操作完成后,通过 SqlSession 调用
    commit()
    提交事务,或调用
    rollback()
    回滚事务。
  8. 关闭会话:及时关闭 SqlSession 以释放数据库连接等底层资源。

MyBatis的一级缓存与二级缓存机制解析:

为了提升数据库操作性能,MyBatis内置了缓存功能,能够有效减少对数据库的重复查询。该缓存体系主要分为两个层级:一级缓存和二级缓存。

一级缓存(SqlSession级别,默认启用)

  • 作用范围:在同一个SqlSession中,若多次执行相同的SQL语句且参数一致,则仅首次访问数据库,后续结果将从缓存中读取。
  • 实现方式:SqlSession内部通过一个HashMap来维护缓存数据,其中键值由SQL语句、参数、RowBounds以及当前环境等信息共同构成,对应的值为查询返回的结果集。
  • 失效条件
    • 执行任何insert、update或delete操作时,一级缓存会被自动清空;
    • 调用相关方法手动清除缓存;
    • 当SqlSession被关闭或事务提交后,缓存也随之失效。
SqlSession.clearCache()

二级缓存(Mapper级别,需手动开启)

  • 作用范围:跨多个SqlSession,在同一命名空间(即同一个Mapper接口)下共享缓存数据。
  • 启用步骤
    1. 在全局配置文件mybatis-config.xml中确认已开启缓存支持(此项通常默认开启,可不显式配置);
    2. 在对应的Mapper.xml文件中添加<cache/>标签,以激活该Mapper的二级缓存功能。
    <setting name="cacheEnabled" value="true"/>
    <cache/>
  • 工作原理:每个Mapper命名空间对应一个独立的Cache实例。当某个SqlSession完成一次查询后,结果会存储到二级缓存中;其他SqlSession在执行相同查询时,会优先尝试从该缓存中获取数据。
  • 注意事项
    • 被缓存的对象必须实现Serializable接口,因为二级缓存可能涉及序列化存储,尤其是在分布式或多JVM环境中。
    Serializable

核心配置文件示例(mybatis-config.xml)

<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=false"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="com/example/mapper/UserMapper.xml"/>
  </mappers>
</configuration>

Mapper映射文件示例(UserMapper.xml)

<mapper namespace="com.example.mapper.UserMapper">
  <resultMap id="UserResultMap" type="com.example.entity.User">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <result column="age" property="age"/>
  </resultMap>

  <select id="selectUserById" parameterType="int" resultMap="UserResultMap">
    SELECT id, name, age FROM user WHERE id = #{id}
  </select>

  <insert id="insertUser" parameterType="com.example.entity.User">
    INSERT INTO user (name, age) VALUES (#{name}, #{age})
  </insert>
</mapper>

执行插入(insert)、更新(update)或删除(delete)操作时,会清空当前Mapper的二级缓存。

可通过以下方式控制缓存行为:

useCache="false"
—— 用于查询语句

flushCache="true"
—— 用于增删改语句

缓存的查询顺序如下:首先尝试从二级缓存获取数据,若未命中则查找一级缓存,最后再访问数据库。

七、设计模式与性能优化

1. 单例模式的实现方式及其线程安全性分析

单例模式的核心目标是确保一个类在整个应用中仅存在一个实例,并提供全局访问点。常见的实现方式包括以下几种:

(1)饿汉式(线程安全,非懒加载)

public class Singleton {
    // 类加载阶段即创建实例
    private static final Singleton instance = new Singleton();

    // 私有构造函数,防止外部实例化
    private Singleton() {}

    // 全局访问方法
    public static Singleton getInstance() {
        return instance;
    }
}
  • 优点:实现简单,类加载时完成初始化,天然具备线程安全性;
  • 缺点:不具备懒加载特性,即使实例未被使用也会在类加载时创建,可能造成内存浪费。

(2)懒汉式(从非线程安全到线程安全的演进)

基础版本(线程不安全)

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // 多线程环境下可能产生多个实例
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

该版本在高并发场景下可能导致多个线程同时进入if判断,从而创建多个实例。

优化版本——双重检查锁定(DCL),支持懒加载且线程安全

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                    // 第一次检查:无锁快速返回
            synchronized (Singleton.class) {       // 加锁同步
                if (instance == null) {            // 第二次检查:避免重复创建
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 优点:实现懒加载,延迟实例创建,兼顾性能与线程安全;
  • 关键点:必须使用volatile关键字,防止
    instance = new Singleton()
    所示的指令重排序问题(如内存分配、初始化和引用赋值顺序被打乱),否则其他线程可能获取到尚未完全初始化的对象。

(3)静态内部类方式(推荐方案,线程安全且支持懒加载)

public class Singleton {
    private Singleton() {}

    // 静态内部类,只有在调用getInstance时才会加载
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}
  • 优点:利用类加载机制保证线程安全,实现懒加载,代码简洁,无显式加锁开销;
  • 原理:静态内部类在首次被引用时才加载,而类的加载过程由JVM保证线程安全,因此能确保实例唯一性。

(4)枚举方式(最佳实践,防反射与序列化攻击)

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("Singleton enum");
    }
}
  • 优点
    • 天然线程安全,实例在类加载时创建;
    • 防止通过反射强行创建新实例(编译器私有化构造方法);
    • 默认实现Serializable接口,反序列化不会生成新对象;
  • 缺点:不支持懒加载,实例随类加载即创建。

2. Java性能优化的常见策略

Java系统的性能优化需要从多个层面协同推进,主要包括代码层、JVM层、数据库层以及系统架构层,目标在于提升响应效率并降低资源消耗。

(1)代码层面优化

集合类使用建议

  • 创建ArrayList时应预先指定初始容量,以减少因动态扩容带来的性能损耗;
  • 对于频繁插入和删除操作,优先选用LinkedList;而对于高频读取场景,则更适合ArrayList;
  • 避免在循环体内调用
    ArrayList.add(index, element)
    这类时间复杂度为O(n)的操作,尤其是当集合规模较大时。

字符串处理优化

在Java开发中,字符串拼接操作应优先使用StringBuilder(适用于单线程场景)或StringBuffer(适用于多线程环境),以避免频繁使用String进行拼接所带来的性能问题。由于String是不可变对象,每次拼接都会创建新的临时对象,增加GC负担。

对于常量字符串的处理,应当充分利用JVM的字符串常量池机制,通过直接赋值或调用intern()方法复用已有对象,减少重复对象的生成。

String.intern()

在循环结构中,应注意以下优化点:

  • 尽量减少循环体内对象的创建,例如将集合的size()调用提前到循环外获取,避免每次迭代都执行方法调用;
for (int i = 0; i < list.size(); i++)
int size = list.size(); for (int i = 0; i < size; i++)
  • 避免在循环内部进行复杂的计算、函数调用或表达式运算,这些操作会显著降低循环效率;
  • 合理预防空指针异常,推荐使用Objects.requireNonNull()工具方法或Optional类来封装可能为空的对象,从而减少显式的null判断逻辑。
Objects.requireNonNull()
null

资源管理方面,务必采用try-with-resources语法结构,确保输入输出流、数据库连接等资源在使用完毕后能自动关闭,有效防止资源泄漏问题。

JVM层面的性能调优策略包括:

堆内存配置需合理设定-Xms(初始堆大小)和-Xmx(最大堆大小)。建议将两者设置为相同值,以避免运行时频繁扩容带来的性能波动。通常情况下,堆内存可设为物理内存的1/2至1/3之间。

-Xms
-Xmx

年轻代空间的划分也影响GC效率。可通过调整-XX:NewRatio参数控制年轻代与老年代的比例(默认为2:1),并结合应用中对象的实际存活时间进行优化。同时,Eden区与Survivor区的比例可通过-XX:SurvivorRatio参数调节(默认8:1)。

-XX:NewRatio
-XX:SurvivorRatio

垃圾收集器的选择应根据业务需求而定:

  • 若追求高吞吐量,可选用Parallel Scavenge收集器;
  • 若对响应延迟敏感,则推荐使用G1或ZGC等低延迟收集器。

此外,JVM默认开启逃逸分析功能(-XX:+DoEscapeAnalysis),该机制可识别未发生逃逸的对象,并将其分配在线程栈上而非堆中,从而减轻GC压力,提升执行效率。

-XX:+DoEscapeAnalysis

数据库性能优化的关键措施如下:

合理建立索引是提升查询速度的核心手段。应对WHERE条件、JOIN关联字段以及ORDER BY排序字段添加索引,但需注意避免过度建索引,以免影响INSERT、UPDATE和DELETE操作的性能。

SQL语句编写时应遵循以下原则:

  • 禁止使用SELECT *,仅选择必要的字段返回;
  • 避免在WHERE子句中对字段使用函数或表达式计算(如UPPER(name) = 'TEST'),这会导致索引失效;
WHERE DATE(create_time) = '2025-01-01'
  • 限制多表JOIN的数量,一般不超过三张表;对于大表连接,建议结合分页机制处理;
  • 批量操作优于逐条执行,例如在MyBatis中使用foreach实现批量插入或更新,可大幅提升效率。
batchInsert

数据库连接池的配置同样重要。以HikariCP为例,其maximumPoolSize建议设置为CPU核心数的2倍加1,既能充分利用系统资源,又可防止连接过多导致上下文切换开销。同时要确保连接正确释放,杜绝连接泄漏。

maximum-pool-size

架构设计层面的优化方向包括:

引入缓存机制可显著降低数据库负载。可采用Redis、Ehcache等缓存中间件存储热点数据,如用户会话信息、系统配置项等,提升访问速度。

将非关键路径的耗时任务异步化处理,例如日志记录、邮件发送等,可通过线程池或消息队列实现解耦,提高主流程响应性能。

通过Nginx、LVS等负载均衡技术,将请求均匀分发至多个服务节点,有效分散单机压力,增强系统的可用性与伸缩能力。

实施分布式部署策略,将单体应用拆分为多个微服务模块,按业务边界独立部署与扩展,有助于提升整体并发处理能力和系统稳定性。

总结

本文系统梳理了Java面试中的核心知识点体系,涵盖基础语法、集合框架、多线程编程、JVM原理、Spring生态、数据库操作、设计模式及性能调优等多个维度。每个模块均整合了高频考题、详尽解答与深层原理剖析,兼顾理论理解与实际应用。

在准备面试时,不应仅停留在记忆答案层面,更需深入理解底层机制,例如HashMap如何解决哈希冲突、Spring AOP基于动态代理的实现方式、JVM中各类GC算法的工作原理等。同时,结合个人项目经验阐述技术的实际落地场景,如线程池的具体参数配置过程、事务失效问题的排查思路等,能够显著增强回答的专业性和说服力。

建议重点掌握多线程并发控制、JVM调优策略以及Spring核心原理等进阶内容,这些往往是区分初级开发者与中高级工程师的关键能力指标。

二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

关键词:Java 最新版 面试题 jav collections

您需要登录后才可以回帖 登录 | 我要注册

本版微信群
扫码
拉您进交流群
GMT+8, 2026-1-19 13:34