导图社区 Java 核心技术面试精讲
这是一篇关于Java 核心技术面试精讲的思维导图,主要内容包括:Java基础,Java进阶,Java安全基础,Java性能基础,Java应用开发扩展。
编辑于2025-11-07 15:19:22Java 核心技术面试精讲
Java基础
第2讲 | Exception和Error有什么区别?
Java异常处理机制
Exception和Error
Throwable
Exception
可检查异常
不检查异常(运行时异常)
Error
异常处理的最佳实践
不捕获通用异常
不生吞异常
“生吞异常”,本质上其实是掩盖问题。如果我们不把异常抛出来,或者也没有输出到日志(Logger)之类,程序可能在后续代码以不可控的方式结束。没人能够轻易判断究竟是哪里抛出了异常,以及是什么原因产生了异常。
Throw early, catch late原则
错误发生时及早抛出
在很多情况下,由于没有第一时间暴露出问题,异常堆栈信息可能非常令人费解,往往需要相对复杂的定位。在发现问题的时候,第一时间抛出,能够更加清晰地反映问题。
不推荐写法
推荐写法
子主题
自定义异常的考虑
异常处理的性能开销
try-catch代码段的性能开销
实例化异常的栈快照开销
异常处理的影响因素
不同编程范式对异常处理的影响
第3讲 | 谈谈final、finally、 finalize有什么不同?
final
修饰类、方法、变量
不可继承、不可修改、不可重写
推荐使用final关键字
修饰类、方法、变量
final变量的不可变效果
不可修改的变量
不可修改的对象
实现方式
将 class 自身声明为 final,这样别人就不能扩展来绕过限制了
将所有成员变量定义为 private 和 final,并且不要实现 setter 方法
通常构造对象时,成员变量使用深度拷贝来初始化,而不是直接赋值,这是一种防御措施,因为你无法确定输入对象不被其他人修改
如果确实需要实现 getter 方法,或者其他可能会返回内部状态的方法,使用 copy-on-write 原则,创建私有的 copy
final的性能好处
final 也许会有性能的好处,很多文章或者书籍中都介绍了可在特定场景提高性能,比如,利用 final 可能有助于 JVM 将方法进行内联,可以改善编译器进行条件编译的能力等等。坦白说,很多类似的结论都是基于假设得出的,比如现代高性能 JVM(如 HotSpot)判断内联未必依赖 final 的提示,要相信 JVM 还是非常智能的。类似的,final 字段对性能的影响,大部分情况下,并没有考虑的必要。
finally
保证重点代码一定被执行
try-finally、try-catch-finally
推荐使用Java 7中的try-with-resources语句
finalize
java.lang.Object的方法
对象在被垃圾收集前完成特定资源的回收
不推荐使用
第4讲 | 强引用、软引用、弱引用、幻象引用有什么区别?
第5讲 | String、StringBuffer、StringBuilder有什么区别?
典型回答
String
Immutable类
拼接、裁剪字符串会产生新的String对象
StringBuffer
线程安全的可修改字符序列
使用synchronized关键字实现线程安全
StringBuilder
与StringBuffer功能相似
不具备线程安全性
通常进行字符串拼接的首选
知识扩展
1. 字符串设计和实现考量
String是Immutable类
StringBuffer和StringBuilder底层都利用可修改的数组
需要根据实际需求选择合适的字符串拼接方式
2. 字符串缓存
使用intern()方法可以缓存字符串
JDK 8u20后推出G1 GC下的字符串排重
JVM参数
查看字符串缓存池大小
-XX:+PrintStringTableStatistics
手动调整字符串缓存池大小
-XX:StringTableSize=N
3. String自身的演化
Java 9引入了Compact Strings的设计
带来更小的内存占用、更快的操作速度
编码思想
1.有人说“过早优化是万恶之源”,考虑可靠性、正确性和代码可读性才是大多数应用开发最重要的因素
第6讲 | 动态代理是基于什么原理?
反射机制
Java反射机制的基础功能
获取类定义、属性和方法
调用方法、构造对象
运行时修改类定义
反射机制的应用场景
O/R Mapping框架
绕过API访问控制
Java 9对反射访问的限制
setAccessible方法的使用限制
模块化系统对反射访问的限制
动态代理
代理模式的作用
解耦调用者与实现者
动态代理的发展
静态代理引入的额外工作
动态代理提高生产力
JDK动态代理的实现
以接口为中心的限制
cglib动态代理的优势
克服对接口的依赖
高性能
理解代理模式和装饰器模式的区别
代理模式
在不改变原始类(被代理类)代码的情况下,通过引入代理类来给原始类附加功能。(控制访问,附加功能,而非加强功能)
装饰器模式(加强功能)
代理类附加的是跟原始类无关的功能,而装饰器类附加的是跟原始类相关的增强功能。 推荐王争的《设计模式之美》
第7讲 | int和Integer有什么区别?
Integer包装类
自动装箱和自动拆箱功能
发生在编译阶段
使用静态工厂方法valueOf
缓存机制
缓存范围
默认缓存范围是-128到127
JVM参数调整缓存范围
-XX:AutoBoxCacheMax=N
这些实现,都体现在java.lang.Integer源码之中,并实现在 IntegerCache 的静态初始化块里。
其他包装类型也有缓存机制
Boolean
缓存了 true/false 对应实例,确切说,只会返回两个常量实例 Boolean.TRUE/FALSE
Short
同样是缓存了 -128 到 127 之间的数值
Byte
数值有限,所以全部都被缓存
Character
缓存范围’\u0000’ 到 ‘\u007F’
知识扩展
理解自动装箱、拆箱
自动装箱实际上算是一种语法糖。什么是语法糖?可以简单理解为 Java 平台为我们自动进行了一些转换,保证不同的写法在运行时等价,它们发生在编译阶段,也就是生成的字节码是一致的。
第11讲 | Java提供了哪些IO方式? NIO如何实现多路复用?
第13讲 | 谈谈接口和抽象类有什么区别?
接口
接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。
抽象类
抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。
知识扩展
面向对象设计
封装
封装的目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。
继承
继承是代码复用的基础机制,类似于我们对于马、白马、黑马的归纳总结。但要注意,继承可以看作是非常紧耦合的一种关系,父类代码修改,子类行为也会变动。在实践中,过度滥用继承,可能会起到反效果
多态
设计原则(S.O.L.I.D 原则)
单一职责(Single Responsibility)
类或者对象最好是只有单一职责,在程序设计中如果发现某个类承担着多种义务,可以考虑进行拆分。
开关原则(Open-Close, Open for extension, close for 开关原则(Open-Close, Open for extension, close for modification)
设计要对扩展开放,对修改关闭。换句话说,程序设计应保证平滑的扩展性,尽量避免因为新增同类功能而修改已有实现,这样可以少产出些回归(regression)问题。
里氏替换(Liskov Substitution)
这是面向对象的基本要素之一,进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类替换。
接口分离(Interface Segregation)
我们在进行类和接口设计时,如果在一个接口里定义了太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏了程序的内聚性。对于这种情况,可以通过拆分成功能单一的多个接口,将行为进行解耦。在未来维护中,如果某个接口设计有变,不会对使用其他接口的子类构成影响。
依赖反转(Dependency Inversion)
实体应该依赖于抽象而不是实现。也就是说高层次模块,不应该依赖于低层次模块,而是应该基于抽象。实践这一原则是保证产品代码之间适当耦合度的法宝。
第14讲 | 谈谈你知道的设计模式?
23种设计模式
创建型模式:是对对象创建过程的各种问题和解决方案的总结
各种工厂模式(Factory、Abstract Factory)
单例模式(Singleton)
构建器模式(Builder)
原型模式(ProtoType)
结构型模式:是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验
桥接模式(Bridge)
适配器模式(Adapter)
装饰者模式(Decorator)
代理模式(Proxy)
组合模式(Composite)
外观模式(Facade)
享元模式(Flyweight)
行为型模式:是从类或对象之间交互、职责划分等角度总结的模式
策略模式(Strategy)
解释器模式(Interpreter)
命令模式(Command)
观察者模式(Observer)
迭代器模式(Iterator)
模板方法模式(Template Method)
访问者模式(Visitor)
Java进阶
第15讲 | synchronized和ReentrantLock有什么区别呢?
synchronized和ReentrantLock有什么区别?
典型回答
synchronized是Java内建的同步机制
可以修饰方法或代码块
ReentrantLock是Java 5提供的锁实现
通过调用lock()方法获取
提供了很多实用的方法
ReentrantLock
可重入性
可实现公平锁
...
性能比较
synchronized 和 ReentrantLock 的性能不能一概而论,早期版本 synchronized 在很多场景下性能相差较大,在后续版本进行了较多改进,在低竞争场景中表现可能优于 ReentrantLock
考点分析
并发编程的常见基础题
掌握的基本知识
理解线程安全
synchronized、ReentrantLock的基本使用
知识扩展
理解线程安全
保证多线程环境下共享的可修改状态的正确性
保证原子性、可见性、有序性
深入底层原理
掌握 synchronized、ReentrantLock 底层实现;理解锁膨胀、降级;理解偏斜锁、自旋锁、轻量级锁、重量级锁等概念
掌握并发包中 java.util.concurrent.lock 各种不同实现和案例分析
ReentrantLock如何实现可重入
每次获取锁的时候,看下当前维护的那个线程和当前请求的线程是否一样,一样就可重入了
概念解析
线程安全
1.线程安全是一个多线程环境下正确性的概念,也就是保证多线程环境下共享的、可修改的状态的正确性,这里的状态反映在程序中其实可以看作是数据。
2.换个角度来看,如果状态不是共享的,或者不是可修改的,也就不存在线程安全问题,进而可以推理出保证线程安全的两个办法
封装:通过封装,我们可以将对象内部状态隐藏、保护起来
不可变:还记得我们在专栏第 3 讲强调的 final 和 immutable 吗,就是这个道理,Java 语言目前还没有真正意义上的原生不可变,但是未来也许会引入。
公平锁
设计公平锁的原因
如果使用 synchronized,我们根本无法进行公平性的选择,其永远是不公平的,这也是主流操作系统线程调度的选择。通用场景中,公平性未必有想象中的那么重要,Java 默认的调度策略很少会导致 “饥饿”发生。与此同时,若要保证公平性则会引入额外开销,自然会导致一定的吞吐量下降。所以,我建议只有当你的程序确实有公平性需要的时候,才有必要指定它。
公平性是减少线程“饥饿”(个别线程长期等待锁,但始终无法获取)情况发生的一个办法
本章内容精彩评论
所有的Lock都是基于AQS来实现了。AQS和Condition各自维护了不同的队列,在使用lock和condition的时候,其实就是两个队列的互相移动。如果我们想自定义一个同步器,可以实现AQS。它提供了获取共享锁和互斥锁的方式,都是基于对state操作而言的。ReentranLock这个是可重入的。其实要弄明白它为啥可重入的呢,咋实现的呢。其实它内部自定义了同步器Sync,这个又实现了AQS,同时又实现了AOS,而后者就提供了一种互斥锁持有的方式。其实就是每次获取锁的时候,看下当前维护的那个线程和当前请求的线程是否一样,一样就可重入了。
第16讲 | synchronized底层如何实现?什么是锁的升级、降级?
Monitor对象
依赖操作系统内部的互斥锁
Java 6之前的实现
现代JDK中的改进
三种不同的Monitor实现
偏斜锁
轻量级锁
重量级锁
锁的升级、降级
JVM优化synchronized运行的机制
偏斜锁、轻量级锁、重量级锁的切换
源码层面分析
Runtime相关功能实现
UseBiasedLocking检查
fast_enter和slow_enter方法
ObjectSynchronizer类
其他锁类型
ReadWriteLock
StampedLock
问题扩展
自旋锁的作用和使用场景
精彩评论
自旋锁:竞争锁的失败的线程,并不会真实的在操作系统层面挂起等待,而是JVM会让线程做几个空循环(基于预测在不久的将来就能获得),在经过若干次循环后,如果可以获得锁,那么进入临界区,如果还不能获得锁,才会真实的将线程在操作系统层面进行挂起。 适用场景:自旋锁可以减少线程的阻塞,这对于锁竞争不激烈,且占用锁时间非常短的代码块来说,有较大的性能提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗。如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,线程自旋的消耗大于线程阻塞挂起操作的消耗,造成cpu的浪费。 同时自旋也是导致synchronized为非公平锁的原因之一,当一个新的线程竞争当前锁的对象时会先自旋,而不是进入等待队列,这对等待队列里面的线程是不公平的。
第17讲 | 一个线程两次调用start()方法会出现什么情况?
线程状态
第18讲 | 什么情况下Java程序会产生死锁?如何定位、修复?
常见出现死锁情况
定位死锁
jstack工具
jps
获取Java进程ID
jstack ID
查看进程堆栈信息
ThreadMXBean
我们可以通过Java 提供的标准API ThreadMXBean 开发方便线程观察或排除服务死锁问题的接口,ThreadMXBean其直接就提供了 findDeadlockedThreads() 方法用于定位。
线程状态
并发相关元素
预防死锁
避免使用多个锁
设计好锁的获取顺序
使用带超时的方法
静态代码分析
第19讲 | Java并发包提供了哪些并发工具类?
Java并发包提供了哪些并发工具类?
ava.util.concurrent及其子包
高级同步结构
CountDownLatch
CyclicBarrier
Semaphore
线程安全容器
ConcurrentHashMap
ConcurrentSkipListMap
CopyOnWriteArrayList
并发队列实现
BlockingQueue
ArrayBlockingQueue
SynchronousQueue
PriorityBlockingQueue
Executor框架
知识扩展
CountDownLatch vs. CyclicBarrier
CopyOnWrite容器原理
第20讲 | 并发包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别?
知识扩展
线程安全队列一览
ArrayBlockingQueue
是最典型的的有界队列,其内部以final的数组保存数据,数组的大小就决定了队列的边界
LinkedBlockingQueue
其行为和内部代码都是基于有界的逻辑实现的,只不过如果我们没有在创建队列时就指定容量,那么其容量限制就自动被设置为Integer.MAX_VALUE,成为了无界队列
SynchronousQueue
这是一个非常奇葩的队列实现,每个删除操作都要等待插入操作,反之每个插入操作也都要等待删除动作
队列使用场景与典型用例
生产者-消费者场景
使用BlockingQueue来实现,由于其提供的等待机制,我们可以少操心很多协调工作
第21讲 | Java并发类库提供的线程池有哪几种? 分别有什么特点?
Java并发类库提供的线程池有哪几种? 分别有什么特点?
Executors
newCachedThreadPool()
用于处理大量短时间工作任务
试图缓存线程并重用
使用SynchronousQueue作为工作队列
newFixedThreadPool(int nThreads)
重用指定数目的线程
使用无界的工作队列
newSingleThreadExecutor()
工作线程数目被限制为1
无界的工作队列
newSingleThreadScheduledExecutor()
创建单一工作线程的ScheduledExecutorService
newScheduledThreadPool(int corePoolSize)
创建多个工作线程的ScheduledExecutorService
newWorkStealingPool(int parallelism)
Java 8新增
并行地处理任务,不保证处理顺序
ForkJoinPool
并行流parallelStream底层也是用了ForkJoin,并行流适合没有线程安全问题、较单纯的数据处理任务
Executor框架
Executor
初衷是将任务提交和任务执行细节解耦
提供execute方法
ExecutorService
提供service的管理功能
提供更全面的提交任务机制
提供返回结果的submit方法
线程池的设计特点
高度的可调节性和灵活性
Executor 框架的基本组成
线程池的构造函数
corePoolSize
maximumPoolSize
keepAliveTime
TimeUnit
workQueue
ThreadFactory
RejectedExecutionHandler
核心参数解读可参考链接
线程池的状态
RUNNING
SHUTDOWN
STOP
TIDYING
TERMINATED
线程池实践
避免任务堆积
避免过度扩展线程
避免死锁等同步问题
尽量避免在使用线程池时操作ThreadLocal
线程池大小的选择策略
CPU计算任务
I/O操作任务
考虑系统资源限制
第22讲 | AtomicInteger底层实现原理是什么?如何在自己的产品代码中应用CAS操作?
AtomicInteger底层实现原理
CAS操作细节
CAS的底层实现
CAS的应用
AQS(AbstractQueuedSynchronizer)原理解析
第23讲 | 请介绍类加载过程,什么是双亲委派模型?
第24讲 | 有哪些方法可以在运行时动态生成一个Java类?
第25讲 | 谈谈JVM内存区域的划分,哪些区域可能发生OutOfMemoryError?
第26讲 | 如何监控和诊断JVM堆内和堆外内存使用?
JVM内存区域划分
堆内存
堆外内存
对内存区域划分
监控和诊断方法
图形化工具
JConsole
VisualVM
命令行工具
jstat
jmap
堆转储分析工具
jhat
Eclipse MAT
服务器提供的功能
GC日志
特殊部分:堆外内存中的直接内存
JDK的Native Memory Tracking(NMT)特性
TLAB
Thread Local Allocation Buffer
概念阐述
从内存模型而不是垃圾收集的角度,对 Eden 区域继续进行划分,Hotspot JVM 还有一个概念叫做 Thread Local Allocation Buffer(TLAB),据我所知所有 OpenJDK 衍生出来的 JVM 都提供了 TLAB 的设计。这是 JVM 为每个线程分配的一个私有缓存区域,否则,多线程同时分配内存时,为避免操作同一地址,可能需要使用加锁等机制,进而影响分配速度,你可以参考下面的示意图。从图中可以看出,TLAB 仍然在堆上,它是分配在 Eden 区域内的。其内部结构比较直观易懂,start、end 就是起始地址,top(指针)则表示已经分配到哪里了。所以我们分配新对象,JVM 就会移动 top,当 top 和 end 相遇时,即表示该缓存已满,JVM 会试图再从 Eden 里分配一块儿
图示
JVM基本参数
最大堆体积
-Xmx value
初始的最小堆体积
-Xms value
老年代和新生代的比例
默认情况下,这个数值是 2,意味着老年代是新生代的 2 倍大;换句话说,新生代是堆大小的 1/3
-XX:NewRatio=value
直接指定新生代大小
-XX:NewSize=value
指定Eden和幸存区比例
Eden 和 Survivor 的大小是按照比例设置的,如果 SurvivorRatio 是 8,那么 Survivor 区域就是 Eden 的 1/8 大小,也就是新生代的 1/10,因为 YoungGen=Eden + 2*Survivor
-XX:SurvivorRatio=value
第27讲 | Java常见的垃圾收集器有哪些?
典型回答
Serial GC
-XX:+UseSerialGC
ParNew GC
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
CMS GC
Parallel GC
-XX:+UseParallelGC
-XX:MaxGCPauseMillis=value
-XX:GCTimeRatio=N
G1 GC
垃圾收集的算法
引用计数算法
可达性分析
复制算法
标记-清除算法
标记-整理算法
第28讲 | 谈谈你的GC调优思路?
第29讲 | Java内存模型中的happen-before是什么?
第30讲 | Java程序运行在Docker等容器环境有哪些新问题?
Java安全基础
第31讲 | 你了解Java应用开发中的注入攻击吗?
Java应用开发中的注入攻击
主要的注入式攻击途径
SQL注入攻击
解决方案
在数据输入阶段,填补期望输入和可能输入之间的鸿沟。可以进行输入校验,限定什么类型的输入是合法的,例如,不允许输入标点符号等特殊字符,或者特定结构的输入
在 Java 应用进行数据库访问时,如果不用完全动态的 SQL,而是利用 PreparedStatement,可以有效防范 SQL 注入。不管是 SQL 注入,还是 OS 命令注入,程序利用字符串拼接生成运行逻辑都是个可能的风险点!
操作系统命令注入
Java 语言提供了类似 Runtime.exec(…) 的 API,可以用来执行特定命令
XML注入攻击
DOS(Denial-Of-Service attack)
利用哈希碰撞发起拒绝服务攻击(DOS,Denial-Of-Service attack),常见的场景是,攻击者可以事先构造大量相同哈希值的数据,然后以 JSON 数据的形式发送给服务器端,服务器端在将其构建成为 Java 对象过程中,通常以 Hastable 或 HashMap 等形式存储,哈希碰撞将导致哈希表发生严重退化,算法复杂度可能上升一个数量级,进而耗费大量 CPU 资源。
将目标网站的带宽或者其他资源耗尽,导致其无法响应正常用户的请求。
第32讲 | 如何写出安全的Java代码?
序列化带来的安全问题
处理方式
1.敏感信息不要被序列化!在编码中,建议使用 transient 关键字将其保护起来
2.反序列化中,建议在 readObject 中实现与对象构件过程相同的安全检查和数据检查
Java性能基础
第33讲 | 后台服务出现明显“变慢”,谈谈你的诊断思路?
第34讲 | 有人说“Lambda能让Java程序慢30倍”,你怎么看?
第35讲 | JVM优化Java代码时都做了什么?
运行时优化
解释执行
动态编译
锁机制
内存分配机制
模版解释器
内联缓存
即时编译器(JIT)优化
方法内联
逃逸分析
投机性优化
栈上替换技术
JVM优化范畴
阻塞代码
调优角度和手段
调整热点代码门限值
调整Code Cache大小
调整编译器线程数
优化安全点
验证final关键字影响性能
Java应用开发扩展
第36讲 | 谈谈MySQL支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景?
第37讲 | 谈谈Spring Bean的生命周期和作用域?
Spring Bean的生命周期和作用域
Spring Bean的生命周期
创建过程
实例化Bean对象
设置Bean属性
注入Bean对容器基础设施层面的依赖
BeanNameAware
BeanFactoryAware
ApplicationContextAware
调用BeanPostProcessor的前置初始化方法postProcessBeforeInitialization
调用InitializingBean接口的afterPropertiesSet方法
调用Bean自身定义的init方法
调用BeanPostProcessor的后置初始化方法postProcessAfterInitialization
销毁过程
调用DisposableBean的destroy方法
调用Bean自身定制的destroy方法
Spring Bean的作用域
Singleton
为每个IOC容器创建唯一的一个Bean实例
Prototype
针对每个getBean请求,容器都会单独创建一个Bean实例
Request (Web容器)
为每个HTTP请求创建单独的Bean实例
Session (Web容器)
Bean实例的作用域是Session范围
GlobalSession (Portlet容器)
提供一个全局性的HTTP Session
Spring AOP的设计和实现细节
Aspect
Join Point
Pointcut
Advice
第38讲 | 对比Java标准NIO类库,你知道Netty是如何实现更高性能的吗?
第39讲 | 谈谈常用的分布式ID的设计方案?Snowflake是否受冬令时切换影响?
主题
主题