一、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)方法获取指定位置的元素;Listget(int index) - Set依赖
hashCode()和equals()方法来确保元素的唯一性;Setequals()hashCode() - Map中key不允许重复(若重复则覆盖原有value),value可以重复;从JDK8开始,Map接口提供了
forEach()、computeIfAbsent()等便捷操作方法。MapforEach()computeIfAbsent()
java.utilCollectionListSetMap
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:适合频繁在中间插入或删除元素的场景,也常用于实现栈或队列(如消息队列)。
ThrowableAutoCloseable
3. HashMap的底层实现原理(JDK1.7 vs JDK1.8)?
答案:
HashMap是基于哈希表的Map实现,采用“数组 + 链表/红黑树”的结构设计,旨在兼顾查询与修改性能。
JDK1.7 实现方式:
- 底层结构:Entry数组 + 单向链表;
- 存储流程:
- 调用key的
hashCode()方法获取哈希码; - 经过哈希扰动函数处理以减少冲突;
hashCode() ^ (hashCode() >>> 16) - 通过
(n - 1) & hash计算出数组下标(n为数组长度); - 若对应位置为空,则直接插入;否则发生哈希冲突,采用“头插法”将新节点插入链表头部。
- 调用key的
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可能导致数据覆盖。
推荐替代方案:ConcurrentHashMap 或 Collections.synchronizedMap()。
ConcurrentHashMapCollections.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)
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
BLOCKED
synchronized
WAITING
Object.wait()
Thread.join()
LockSupport.park()
TIMED_WAITING
Thread.sleep(ms)
Object.wait(ms)
Thread.join(ms)
TERMINATED
核心状态转换路径:
- NEW → RUNNABLE:调用 start() 方法;
NEW
RUNNABLE
start()
TERMINATED
RUNNABLE
BLOCKED
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 | 拒绝策略,当线程池饱和(最大线程数已达上限且队列已满)时如何处理新提交的任务 |
线程池的工作流程:
- 当有新任务提交时,若当前运行线程数小于corePoolSize,则创建新的核心线程执行任务;
- 若核心线程已满,则将任务加入workQueue等待执行;
- 若队列也已满,但总线程数小于maximumPoolSize,则创建临时线程处理任务;
- 若线程数达到maximumPoolSize且队列已满,则触发拒绝策略;
- 当临时线程空闲时间超过keepAliveTime,会被自动销毁以释放资源。
常见的拒绝策略:
- AbortPolicy
:默认策略,直接抛出AbortPolicyRejectedExecutionException异常
;RejectedExecutionException - CallerRunsPolicy
:由调用者所在的线程(即提交任务的线程)直接执行该任务;CallerRunsPolicy - DiscardPolicy
:静默丢弃新任务,不抛异常;DiscardPolicy - DiscardOldestPolicy
:丢弃队列中最老的一个任务,然后尝试提交新任务。DiscardOldestPolicy
Executors工具类提供的常见线程池类型:
- newFixedThreadPool
:固定大小线程池,corePoolSize = maximumPoolSize = n,使用无界队列LinkedBlockingQueue;Executors.newFixedThreadPool(n) - newCachedThreadPool
:缓存线程池,corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,空闲线程存活60秒,使用SynchronousQueue;Executors.newCachedThreadPool() - newSingleThreadExecutor
:单线程池,保证任务按顺序执行,corePoolSize=1,maximumPoolSize=1,使用无界队列;Executors.newSingleThreadExecutor() - newScheduledThreadPool
:支持定时及周期性任务调度的线程池,底层使用DelayedWorkQueue。Executors.newScheduledThreadPool(n)
注意事项:
根据《阿里巴巴Java开发手册》建议,不应使用Executors创建线程池,原因在于:
- FixedThreadPool 和 SingleThreadExecutor
/newFixedThreadPool
:采用无界队列,可能导致大量任务积压,引发OutOfMemoryError(OOM);newSingleThreadExecutor - 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 对象的过程,整个流程分为五个阶段:
- 加载(Loading):通过类加载器读取 .class 文件内容,生成二进制字节流,并在堆中创建相应的 Class 类实例。
- 验证(Verification):确保字节码的安全性和合法性,包括格式检查、语法语义分析、符号引用验证等,防止恶意或错误代码破坏JVM运行环境。
- 准备(Preparation):为类的静态变量分配内存空间,并设置初始默认值(如 int 为 0,boolean 为 false),不涉及实例字段。
- 解析(Resolution):将符号引用(如类名、方法签名)转换为直接指向内存地址的直接引用。
- 初始化(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
依赖注入的三种常用方式:
- 构造函数注入:通过构造方法传入依赖对象,适合强制依赖项。
- Setter 方法注入:通过Setter方法设置依赖,灵活性较高,适用于可选依赖。
- 字段注入(基于注解):直接在字段上使用 @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容器初始化流程
- 加载配置源,例如XML文件或基于注解的配置类;
- 解析配置信息,扫描并识别带有@Component、@Service、@Controller等注解的类,将其元数据注册到BeanDefinitionRegistry中;
- 根据Bean定义进行实例化,默认采用单例模式(懒加载除外);
- 执行依赖注入(DI),利用BeanPostProcessor等后置处理器机制,自动装配所需的依赖对象;
- 初始化Bean:依次调用由@PostConstruct标注的方法、实现InitializingBean接口的afterPropertiesSet()方法,以及自定义的init-method;
- 此时Bean已准备就绪,可供应用程序正常使用;
- 当容器关闭时,触发销毁逻辑:执行@PreDestroy注解标记的方法、DisposableBean接口的destroy()方法,以及配置的destroy-method。
@Configuration
@Component
@Service
@Repository
@PostConstruct
afterPropertiesSet()
@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 方法有效; - 内部方法调用 —— 同一类中方法直接调用绕过代理,导致事务不生效;
- 异常类型不匹配 —— 默认仅对
RuntimeException和Error回滚,检查型异常需显式声明; - 使用
try-catch捕获异常但未重新抛出 —— 事务切面无法感知异常,导致无法触发回滚; - 传播行为设置不当 —— 如使用
NOT_SUPPORTED或NEVER会导致事务被挂起或拒绝; - 数据源未正确绑定事务管理器 ——
DataSourceTransactionManager未被Spring容器管理,导致事务配置无效。
RuntimeException:运行时异常;Error:错误类型;rollbackFor:用于指定回滚的异常类型;try-catch:捕获异常代码块;throw:未抛出异常语句;PlatformTransactionManager:事务管理器未注册到Spring上下文中。
六、数据库与MyBatis
1. JDBC的核心操作步骤
JDBC(Java Database Connectivity)是Java程序连接和操作数据库的标准API,基本流程包括:
- 加载数据库驱动:JDK 8及以上版本无需手动加载,DriverManager可自动发现驱动;
- 获取数据库连接:通过
DriverManager.getConnection()建立与数据库的连接; - 创建执行对象:使用 Connection 创建
Statement或PreparedStatement实例; - 执行SQL语句:
- 查询操作使用
executeQuery()方法; - 增删改操作调用
executeUpdate()方法;
- 查询操作使用
DriverManager.getConnection(url, username, password):建立数据库连接;executeQuery():执行查询语句;executeUpdate():执行增删改操作。数据库操作流程主要包括以下几个步骤:
- 加载驱动:针对 MySQL 8.0 及以上版本,需加载的驱动类为 com.mysql.cj.jdbc.Driver。
- 建立连接:通过 JDBC URL、用户名和密码获取数据库连接对象 Connection。
- 创建 PreparedStatement:使用预编译 SQL 语句,支持参数占位符(?),可有效防止 SQL 注入攻击。
- 执行查询:调用 executeQuery() 方法发送 SQL 请求并返回结果集 ResultSet。
- 遍历处理结果集:通过 while 循环调用 rs.next() 逐行读取数据,并使用 getXXX 方法提取字段值。
- 资源释放:在 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。
工作流程概述:
- 配置加载阶段:
负责读取 mybatis-config.xml 和各个 Mapper.xml 文件,解析出数据库连接信息、SQL 定义、映射关系等元数据。SqlSessionFactoryBuilder - 创建 SqlSessionFactory:根据解析后的 Configuration 构建 SqlSessionFactory 实例,一般在整个应用生命周期中仅创建一次。
- 获取 SqlSession:通过 SqlSessionFactory 调用
方法创建 SqlSession,默认情况下事务不会自动提交。openSession() - 获取 Mapper 代理对象:调用 SqlSession 的
方法,获得指定 Mapper 接口的代理实现。getMapper(Mapper接口.class) - 执行 SQL 操作:调用 Mapper 接口中的方法时,MyBatis 自动查找对应 SQL 并执行 JDBC 操作。
- 结果集映射:根据 resultType 或 resultMap 的配置,将 ResultSet 自动转换为所需的 Java 实体对象。
- 事务管理:操作完成后,通过 SqlSession 调用
提交事务,或调用commit()
回滚事务。rollback() - 关闭会话:及时关闭 SqlSession 以释放数据库连接等底层资源。
MyBatis的一级缓存与二级缓存机制解析:
为了提升数据库操作性能,MyBatis内置了缓存功能,能够有效减少对数据库的重复查询。该缓存体系主要分为两个层级:一级缓存和二级缓存。
一级缓存(SqlSession级别,默认启用)
- 作用范围:在同一个SqlSession中,若多次执行相同的SQL语句且参数一致,则仅首次访问数据库,后续结果将从缓存中读取。
- 实现方式:SqlSession内部通过一个HashMap来维护缓存数据,其中键值由SQL语句、参数、RowBounds以及当前环境等信息共同构成,对应的值为查询返回的结果集。
- 失效条件:
- 执行任何insert、update或delete操作时,一级缓存会被自动清空;
- 调用相关方法手动清除缓存;
- 当SqlSession被关闭或事务提交后,缓存也随之失效。
SqlSession.clearCache()
二级缓存(Mapper级别,需手动开启)
- 作用范围:跨多个SqlSession,在同一命名空间(即同一个Mapper接口)下共享缓存数据。
- 启用步骤:
- 在全局配置文件mybatis-config.xml中确认已开启缓存支持(此项通常默认开启,可不显式配置);
- 在对应的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;
- 避免在循环体内调用
这类时间复杂度为O(n)的操作,尤其是当集合规模较大时。ArrayList.add(index, element)
字符串处理优化
在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核心原理等进阶内容,这些往往是区分初级开发者与中高级工程师的关键能力指标。


雷达卡


京公网安备 11010802022788号







