面试准备-技能栈八股文大全
本文档整理了480+道高频面试题,涵盖Java、Spring、MySQL、Redis、消息队列、容器化等核心技术栈。 每道题都包含详细答案、原理图解、源码分析、实战场景、常见误区和追问,适合面试前系统复习。
📚 目录
难度说明
🟢 基础:校招必考,工作1-3年
🟡 进阶:社招常见,工作3-5年
🔴 高级:架构师级别,工作5年+
⭐ 高频题:面试中出现频率>80%
Java基础与JVM 一、Java基础篇(30题) 🟢⭐ 1. 说说Java中的==和equals()的区别 面试官视角 :考察对Java基础概念的理解,以及对引用类型和值类型的认知。
详细答案 :
==和equals()是Java中两种不同的比较方式,它们的核心区别在于比较的内容不同:
1. ==运算符
对于基本数据类型(byte、short、int、long、float、double、char、boolean),==比较的是值是否相等
对于引用数据类型,==比较的是两个对象的内存地址是否相同,即是否指向同一个对象
2. equals()方法
equals()是Object类的方法,所有类都继承了这个方法
Object类中的默认实现就是使用==比较
但很多类重写了equals()方法,如String、Integer等,改为比较对象的内容
原理图解 :
1 2 3 4 5 6 7 8 9 10 11 12 13 内存模型: 栈区(Stack) 堆区(Heap) +--------+ +-----------------+ | s1 ---------> | "Hello" | 地址: 0 x100 +--------+ +-----------------+ | s2 ---------> | "Hello" | 地址: 0 x200 +--------+ +-----------------+ | s3 ---------> | "Hello" | 地址: 0 x100 (同s1) +--------+ +-----------------+s1 == s2 -> false (不同地址)s1 .equals(s2) -> true (内容相同)s1 == s3 -> true (相同地址)
源码分析 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public boolean equals (Object obj) { return (this == obj); }public boolean equals (Object anObject) { if (this == anObject) { return true ; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0 ; while (n-- != 0 ) { if (v1[i] != v2[i]) return false ; i++; } return true ; } } return false ; }
实战场景 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 String s1 = new String ("Hello" );String s2 = new String ("Hello" ); System.out.println(s1 == s2); System.out.println(s1.equals(s2)); String s3 = "Hello" ;String s4 = "Hello" ; System.out.println(s3 == s4); class Person { String name; int age; }Person p1 = new Person ("张三" , 25 );Person p2 = new Person ("张三" , 25 ); System.out.println(p1.equals(p2));
常见误区 :
误区1 :认为equals()一定比较内容
纠正:如果类没有重写equals(),默认还是比较地址
误区2 :忽略字符串常量池的影响
纠正:直接赋值的字符串字面量会放入常量池,相同内容共享同一对象
误区3 :重写equals()后忘记重写hashCode()
追问1:为什么重写equals()必须重写hashCode()?
答:这是Java的契约规定。如果两个对象通过equals()比较相等,那么它们的hashCode()必须返回相同的值。原因是:
HashMap、HashSet等集合使用hashCode()来确定对象的存储位置
查找时先比较hashCode(),相同才调用equals()
如果不重写hashCode(),即使equals()返回true,两个对象的hashCode()可能不同,导致在HashMap中被当作不同的键
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Person { String name; int age; @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode () { return Objects.hash(name, age); } }
追问2:String的equals()为什么先比较地址?
答:这是一种性能优化。如果两个字符串引用指向同一个对象(地址相同),那么它们肯定相等,可以直接返回true,避免后续的逐字符比较,提高效率。这种优化在字符串常量池场景下特别有效。
🟢⭐ 2. Java中的String为什么设计成不可变的? 面试官视角 :考察对String内部实现的理解,以及不可变性带来的优势。
详细答案 :
String被设计成不可变类(Immutable Class)有多个重要原因,这是一个经过深思熟虑的设计决策:
1. 安全性
String经常用作参数传递,如数据库连接URL、文件路径、网络连接等
如果String可变,恶意代码可能在传递过程中修改其内容,造成安全隐患
不可变保证了String作为参数传递时的线程安全
2. 字符串常量池优化
JVM维护了一个字符串常量池(String Pool)
相同内容的字符串字面量共享同一个对象,节省内存
只有不可变才能安全地共享,否则修改会影响所有引用
3. 线程安全
不可变对象天生线程安全,多线程访问无需同步
避免了同步带来的性能开销
4. hashCode缓存
String的hashCode()结果可以缓存,不需要每次重新计算
这使得String作为HashMap的key时性能更优
5. 类加载器安全
类加载器使用String作为类名参数
不可变保证了类加载的安全性
原理图解 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 String不可变性实现:+-------------------+ | String对象 | |-------------------| | final char[] value| <- final修饰,引用不可变 | private int hash | <- hashCode缓存 |-------------------|| 无setter方法 | <- 关键设计 +-------------------+ 字符串常量池:常量池(Heap) +------------------+ | "Hello" (0x100) | <--- s1指向+------------------+ s2也指向(共享)| "World" (0x200) | +------------------+ 如果可变会怎样: String s1 = "Hello"; String s2 = "Hello"; // 共享同一对象 s1.modify("Hi"); // 假设有这个方法
源码分析 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public final class String implements java .io.Serializable, Comparable<String>, CharSequence { private final char value[]; private int hash; public String substring (int beginIndex, int endIndex) { if (beginIndex < 0 ) { throw new StringIndexOutOfBoundsException (beginIndex); } if (endIndex > value.length) { throw new StringIndexOutOfBoundsException (endIndex); } int subLen = endIndex - beginIndex; if (subLen < 0 ) { throw new StringIndexOutOfBoundsException (subLen); } return ((beginIndex == 0 ) && (endIndex == value.length)) ? this : new String (value, beginIndex, subLen); } public String concat (String str) { int otherLen = str.length(); if (otherLen == 0 ) { return this ; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String (buf, true ); } }
实战场景 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 String s1 = "Hello" ;String s2 = "Hello" ;String s3 = "Hel" + "lo" ; System.out.println(s1 == s2); System.out.println(s1 == s3); String s4 = "Hello" ;String s5 = s4.toUpperCase(); System.out.println(s4); System.out.println(s5); public void processUrl (String url) { new Thread (() -> { connect(url); }).start(); } Map<String, User> userMap = new HashMap <>();String key = "user123" ; userMap.put(key, user);
常见误区 :
误区1 :认为String不可变是因为final修饰
纠正:final只保证引用不变,不保证内容不变。String不可变是多重设计的结果
误区2 :大量字符串拼接也用String
纠正:应该使用StringBuilder或StringBuffer,避免创建大量临时对象
误区3 :通过反射可以修改String的value数组,所以String可变
纠正:虽然技术上可行,但这是不安全的hack,违背了设计契约
追问1:既然String不可变,为什么还有StringBuilder和StringBuffer?
答:正因为String不可变,每次修改都会创建新对象,在频繁修改字符串的场景下性能很差。例如:
1 2 3 4 5 6 7 8 9 10 11 12 String s = "" ;for (int i = 0 ; i < 10000 ; i++) { s += i; }StringBuilder sb = new StringBuilder ();for (int i = 0 ; i < 10000 ; i++) { sb.append(i); }String result = sb.toString();
StringBuilder和StringBuffer内部使用可变的char数组,支持原地修改,适合频繁拼接场景。区别是StringBuffer是线程安全的(synchronized),StringBuilder不是。
追问2:JDK 9为什么把String的char[]改成byte[]?
答:这是Compact Strings优化。大多数String只包含Latin1字符(单字节),用char(2字节)存储浪费空间。JDK 9引入了coder字段标识编码方式:
LATIN1(0):每个字符1字节
UTF16(1):每个字符2字节
这样可以节省约50%的内存,提升性能。源码示例:
1 2 3 4 5 6 private final byte [] value;private final byte coder; static final byte LATIN1 = 0 ;static final byte UTF16 = 1 ;
🟡⭐ 3. 深入理解Java中的四种引用类型 面试官视角 :考察对JVM内存管理和垃圾回收机制的深入理解,以及在实际场景中的应用能力。
详细答案 :
Java提供了四种引用类型,用于更精细地控制对象的生命周期和垃圾回收行为:强引用、软引用、弱引用、虚引用。
1. 强引用(Strong Reference)
最常见的引用类型,如 Object obj = new Object()
只要强引用存在,GC永远不会回收被引用的对象
即使内存不足抛出OOM,也不会回收强引用对象
2. 软引用(Soft Reference)
使用SoftReference类实现
在内存充足时,GC不会回收软引用对象
在内存不足(即将OOM)时,GC会回收软引用对象
适合实现内存敏感的缓存
3. 弱引用(Weak Reference)
使用WeakReference类实现
无论内存是否充足,GC时一旦发现弱引用对象,就会回收
生命周期只能存活到下一次GC之前
适合实现短期缓存、监听器管理等
4. 虚引用(Phantom Reference)
使用PhantomReference类实现
最弱的引用类型,形同虚设
无法通过虚引用获取对象实例(get()永远返回null)
必须配合引用队列(ReferenceQueue)使用
用于跟踪对象被GC的状态,做清理工作
原理图解 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 GC回收策略: 内存充足时: +-------------+ +-------------+ +-------------+ +-------------+ | 强引用对象 | --> | 软引用对象 | --> | 弱引用对象 | --> | 虚引用对象 | | 不回收 | | 不回收 | | 可能回收 | | 跟踪回收 | +-------------+ +-------------+ +-------------+ +-------------+ 内存不足时(即将OOM): +-------------+ +-------------+ +-------------+ +-------------+ | 强引用对象 | --> | 软引用对象 | --> | 弱引用对象 | --> | 虚引用对象 | | 不回收 | | 回收 | | 回收 | | 跟踪回收 | +-------------+ +-------------+ +-------------+ +-------------+ 引用队列机制: 对象 --> Reference --> ReferenceQueue | | GC回收对象后 v Reference进入队列,可以做清理工作
源码分析 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 public class SoftReferenceExample { public static void main (String[] args) { Object obj = new Object (); SoftReference<Object> softRef = new SoftReference <>(obj); obj = null ; Object retrieved = softRef.get(); if (retrieved != null ) { System.out.println("对象还在" ); } else { System.out.println("对象已被回收" ); } } }public class WeakReferenceExample { public static void main (String[] args) { Object obj = new Object (); WeakReference<Object> weakRef = new WeakReference <>(obj); obj = null ; System.gc(); if (weakRef.get() == null ) { System.out.println("弱引用对象已被回收" ); } } }public class PhantomReferenceExample { public static void main (String[] args) throws InterruptedException { Object obj = new Object (); ReferenceQueue<Object> queue = new ReferenceQueue <>(); PhantomReference<Object> phantomRef = new PhantomReference <>(obj, queue); System.out.println(phantomRef.get()); obj = null ; System.gc(); Thread.sleep(1000 ); Reference<?> ref = queue.poll(); if (ref != null ) { System.out.println("对象已被回收,可以做清理工作" ); } } }public abstract class Reference <T> { private T referent; volatile ReferenceQueue<? super T> queue; public T get () { return this .referent; } public void clear () { this .referent = null ; } }
实战场景 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 public class ImageCache { private Map<String, SoftReference<Image>> cache = new HashMap <>(); public Image getImage (String path) { SoftReference<Image> ref = cache.get(path); if (ref != null ) { Image img = ref.get(); if (img != null ) { return img; } } Image img = loadImage(path); cache.put(path, new SoftReference <>(img)); return img; } }public class SessionManager { private WeakHashMap<User, Session> sessions = new WeakHashMap <>(); public void createSession (User user) { sessions.put(user, new Session ()); } }public class ThreadLocal <T> { static class ThreadLocalMap { static class Entry extends WeakReference <ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super (k); value = v; } } } }public class Cleaner extends PhantomReference <Object> { private final Runnable thunk; public void clean () { if (remove(this )) { try { this .thunk.run(); } catch (final Throwable var2) { } } } }
常见误区 :
误区1 :软引用和弱引用混淆
纠正:软引用在内存不足时才回收,弱引用在GC时就回收
误区2 :认为虚引用可以获取对象
纠正:虚引用的get()永远返回null,只用于跟踪回收状态
误区3 :不理解ThreadLocal的内存泄漏
纠正:ThreadLocal的key是弱引用会被回收,但value是强引用不会回收,需要手动remove()
误区4 :过度使用软引用缓存
纠正:软引用的回收时机不确定,可能导致Full GC,影响性能
追问1:软引用什么时候被回收?
答:软引用的回收时机取决于GC算法和内存压力:
G1 GC :当老年代占用超过InitiatingHeapOccupancyPercent阈值(默认45%)时,会清理软引用
CMS GC :在即将发生OOM前清理软引用
JVM参数 :可以通过-XX:SoftRefLRUPolicyMSPerMB调整,表示每MB空闲堆空间软引用存活的毫秒数
1 2 3 4 5 long ms = SoftRefLRUPolicyMSPerMB * freeMB;if (now - lastAccess > ms) { }
追问2:为什么WeakHashMap能解决内存泄漏,而HashMap不能?
答:关键在于key的引用类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 Map<User, Data> map = new HashMap <>();User user = new User (); map.put(user, data); user = null ; Map<User, Data> map = new WeakHashMap <>();User user = new User (); map.put(user, data); user = null ;
WeakHashMap的Entry继承自WeakReference,当key没有外部强引用时,GC会回收key,然后WeakHashMap会清理对应的entry,避免内存泄漏。
追问3:DirectByteBuffer如何使用虚引用管理堆外内存?
答:DirectByteBuffer分配的是堆外内存(Native Memory),不受JVM GC管理。如果不释放会导致内存泄漏。虚引用的作用是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class DirectByteBuffer extends MappedByteBuffer { DirectByteBuffer(int cap) { long base = unsafe.allocateMemory(size); cleaner = Cleaner.create(this , new Deallocator (base, size)); } private static class Deallocator implements Runnable { public void run () { unsafe.freeMemory(address); } } }
当DirectByteBuffer对象被GC回收时,虚引用会进入引用队列,触发Cleaner执行清理任务,释放堆外内存。这是一种finalizer机制的替代方案,性能更好。
🟡⭐ 4. 深入剖析Java泛型的实现机制 面试官视角 :考察对泛型擦除、类型安全、泛型限制等核心概念的理解,以及对反射和泛型交互的认知。
详细答案 :
Java泛型是JDK 5引入的特性,用于提供编译时类型安全检查。但与C++模板不同,Java泛型使用**类型擦除(Type Erasure)**实现,这导致了一些独特的特性和限制。
核心概念 :
1. 类型擦除(Type Erasure)
泛型信息只存在于编译期,运行时会被擦除
擦除后替换为原始类型(Raw Type),通常是Object或第一个边界类型
编译器自动插入类型转换代码
2. 泛型的本质
编译期的语法糖,运行时不存在
通过编译器检查保证类型安全
避免了运行时的类型转换异常
3. 泛型擦除的原因
保持与旧版本代码的兼容性(泛型前的代码)
避免JVM的大规模改动
减少类文件膨胀(不像C++为每个类型生成新类)
原理图解 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 泛型擦除过程:编译前(源代码): +---------------------------+ | List<String> list = | | new ArrayList<>(); | | list.add("Hello"); || String s = list.get(0); | +---------------------------+ 编译后(字节码): +---------------------------+ | List list = | <- 泛型擦除为List | new ArrayList(); | | list.add("Hello"); | <- 编译器检查类型 | String s = || (String)list.get(0); | <- 编译器插入强转 +---------------------------+ 泛型边界擦除: List<T> -> List (擦除为Object) List<T extends Number> -> List (擦除为Number) List<?> -> List (擦除为Object) 多个边界的擦除: <T extends Comparable & Serializable> -> 擦除为第一个边界 Comparable
源码分析 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 public class Box <T> { private T value; public void set (T value) { this .value = value; } public T get () { return value; } }public class Box { private Object value; public void set (Object value) { this .value = value; } public Object get () { return value; } } Box<String> box = new Box <>(); box.set("Hello" );String s = box.get(); public class NumberBox <T extends Number > { private T value; public void set (T value) { this .value = value; } public double doubleValue () { return value.doubleValue(); } }public class NumberBox { private Number value; public void set (Number value) { this .value = value; } public double doubleValue () { return value.doubleValue(); } }class Node <T> { public T data; public void setData (T data) { this .data = data; } }class MyNode extends Node <Integer> { @Override public void setData (Integer data) { super .setData(data); } }class MyNode extends Node { public void setData (Integer data) { super .setData(data); } @Override public void setData (Object data) { setData((Integer) data); } }public class GenericArray <T> { private Object[] array = new Object [10 ]; @SuppressWarnings("unchecked") public T get (int index) { return (T) array[index]; } private T[] array; @SuppressWarnings("unchecked") public GenericArray (Class<T> clazz, int size) { array = (T[]) Array.newInstance(clazz, size); } }
实战场景 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 List<String> list1 = new ArrayList <>(); List<Integer> list2 = new ArrayList <>(); System.out.println(list1.getClass() == list2.getClass()); public <T> void check (Object obj) { if (obj instanceof List) { } }public class Example { public void print (List<String> list) { } public void print (Set<String> set) { } }public class GenericInfo { private List<String> stringList; public static void main (String[] args) throws Exception { Field field = GenericInfo.class.getDeclaredField("stringList" ); Type genericType = field.getGenericType(); if (genericType instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) genericType; System.out.println(pt.getRawType()); System.out.println(pt.getActualTypeArguments()[0 ]); } } }public abstract class BaseDao <T> { private Class<T> entityClass; @SuppressWarnings("unchecked") public BaseDao () { Type type = getClass().getGenericSuperclass(); if (type instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) type; entityClass = (Class<T>) pt.getActualTypeArguments()[0 ]; } } public void save (T entity) { System.out.println("Saving: " + entityClass.getName()); } }public class UserDao extends BaseDao <User> { }UserDao userDao = new UserDao (); userDao.save(new User ());
常见误区 :
误区1 :认为泛型在运行时存在
纠正:泛型信息在运行时被擦除,只能通过反射从元数据中获取
误区2 :认为List<String>和List<Object>有继承关系
纠正:泛型不支持协变,List<String>不是List<Object>的子类
误区3 :试图创建泛型数组
纠正:不能直接new T[],需要通过反射或使用Object[]
误区4 :在静态方法/字段中使用类泛型参数
纠正:静态成员属于类,类泛型参数属于实例,不能混用
追问1:为什么不能创建泛型数组?
答:这是类型擦除导致的安全问题。假设可以创建泛型数组:
1 2 3 4 5 6 7 List<String>[] stringLists = new List <String>[1 ]; Object[] objects = stringLists; objects[0 ] = new ArrayList <Integer>(); String s = stringLists[0 ].get(0 );
由于类型擦除,运行时数组只知道存储的是List,无法检查具体类型参数,导致类型安全问题。所以Java禁止创建泛型数组。
追问2:通配符?、? extends T、? super T的区别?
答:这是PECS原则(Producer Extends, Consumer Super):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 List<?> list = new ArrayList <String>();Object obj = list.get(0 ); List<? extends Number > numbers = new ArrayList <Integer>();Number num = numbers.get(0 ); List<? super Integer> list = new ArrayList <Number>(); list.add(1 ); public class Collections { public static <T> void copy ( List<? super T> dest, // dest是消费者,接收T,用super List<? extends T> src) { for (T t : src) { dest.add(t); } } } List<Number> numbers = new ArrayList <>(); List<Integer> integers = Arrays.asList(1 , 2 , 3 ); Collections.copy(numbers, integers);
追问3:泛型的桥接方法是什么?
答:桥接方法是编译器生成的合成方法,用于保持泛型类继承时的多态性。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Node <T> { public void setData (T data) { } }class MyNode extends Node <Integer> { @Override public void setData (Integer data) { } }class MyNode extends Node { public void setData (Integer data) { } public void setData (Object data) { setData((Integer) data); } }for (Method m : MyNode.class.getDeclaredMethods()) { System.out.println(m + " isBridge=" + m.isBridge()); }
二、JVM内存模型与GC(35题) 🟢⭐ 5. 详细说说JVM的内存结构 面试官视角 :考察对JVM运行时数据区的理解,这是JVM相关问题的基础。
详细答案 :
JVM运行时内存结构分为线程共享和线程私有两大类,总共包含5个主要区域(JDK 8+)。
内存结构划分 :
线程私有(每个线程独立):
程序计数器(Program Counter Register)
虚拟机栈(VM Stack)
本地方法栈(Native Method Stack)
线程共享(所有线程共享): 4. 堆(Heap) 5. 方法区(Method Area) - JDK 8改为元空间(Metaspace)
原理图解 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 JVM运行时数据区: +------------------------------------------------------------------+ | JVM Process | +------------------------------------------------------------------+ | 线程1 线程2 线程3 | | +------------+ +------------+ +------------+ | | | 程序计数器 | | 程序计数器 | | 程序计数器 | | | +------------+ +------------+ +------------+ | | | 虚拟机栈 | | 虚拟机栈 | | 虚拟机栈 | | | | +-Stack--+ | | +-Stack--+ | | +-Stack--+ | | | | | Frame1 | | | | Frame1 | | | | Frame1 | | | | | | Frame2 | | | | Frame2 | | | | Frame2 | | | | | +--------+ | | +--------+ | | +--------+ | | | +------------+ +------------+ +------------+ | | | 本地方法栈 | | 本地方法栈 | | 本地方法栈 | | | +------------+ +------------+ +------------+ | +------------------------------------------------------------------+ | 线程共享区域 | | +------------------------------------------------------------+ | | | 堆 (Heap) | | | | +------------------+ +----------------------------------+ | | | | | 新生代 (Young) | | 老年代 (Old/Tenured) | | | | | | +------+------+ | | | | | | | | | Eden |Survivor | | | | | | | | | | | S0 | S1 | | | | | | | | | +------+------+ | | | | | | | +------------------+ +----------------------------------+ | | | +------------------------------------------------------------+ | | +------------------------------------------------------------+ | | | 方法区 (Method Area / Metaspace) | | | | 类信息、常量、静态变量、即时编译代码等 | | | | +----------------+ +----------------------------------+ | | | | | 运行时常量池 | | 类元数据 (Class Metadata) | | | | +----------------+ +----------------------------------+ | | | +------------------------------------------------------------+ | +------------------------------------------------------------------+ | 直接内存 (Direct Memory) | | NIO Buffer、DirectByteBuffer (不属于JVM运行时数据区) | +------------------------------------------------------------------+
各区域详解 :
1. 程序计数器(Program Counter Register)
当前线程执行的字节码行号指示器
记录正在执行的虚拟机字节码指令地址
如果执行Native方法,计数器值为空(Undefined)
唯一不会出现OOM的内存区域
线程切换后能恢复到正确的执行位置
2. 虚拟机栈(VM Stack)
描述Java方法执行的内存模型
每个方法执行时创建一个栈帧(Stack Frame)
栈帧包含:局部变量表、操作数栈、动态链接、方法出口
生命周期与线程相同
异常:StackOverflowError(栈深度溢出)、OutOfMemoryError(扩展失败)
3. 本地方法栈(Native Method Stack)
为Native方法服务(HotSpot将其与虚拟机栈合并)
调用C/C++代码时使用
也会抛出StackOverflowError和OutOfMemoryError
4. 堆(Heap)
JVM最大的内存区域,所有线程共享
存放对象实例和数组
GC的主要区域(也叫”GC堆”)
分为新生代和老年代
异常:OutOfMemoryError: Java heap space
5. 方法区(Method Area / Metaspace)
存储已被加载的类信息、常量、静态变量、JIT编译后的代码
JDK 7前叫永久代(PermGen),在堆中
JDK 8改为元空间(Metaspace),使用本地内存
异常:OutOfMemoryError: Metaspace
源码分析与实战 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 public class MemoryDemo { private static int staticVar = 100 ; private static final String CONSTANT = "Hello" ; private int instanceVar; public static void main (String[] args) { int localVar = 10 ; MemoryDemo obj = new MemoryDemo (); obj.instanceVar = 20 ; String str = "Hello" ; int [] array = new int [100 ]; obj.method(localVar); } public void method (int param) { int result = param * 2 ; } public static void testOOM () { } }public class StackFrameDemo { public int calculate (int a, int b) { int result = a + b; return result; } public static void main (String[] args) { StackFrameDemo demo = new StackFrameDemo (); int value = demo.calculate(5 , 10 ); } }public class DirectMemoryDemo { public static void main (String[] args) { ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 100 ); } }
实战场景 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 public class ObjectAllocation { class Inner { int value; } public void allocate () { Object obj = new Object (); byte [] bigArray = new byte [5 * 1024 * 1024 ]; static Object longLived = new Object (); } }public class MetaspaceUsage { static class MyClass { static String staticField = "static" ; static final String CONSTANT = "constant" ; void method () { } } public static void main (String[] args) { ClassLoader classLoader = new MyClassLoader (); } }public class JVMParams { }
常见误区 :
误区1 :认为方法区就是永久代
纠正:永久代是HotSpot对方法区的实现(JDK 7-),JDK 8改为元空间
误区2 :认为所有对象都在堆上
纠正:通过逃逸分析和标量替换,某些对象可能在栈上分配
误区3 :认为栈只存基本类型
误区4 :混淆堆和栈的职责
追问1:为什么JDK 8要用元空间替代永久代?
答:主要有以下原因:
避免OOM :永久代有固定大小上限(-XX:MaxPermSize),容易OOM。元空间使用本地内存,只受系统内存限制
简化GC :永久代需要Full GC才能回收,效率低。元空间简化了GC实现
融合HotSpot和JRockit :Oracle收购Sun后,需要融合两个JVM,JRockit从来没有永久代
类元数据的生命周期 :类元数据的生命周期与类加载器一致,使用本地内存更合理
字符串常量池移出 :JDK 7已将字符串常量池移到堆中,进一步减少永久代的作用
追问2:对象一定分配在堆上吗?
答:不一定。JVM通过逃逸分析优化,某些对象可能在栈上分配:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class EscapeAnalysis { public User escape () { User user = new User (); return user; } public void noEscape () { User user = new User (); user.setName("张三" ); System.out.println(user.getName()); } public void scalarReplacement () { Point point = new Point (1 , 2 ); int sum = point.x + point.y; } }
追问3:栈帧中的动态链接是什么?
答:动态链接是指向运行时常量池中该栈帧所属方法的引用,支持方法调用过程中的动态连接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class DynamicLinking { public void methodA () { methodB(); } public void methodB () { System.out.println("B" ); } }
🟡⭐ 6. 详细说说Java的垃圾回收机制 面试官视角 :考察对GC算法、垃圾收集器、GC调优的综合理解,这是JVM最重要的知识点之一。
详细答案 :
Java的垃圾回收(Garbage Collection, GC)是自动内存管理机制,负责回收不再使用的对象,释放内存空间。
一、如何判断对象可以回收?
1. 引用计数法(Reference Counting)
给对象添加引用计数器,引用+1,失效-1
优点:实现简单,判定效率高
缺点:无法解决循环引用问题
Java未采用此方法
1 2 3 4 循环引用问题: objA.ref = objB objB.ref = objA // 即使没有外部引用,计数器仍不为0 ,无法回收
2. 可达性分析算法(Reachability Analysis) ⭐
Java采用的方法
从GC Roots作为起点,向下搜索,路径称为引用链
不可达的对象即为可回收对象
GC Roots包括 :
虚拟机栈中引用的对象(局部变量)
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI引用的对象
所有被同步锁持有的对象
JVM内部的引用(Class对象、异常对象、类加载器等)
原理图解 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 可达性分析示例:GC Roots :[栈帧1] [栈帧2] [静态变量] [常量] | | | | v v v v [objA] [objB] [objC] [objD] | | | v v +---- + [objE] [objF] | | v v [objG] [objH] [objI] [objJ] | v [objK] 可达对象(存活):A , B , C , D , E , F , G , J , K 不可达对象(可回收):H , I 对象回收流程:1 . 第一次标记:不可达对象2 . 筛选:是否有必要执行finalize ()3 . 第二次标记:finalize ()中未重新关联到GC Roots 的对象4 . 回收
二、垃圾回收算法
1. 标记-清除算法(Mark-Sweep)
过程 :
标记阶段:标记所有需要回收的对象
清除阶段:回收被标记的对象
优点 :实现简单缺点 :
1 2 3 4 标记-清除过程: 标记前:[obj1] [____] [obj2] [____] [obj3] 标记后:[obj1] [XXXX] [obj2] [XXXX] [obj3] (标记垃圾) 清除后:[obj1] [ ] [obj2] [ ] [obj3] (内存碎片)
2. 复制算法(Copying) ⭐
过程 :
将内存分为两块(From和To)
只使用From区域
GC时将存活对象复制到To区域
清空From区域,交换From和To
优点 :
缺点 :
应用 :新生代(对象存活率低)
1 2 3 4 5 6 7 8 复制算法过程:From 区域:[obj1] [garbage] [obj2] [garbage] [obj3] ↓ (复制) ↓ ↓To 区域: [obj1] [obj2] [obj3] [ ] 交换后:To 变From :[obj1] [obj2] [obj3] [可用空间 ] From 变To :[全部可用 ]
3. 标记-整理算法(Mark-Compact)
过程 :
标记阶段:标记所有存活对象
整理阶段:将存活对象向一端移动
清除阶段:清除边界外的内存
优点 :
缺点 :
应用 :老年代(对象存活率高)
1 2 3 标记-整理过程: 标记后:[obj1] [XXXX] [obj2] [XXXX] [obj3] [XXXX] 整理后:[obj1] [obj2] [obj3] [可用空间 ]
4. 分代收集算法(Generational Collection) ⭐
思想 :根据对象存活周期将内存划分为新生代和老年代,采用不同算法。
新生代 :
对象存活率低(朝生夕死)
采用复制算法
划分为Eden和两个Survivor(8:1:1)
Minor GC频繁,速度快
老年代 :
对象存活率高
采用标记-清除或标记-整理
Major GC/Full GC慢,但频率低
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 分代内存结构: +------------------------------------------------------+ | 新生代 (Young Generation) | 老年代 (Old Gen) | | +------------+-----+-----+ | | | | Eden | S0 | S1 | | | | | 8 | 1 | 1 | | | | +------------+-----+-----+ | | +------------------------------------------------------+ 对象晋升流程: 1. 新对象在Eden分配 2. Eden满了触发Minor GC 3. 存活对象复制到S0 4. 下次GC,S0存活对象复制到S1(年龄+1) 5. 反复复制,年龄达到阈值(默认15)-> 晋升老年代 6. 大对象直接进入老年代
源码分析与实战 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 public class GCDemo { public static void main (String[] args) { byte [] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte [2 * 1024 * 1024 ]; allocation2 = new byte [2 * 1024 * 1024 ]; allocation3 = new byte [2 * 1024 * 1024 ]; allocation4 = new byte [4 * 1024 * 1024 ]; } }public class PromotionDemo { private static final int _1MB = 1024 * 1024 ; @SuppressWarnings("unused") public static void testTenuringThreshold () { byte [] allocation1, allocation2, allocation3; allocation1 = new byte [_1MB / 4 ]; allocation2 = new byte [4 * _1MB]; allocation3 = new byte [4 * _1MB]; allocation3 = null ; allocation3 = new byte [4 * _1MB]; } }public class FinalizeDemo { public static FinalizeDemo SAVE_HOOK = null ; public void isAlive () { System.out.println("I'm still alive!" ); } @Override protected void finalize () throws Throwable { super .finalize(); System.out.println("finalize method executed!" ); FinalizeDemo.SAVE_HOOK = this ; } public static void main (String[] args) throws Exception { SAVE_HOOK = new FinalizeDemo (); SAVE_HOOK = null ; System.gc(); Thread.sleep(500 ); if (SAVE_HOOK != null ) { SAVE_HOOK.isAlive(); } else { System.out.println("I'm dead!" ); } SAVE_HOOK = null ; System.gc(); Thread.sleep(500 ); if (SAVE_HOOK != null ) { SAVE_HOOK.isAlive(); } else { System.out.println("I'm dead!" ); } } }public class ReferenceGCDemo { public static void main (String[] args) { Object strongRef = new Object (); SoftReference<byte []> softRef = new SoftReference <>(new byte [10 * 1024 * 1024 ]); System.out.println(softRef.get()); try { byte [] bytes = new byte [20 * 1024 * 1024 ]; } catch (OutOfMemoryError e) { System.out.println(softRef.get()); } WeakReference<byte []> weakRef = new WeakReference <>(new byte [1024 ]); System.out.println(weakRef.get()); System.gc(); System.out.println(weakRef.get()); ReferenceQueue<byte []> queue = new ReferenceQueue <>(); PhantomReference<byte []> phantomRef = new PhantomReference <>(new byte [1024 ], queue); System.out.println(phantomRef.get()); } }
常见误区 :
误区1 :认为System.gc()一定会触发GC
纠正:System.gc()只是建议JVM进行GC,不保证立即执行
误区2 :认为对象一定在finalize()中被回收
纠正:finalize()可以让对象”自救”,重新关联到GC Roots
误区3 :认为Minor GC不会STW
误区4 :新生代都是复制算法
纠正:不同垃圾收集器实现不同,如G1使用分区的混合算法
追问1:Minor GC、Major GC、Full GC的区别?
答:
类型
作用区域
触发条件
特点
Minor GC
新生代
Eden区满
频繁、快速、STW时间短
Major GC
老年代
老年代满(CMS触发)
慢,部分收集器有此概念
Full GC
整堆+方法区
老年代满、空间分配担保失败、System.gc()等
最慢、STW时间最长
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class FullGCTrigger { }
追问2:什么是空间分配担保?
答:空间分配担保是在Minor GC前的一种安全检查机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
追问3:为什么新生代要有两个Survivor区?
答:这是为了避免内存碎片和提高内存利用率:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 假设只有一个Survivor:[Eden] -> GC -> [Survivor] (90% 满)[Eden] -> GC -> [Survivor] (已满,新对象只能去老年代) 问题:Survivor很快就满,对象过早晋升到老年代 使用两个Survivor:[Eden + S0] -> GC -> [S1] (复制算法,无碎片)[Eden + S1] -> GC -> [S0] (乒乓复制) 优点:1 . 总有一个Survivor是空的,保证复制算法正常工作2 . 无内存碎片3 . 对象在Survivor间多次复制,年龄增长,合理晋升 为什么不是Eden:S0:S1 = 1 :1 :1 ? - 研究表明,98% 的对象是朝生夕死的 - 8 :1 :1 的比例可以最大化内存利用率 - 只浪费10% 的新生代空间(一个Survivor总是空的)
🟡⭐ 7. 详细说说常见的垃圾收集器及其特点 面试官视角 :考察对不同垃圾收集器的理解,以及在实际场景中如何选择和调优。
详细答案 :
垃圾收集器是GC算法的具体实现,不同收集器适用于不同场景。Java提供了多种收集器,从Serial到最新的ZGC。
垃圾收集器发展历程 :
1 2 串行时代 -> 并行时代 -> 并发时代 -> 低延迟时代 Serial -> Parallel -> CMS/G1 -> ZGC/Shenandoah
一、Serial收集器(串行收集器)
特点 :
单线程收集,收集时必须暂停所有工作线程(STW)
新生代使用复制算法,老年代使用标记-整理
简单高效,Client模式下的默认收集器
适合单核CPU、内存较小的环境
优点 :简单高效,没有线程交互开销缺点 :STW时间长,用户体验差
1 2 3 4 Serial GC 工作流程: 用户线程: ████████░░░░░░░░████████GC 线程: ────────██████────────── (暂停) (GC ) (恢复)
参数 :-XX:+UseSerialGC
二、ParNew收集器(并行收集器)
特点 :
Serial的多线程版本
新生代使用复制算法,多线程并行
Server模式下与CMS配合使用的首选新生代收集器
默认线程数等于CPU核心数
优点 :多核环境下效率高缺点 :仍然需要STW
1 2 3 4 5 6 ParNew GC工作流程:用户线程: ████████░░░░░░░░████████ GC线程1: ────────███─────────── GC线程2: ────────███─────────── GC线程3: ────────███─────────── (同时工作,减少STW时间)
参数 :-XX:+UseParNewGC,-XX:ParallelGCThreads=N
三、Parallel Scavenge收集器(吞吐量优先)
特点 :
新生代收集器,复制算法,并行多线程
关注吞吐量(运行用户代码时间 / 总时间)
提供自适应调节策略(GC Ergonomics)
JDK 8默认收集器
优点 :高吞吐量,适合后台计算任务缺点 :不关注停顿时间,STW可能较长
参数 :
-XX:+UseParallelGC:使用Parallel Scavenge + Serial Old
-XX:+UseParallelOldGC:使用Parallel Scavenge + Parallel Old
-XX:MaxGCPauseMillis=N:最大GC停顿时间(毫秒)
-XX:GCTimeRatio=N:吞吐量大小(1/(1+N))
-XX:+UseAdaptiveSizePolicy:自适应调节策略
四、CMS收集器(Concurrent Mark Sweep,并发标记清除) ⭐
特点 :
老年代收集器,关注最短停顿时间
标记-清除算法,并发收集
适合对响应时间要求高的应用(互联网应用)
工作流程 :
初始标记(STW) :标记GC Roots直接关联的对象,速度快
并发标记 :从GC Roots遍历对象图,耗时长,与用户线程并发
重新标记(STW) :修正并发标记期间变动的对象,时间较短
并发清除 :清除标记为可回收的对象,与用户线程并发
1 2 3 4 5 CMS GC工作流程:阶段: 初始 并发标记 重新 并发清除 重置 用户线程: ████░░██████████████░░████████████████████ GC线程: ────██──────────────██────────────────── (STW) (并发) (STW) (并发)
优点 :并发收集,低停顿缺点 :
CPU资源敏感 :并发阶段占用CPU,影响吞吐量
无法处理浮动垃圾 :并发标记/清除时产生的新垃圾,下次GC才能回收
内存碎片 :标记-清除算法产生碎片,可能导致Full GC
Concurrent Mode Failure :并发清除时老年代空间不足,退化为Serial Old
参数 :
-XX:+UseConcMarkSweepGC:启用CMS
-XX:CMSInitiatingOccupancyFraction=N:老年代占用N%时触发CMS(默认68%)
-XX:+UseCMSCompactAtFullCollection:Full GC后进行碎片整理
-XX:CMSFullGCsBeforeCompaction=N:N次Full GC后进行整理
五、G1收集器(Garbage First) ⭐⭐
特点 :
JDK 9默认收集器,面向服务端应用
整堆收集器(不区分新生代/老年代收集器)
基于Region的内存布局
可预测的停顿时间模型
标记-整理算法,无内存碎片
内存布局 :
1 2 3 4 5 6 7 8 9 G1 堆结构(分Region ): +---+---+---+---+---+---+---+---+ | E | E | S | O | O | H | E | O | E =Eden , S =Survivor +---+---+---+---+---+---+---+---+ O =Old , H =Humongous (大对象) | O | E | O | S | E | O | E | O | +---+---+---+---+---+---+---+---+ 每个Region 大小:1MB-32MB(2 的幂次) 总共2048 个Region
核心概念 :
Region :将堆划分为多个大小相等的独立区域
Humongous Region :存储大对象(超过Region 50%)
Remembered Set(RSet) :记录Region间的引用关系
Collection Set(CSet) :一次GC回收的Region集合
工作流程 :
初始标记(STW) :标记GC Roots直接关联的对象
并发标记 :遍历对象图,标记存活对象
最终标记(STW) :处理并发标记期间变化的对象
筛选回收(STW) :根据停顿时间目标,选择价值最大的Region回收
1 2 3 4 5 6 G1 Mixed GC流程: Young GC: 回收所有Eden + Survivor Mixed GC: 回收所有Young + 部分Old(价值最高的) 价值计算:(Region中垃圾量 / 回收时间) 优先回收垃圾最多、回收时间短的Region
优点 :
可预测停顿时间(-XX:MaxGCPauseMillis)
无内存碎片(整理算法)
适合大堆内存(>4GB)
并发与并行结合
缺点 :
内存占用高(RSet占用10%-20%堆空间)
额外执行负载高(维护RSet)
小堆内存下不如CMS
参数 :
-XX:+UseG1GC:启用G1
-XX:MaxGCPauseMillis=200:期望最大停顿时间(默认200ms)
-XX:G1HeapRegionSize=N:Region大小
-XX:InitiatingHeapOccupancyPercent=45:堆占用45%时启动并发标记
六、ZGC收集器(Z Garbage Collector) ⭐⭐⭐
特点 :
JDK 11引入,JDK 15转正
超低延迟收集器,停顿时间<10ms
支持TB级别堆内存
并发收集,几乎全程并发
基于Region,但动态大小
使用着色指针(Colored Pointer)和读屏障(Load Barrier)
核心技术 :
着色指针(Colored Pointer) :
1 2 3 4 5 6 7 64位对象指针布局: +--------+--------+--------+--------+ | Unused | Marked | Remap | Object || 16 bit | 4 bit | 4 bit | 40 bit | +--------+--------+--------+--------+ 元数据存储在指针中,不在对象头
读屏障 :
在从堆中读取对象引用时插入代码
判断对象是否被移动,自动修正
实现并发移动对象
工作流程 :
初始标记(STW,<1ms) :标记GC Roots
并发标记 :遍历对象图
再标记(STW,<1ms) :处理标记队列
并发转移准备 :选择要回收的Region
初始转移(STW,<1ms) :转移GC Roots直接引用的对象
并发转移 :转移其他对象
优点 :
停顿时间极短(<10ms)
支持超大堆内存(最大16TB)
吞吐量损失小(<15%)
缺点 :
参数 :
-XX:+UseZGC:启用ZGC
-Xmx16g:设置最大堆(ZGC不需要-Xms)
七、垃圾收集器组合与选择
经典组合 :
1 2 3 4 5 6 新生代 老年代 Serial + Serial Old (单核、小内存) ParNew + CMS (响应时间优先) Parallel + Parallel Old (吞吐量优先) G1 (不区分新老年代) (大堆、可预测停顿) ZGC (不区分新老年代) (超低延迟)
选择建议 :
场景
推荐收集器
理由
单核CPU、内存<100MB
Serial
简单高效
多核CPU、后台计算任务
Parallel
高吞吐量
互联网应用、响应时间敏感
G1/CMS
低停顿
大堆内存(>32GB)
G1
可预测停顿
超低延迟要求(<10ms)
ZGC/Shenandoah
极低停顿
金融交易系统
ZGC
停顿时间稳定
源码分析与实战 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 public class GCComparison { public static void main (String[] args) { List<byte []> list = new ArrayList <>(); for (int i = 0 ; i < 1000 ; i++) { byte [] bytes = new byte [1024 * 100 ]; list.add(bytes); if (i % 100 == 0 ) { System.out.println("Allocated: " + i + " objects" ); } } for (int i = 0 ; i < list.size() / 2 ; i++) { list.remove(0 ); } System.gc(); try { Thread.sleep(5000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }public class CMSFailureDemo { public static void main (String[] args) { List<byte []> list = new ArrayList <>(); for (int i = 0 ; i < 100 ; i++) { byte [] bytes = new byte [1024 * 200 ]; list.add(bytes); if (i % 10 == 0 ) { System.gc(); } } } }public class G1TuningDemo { public static void main (String[] args) { Map<String, byte []> cache = new ConcurrentHashMap <>(); for (int i = 0 ; i < 10000 ; i++) { String key = "key_" + i; byte [] value = new byte [1024 * 10 ]; cache.put(key, value); if (i % 1000 == 0 && i > 0 ) { cache.clear(); System.out.println("Cleared cache at: " + i); } } } }public class ZGCDemo { public static void main (String[] args) throws InterruptedException { List<byte []> list = new ArrayList <>(); while (true ) { for (int i = 0 ; i < 1000 ; i++) { byte [] bytes = new byte [1024 * 100 ]; list.add(bytes); } if (list.size() > 10000 ) { list.subList(0 , 5000 ).clear(); } Thread.sleep(10 ); } } }
常见误区 :
误区1 :认为CMS没有STW
纠正:CMS的初始标记和重新标记阶段仍需STW,只是时间很短
误区2 :G1适合所有场景
纠正:小堆内存(<4GB)下,G1不如CMS或Parallel
误区3 :MaxGCPauseMillis设置越小越好
误区4 :认为ZGC没有性能损耗
追问1:CMS的Concurrent Mode Failure如何避免?
答:Concurrent Mode Failure发生在CMS并发清理时老年代空间不足,解决方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
追问2:G1的RSet是什么,为什么需要它?
答:RSet(Remembered Set)是G1用来记录Region间引用关系的数据结构,解决跨Region引用问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
追问3:ZGC的着色指针和读屏障如何实现并发移动对象?
答:这是ZGC最核心的技术,实现几乎全程并发:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
🟢⭐ 8. 说说Java类加载机制 面试官视角 :考察对类生命周期、双亲委派模型、类加载器的理解,以及热部署等实际应用。
详细答案 :
Java类加载机制是指将.class文件加载到JVM内存,并转换为Class对象的过程。这是Java动态语言特性的基础。
一、类的生命周期
类从被加载到JVM内存开始,到卸载出内存为止,完整生命周期包括7个阶段:
1 2 3 4 加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载 (Loading -> Linking(Verification -> Preparation -> Resolution) -> Initialization -> Using -> Unloading) 其中验证、准备、解析统称为连接(Linking)
1. 加载(Loading)
JVM需要完成以下工作:
通过类的全限定名获取类的二进制字节流
将字节流代表的静态结构转换为方法区的运行时数据结构
在内存中生成代表这个类的java.lang.Class对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 User user = new User (); User.staticField; User.staticMethod(); Class.forName("com.example.User" );class Child extends Parent { } public static void main (String[] args) { }
2. 验证(Verification)
确保Class文件的字节流符合JVM规范,保证安全性:
文件格式验证 :魔数(0xCAFEBABE)、版本号等
元数据验证 :语义分析,如类是否有父类、final类是否被继承等
字节码验证 :数据流和控制流分析,确保程序语义合法
符号引用验证 :确保解析动作能正常执行
3. 准备(Preparation)
为类的静态变量分配内存并设置初始值(零值):
1 2 3 4 5 6 7 8 9 public class PrepareDemo { private static int value = 123 ; private static final int CONSTANT = 456 ; private int instanceVar = 789 ; }
4. 解析(Resolution)
将常量池中的符号引用替换为直接引用:
符号引用 :用字符串描述,如com/example/User
直接引用 :直接指向目标的指针、偏移量或句柄
5. 初始化(Initialization)
执行类构造器<clinit>()方法,这是类加载的最后一步:
<clinit>()由编译器自动生成,包含所有静态变量赋值和静态代码块
<clinit>()保证父类的<clinit>()先执行
JVM保证<clinit>()在多线程环境下正确同步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class InitDemo { static { System.out.println("静态代码块1" ); } private static int a = 1 ; static { System.out.println("静态代码块2" ); a = 2 ; } private static int b = func(); private static int func () { System.out.println("静态方法" ); return 3 ; } }
二、类加载器(ClassLoader)
Java提供了三层类加载器,以及可以自定义类加载器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 类加载器层次: Bootstrap ClassLoader (C++实现) 加载<JAVA_HOME> /lib ↑ | Extension ClassLoader 加载<JAVA_HOME>/lib/ext ↑ | Application ClassLoader 加载classpath ↑ | Custom ClassLoader 自定义加载
1. 启动类加载器(Bootstrap ClassLoader)
C++实现,JVM的一部分
加载<JAVA_HOME>/lib目录的核心类库(rt.jar等)
无法被Java程序直接引用
2. 扩展类加载器(Extension ClassLoader)
Java实现,sun.misc.Launcher$ExtClassLoader
加载<JAVA_HOME>/lib/ext目录的类库
开发者可以直接使用
3. 应用程序类加载器(Application ClassLoader)
Java实现,sun.misc.Launcher$AppClassLoader
加载classpath上的类库
程序默认的类加载器
三、双亲委派模型(Parent Delegation Model) ⭐⭐
工作原理 : 当类加载器收到类加载请求时:
不会自己先加载,而是委派给父加载器
父加载器无法加载时,才由子加载器加载
最终委派到Bootstrap ClassLoader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 双亲委派流程: 自定义ClassLoader ↓ ( 委派) AppClassLoader ↓ ( 委派) ExtClassLoader ↓ ( 委派) BootstrapClassLoader | (尝试加载) ↓ ( 加载失败) ExtClassLoader | (尝试加载) ↓ ( 加载失败) AppClassLoader | (尝试加载) ↓ ( 加载失败) 自定义ClassLoader | (自己加载)
优势 :
避免类的重复加载 :父加载器已加载的类,子加载器不会再加载
保护核心类库 :防止核心API被篡改(如自定义java.lang.String会加载失败)
源码分析 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null ) { long t0 = System.nanoTime(); try { if (parent != null ) { c = parent.loadClass(name, false ); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null ) { long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }public class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader (String classPath) { this .classPath = classPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte [] classData = loadClassData(name); if (classData == null ) { throw new ClassNotFoundException (); } return defineClass(name, classData, 0 , classData.length); } catch (IOException e) { throw new ClassNotFoundException ("Could not load class: " + name, e); } } private byte [] loadClassData(String className) throws IOException { String fileName = classPath + File.separatorChar + className.replace('.' , File.separatorChar) + ".class" ; try (InputStream ins = new FileInputStream (fileName); ByteArrayOutputStream baos = new ByteArrayOutputStream ()) { byte [] buffer = new byte [1024 ]; int len; while ((len = ins.read(buffer)) != -1 ) { baos.write(buffer, 0 , len); } return baos.toByteArray(); } } }public class CustomClassLoaderDemo { public static void main (String[] args) throws Exception { MyClassLoader loader = new MyClassLoader ("D:/classes" ); Class<?> clazz = loader.loadClass("com.example.MyClass" ); Object obj = clazz.newInstance(); Method method = clazz.getMethod("sayHello" ); method.invoke(obj); System.out.println("ClassLoader: " + clazz.getClassLoader()); System.out.println("Parent: " + clazz.getClassLoader().getParent()); } }
实战场景 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 public class JDBCDemo { public static void main (String[] args) throws Exception { Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/db" , "user" , "password" ); } }public class ClassIsolationDemo { public static void main (String[] args) throws Exception { MyClassLoader loader1 = new MyClassLoader ("lib/fastjson-1.2.47.jar" ); MyClassLoader loader2 = new MyClassLoader ("lib/fastjson-1.2.68.jar" ); Class<?> class1 = loader1.loadClass("com.alibaba.fastjson.JSON" ); Class<?> class2 = loader2.loadClass("com.alibaba.fastjson.JSON" ); System.out.println(class1 == class2); } }public class ClassUnloadDemo { public static void main (String[] args) throws Exception { MyClassLoader loader = new MyClassLoader ("D:/classes" ); Class<?> clazz = loader.loadClass("com.example.MyClass" ); Object obj = clazz.newInstance(); obj = null ; clazz = null ; loader = null ; System.gc(); } }
常见误区 :
误区1 :认为双亲委派模型不可打破
纠正:可以重写loadClass()方法打破,但需要谨慎
误区2 :Class.forName()和ClassLoader.loadClass()相同
纠正:Class.forName()会执行类的初始化,loadClass()不会
误区3 :同一个类只会被加载一次
纠正:不同ClassLoader可以加载同一个类,产生不同的Class对象
误区4 :认为父类加载器是子类加载器的父类
追问1:为什么要打破双亲委派模型?
答:某些场景下双亲委派模型无法满足需求:
SPI机制 :父加载器需要加载子加载器路径中的类(如JDBC驱动)
热部署 :需要重新加载类而不影响其他模块
代码隔离 :不同模块使用不同版本的库(如Tomcat的WebApp)
插件化架构 :动态加载/卸载插件(如OSGi、Eclipse)
实现方式 :
重写loadClass()方法,改变委派逻辑
使用线程上下文类加载器
使用自定义类加载器架构
追问2:Class.forName()和ClassLoader.loadClass()的区别?
答:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Class<?> clazz1 = Class.forName("com.example.MyClass" ); Class<?> clazz1 = Class.forName("com.example.MyClass" , true , Thread.currentThread().getContextClassLoader());ClassLoader loader = Thread.currentThread().getContextClassLoader(); Class<?> clazz2 = loader.loadClass("com.example.MyClass" );public class InitTest { static { System.out.println("InitTest被初始化" ); } } Class.forName("InitTest" ); loader.loadClass("InitTest" ); Class.forName("com.mysql.jdbc.Driver" ); loader.loadClass("com.example.Bean" );
追问3:如何实现热部署/热加载?
答:热部署的核心是废弃旧的ClassLoader,创建新的ClassLoader加载新版本的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 public class HotDeployDemo { private static MyClassLoader loader; private static Class<?> clazz; private static long lastModified; public static void main (String[] args) throws Exception { String className = "com.example.HotClass" ; String classPath = "D:/classes" ; while (true ) { long currentModified = getClassFileLastModified(className, classPath); if (currentModified != lastModified) { System.out.println("检测到类文件变化,重新加载..." ); loader = null ; clazz = null ; System.gc(); loader = new MyClassLoader (classPath); clazz = loader.loadClass(className); lastModified = currentModified; } Object obj = clazz.newInstance(); Method method = clazz.getMethod("execute" ); method.invoke(obj); Thread.sleep(5000 ); } } private static long getClassFileLastModified (String className, String classPath) { String filePath = classPath + File.separator + className.replace('.' , File.separatorChar) + ".class" ; return new File (filePath).lastModified(); } }
三、并发编程(35题) 🟢⭐ 9. 说说synchronized的实现原理 面试官视角 :考察对Java锁机制的理解,synchronized是并发编程的基础。
详细答案 :
synchronized是Java提供的内置锁机制,用于实现线程同步,保证同一时刻只有一个线程执行被synchronized修饰的代码块或方法。
一、使用方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class SynchronizedDemo { public synchronized void method1 () { } public static synchronized void method2 () { } public void method3 () { synchronized (this ) { } } private final Object lock = new Object (); public void method4 () { synchronized (lock) { } } }
二、底层实现原理
1. 字节码层面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class BytecodeDemo { public void syncBlock () { synchronized (this ) { System.out.println("sync block" ); } } }public void syncBlock () ; Code: 0 : aload_0 1 : dup 2 : astore_1 3 : monitorenter 4 : getstatic #2 7 : ldc #3 9 : invokevirtual #4 12 : aload_1 13 : monitorexit 14 : goto 22 17 : astore_2 18 : aload_1 19 : monitorexit 20 : aload_2 21 : athrow 22 : return
2. 对象头结构
synchronized的锁信息存储在Java对象头中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 Java对象内存布局: +-------------------+ | 对象头 (Header) | +-------------------+ | 实例数据 (Data) | +-------------------+ | 对齐填充 (Padding)| +-------------------+ 对象头结构(64位JVM): +----------------------------------------------------+ | Mark Word (8 bytes) | Klass Pointer (8 bytes) | +----------------------------------------------------+ | 锁状态、GC信息、hashCode | 指向类元数据的指针 | +----------------------------------------------------+ Mark Word的不同状态(64位): +----------------------------------------------------------------------+ | 锁状态 | 25bit | 31bit | 1bit | 4bit | 1bit | 2bit || | unused | hashcode |unused | age | biased| lock | +----------------------------------------------------------------------+ | 无锁 | 未使用 | hashcode | 0 | age | 0 | 01 | | 偏向锁 | ThreadID | Epoch | 0 | age | 1 | 01 | | 轻量级锁 | 指向栈中Lock Record的指针 | 00 | | 重量级锁 | 指向互斥量(重量级锁)的指针 | 10 || GC标记 | 空 | 11 | +----------------------------------------------------------------------+ 锁标志位(lock): 00 - 轻量级锁 01 - 无锁或偏向锁(根据biased位区分) 10 - 重量级锁 11 - GC标记
三、锁升级机制 ⭐⭐
synchronized采用锁升级策略,从偏向锁 -> 轻量级锁 -> 重量级锁,避免不必要的性能开销。
1 2 3 4 锁升级流程: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 (只升级不降级)
1. 偏向锁(Biased Locking)
思想 :大多数情况下,锁不存在多线程竞争,且总是由同一线程多次获取。
原理 :
第一个线程获取锁时,在对象头记录线程ID
该线程再次进入,只需检查线程ID是否匹配,无需CAS操作
其他线程竞争时,撤销偏向锁,升级为轻量级锁
1 2 3 4 5 6 7 1. 检查对象头的Mark Word是否为可偏向状态(biased=1 , lock=01 )2. 如果是,检查ThreadID是否为当前线程: - 是:直接进入同步块 - 否:通过CAS尝试将ThreadID改为当前线程 - 成功:获得偏向锁 - 失败:说明有竞争,撤销偏向锁,升级为轻量级锁
优点 :只有第一次获取锁需要CAS,后续无需任何同步操作,性能极高缺点 :如果有多线程竞争,偏向锁的撤销代价较大
参数 :
-XX:+UseBiasedLocking:开启偏向锁(JDK 6-14默认开启,JDK 15废弃)
-XX:BiasedLockingStartupDelay=0:JVM启动后立即开启偏向锁
2. 轻量级锁(Lightweight Locking)
思想 :线程交替执行同步块,不存在实际竞争。
原理 :
线程在栈帧中创建Lock Record,存储对象头的Mark Word拷贝
使用CAS将对象头的Mark Word替换为指向Lock Record的指针
获取失败则自旋重试,自旋一定次数后升级为重量级锁
1 2 3 4 5 6 7 8 1. 在当前线程的栈帧中创建Lock Record2. 将对象头的Mark Word复制到Lock Record中(Displaced Mark Word)3. 使用CAS尝试将对象头的Mark Word替换为指向Lock Record的指针 - 成功:获得轻量级锁 - 失败:检查Mark Word是否指向当前线程的栈帧 - 是:重入锁 - 否:有其他线程竞争,膨胀为重量级锁
优点 :避免重量级锁的开销,适合线程交替执行的场景缺点 :自旋消耗CPU,不适合锁持有时间长或并发度高的场景
3. 重量级锁(Heavyweight Locking)
思想 :基于操作系统的互斥量(Mutex)实现,竞争失败的线程阻塞。
原理 :
对象头的Mark Word指向Monitor对象
Monitor包含Owner、EntryList、WaitSet等
线程竞争锁失败后进入EntryList阻塞,等待被唤醒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Monitor结构: +----------------------------+ | Monitor | +----------------------------+ | Owner: 持有锁的线程 | | EntryList: 等待锁的线程队列 | | WaitSet: wait()的线程队列 || count: 重入次数 | +----------------------------+ 线程状态转换: 新建线程 -> EntryList(阻塞) -> Owner(运行) ↑ ↓ +-- wait() --> WaitSet(等待) notify()
优点 :支持阻塞/唤醒机制,避免空自旋浪费CPU缺点 :需要用户态和内核态切换,开销大(10-100倍于无锁操作)
源码分析与实战 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 public class LockUpgradeDemo { private static final Object lock = new Object (); public static void main (String[] args) throws Exception { System.out.println("=== 偏向锁 ===" ); synchronized (lock) { System.out.println(ClassLayout.parseInstance(lock).toPrintable()); } System.out.println("=== 轻量级锁 ===" ); Thread t1 = new Thread (() -> { synchronized (lock) { System.out.println("T1: " + ClassLayout.parseInstance(lock).toPrintable()); } }); t1.start(); t1.join(); synchronized (lock) { System.out.println("Main: " + ClassLayout.parseInstance(lock).toPrintable()); } System.out.println("=== 重量级锁 ===" ); Thread t2 = new Thread (() -> { synchronized (lock) { try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread t3 = new Thread (() -> { synchronized (lock) { System.out.println("T3: " + ClassLayout.parseInstance(lock).toPrintable()); } }); t2.start(); Thread.sleep(100 ); t3.start(); t2.join(); t3.join(); } }public class LockEliminationDemo { public void method () { Object lock = new Object (); synchronized (lock) { } } public String concat (String s1, String s2) { StringBuffer sb = new StringBuffer (); sb.append(s1); sb.append(s2); return sb.toString(); } }public class LockCoarseningDemo { private final Object lock = new Object (); public void method () { synchronized (lock) { } synchronized (lock) { } synchronized (lock) { } } }public class AdaptiveSpinningDemo { private final Object lock = new Object (); public void method () { synchronized (lock) { } } }
常见误区 :
误区1 :认为synchronized性能很差
纠正:JDK 6后锁优化(偏向锁、轻量级锁),性能大幅提升,某些场景不逊于ReentrantLock
误区2 :认为锁会降级
纠正:锁只能升级,不能降级(偏向锁->轻量级锁->重量级锁)
误区3 :认为synchronized不可中断
纠正:确实不可中断,但可以通过wait/notify配合实现类似效果
误区4 :滥用synchronized(this)
纠正:可能导致死锁或性能问题,建议使用私有final Object lock
追问1:synchronized和ReentrantLock的区别?
答:
特性
synchronized
ReentrantLock
实现层面
JVM内置关键字,C++实现
JDK层面的类,Java实现
锁类型
非公平锁(JDK 6后优化)
可选公平锁/非公平锁
可中断
不可中断
lockInterruptibly()可中断
超时获取锁
不支持
tryLock(timeout)支持
条件变量
单个wait/notify
多个Condition
锁释放
自动释放(异常也会释放)
手动释放,finally中unlock
性能
JDK 6后性能接近
高并发下略优
使用难度
简单,不易出错
复杂,需手动释放
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ReentrantLock lock = new ReentrantLock (true ); try { if (lock.tryLock(1 , TimeUnit.SECONDS)) { try { } finally { lock.unlock(); } } } catch (InterruptedException e) { }
追问2:什么是锁消除和锁粗化?
答:这是JIT编译器的两种锁优化技术:
锁消除(Lock Elimination) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public String concat (String s1, String s2) { StringBuffer sb = new StringBuffer (); sb.append(s1); sb.append(s2); return sb.toString(); }public String concat2 (String s1, String s2) { StringBuilder sb = new StringBuilder (); sb.append(s1); sb.append(s2); return sb.toString(); }
锁粗化(Lock Coarsening) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void append (List<String> list) { for (String s : list) { synchronized (lock) { sb.append(s); } } synchronized (lock) { for (String s : list) { sb.append(s); } } }
追问3:为什么JDK 15废弃了偏向锁?
答:偏向锁被废弃的原因:
现代应用多线程化 :偏向锁假设锁总是由单线程访问,但现代应用广泛使用线程池、并发框架,这个假设不再成立
维护成本高 :偏向锁的撤销过程复杂,需要STW,反而影响性能
收益递减 :JDK内部很多API(如ConcurrentHashMap)已经改用无锁算法,偏向锁的应用场景变少
轻量级锁足够好 :自适应自旋优化后,轻量级锁的性能已经很好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
🟡⭐ 10. 深入理解volatile关键字 面试官视角 :考察对Java内存模型、可见性、有序性的理解,volatile是并发编程的重要知识点。
详细答案 :
volatile是Java提供的轻量级同步机制,保证变量的可见性 和有序性 ,但不保证原子性。
一、volatile的三大特性
1. 可见性(Visibility)
一个线程修改volatile变量,其他线程立即可见
通过内存屏障实现,强制从主内存读取/写入
2. 有序性(Ordering)
禁止指令重排序优化
保证volatile变量前后的代码不会被重排序
3. 不保证原子性(Non-Atomic)
volatile不能保证复合操作的原子性
如i++不是原子操作,需要使用synchronized或Atomic类
原理图解 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Java内存模型(JMM): 线程1 主内存 线程2 +--------+ +--------+ +--------+ | 工作内存 | | volatile | | 工作内存 | | count=0 | <--读--- | count | ---读--> | count=0 | +--------+ +--------+ +--------+ | ↑ | | 写count=1 | | 读到最新值 | | | count=1 +------写回主内存-----+ | volatile保证: 1. 写操作立即刷新到主内存 2. 读操作从主内存读取最新值 3. 禁止指令重排序 普通变量(无volatile): - 线程工作内存与主内存不同步 - 可能读到过期数据 - 指令可能被重排序
二、volatile的实现原理
1. 内存屏障(Memory Barrier)
volatile通过插入内存屏障指令来实现可见性和有序性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 内存屏障类型: LoadLoad屏障: Load1 确保Load1 数据加载先于Load2 StoreStore屏障: Store1 确保Store1 数据对其他处理器可见先于Store2 LoadStore屏障: Load1 确保Load1 数据加载先于Store2 刷新 StoreLoad屏障: Store1 确保Store1 数据对所有处理器可见先于Load2 加载 开销最大的屏障volatile 写插入的屏障: ... 普通写 ... StoreStore屏障 volatile 写 StoreLoad屏障volatile 读插入的屏障: volatile 读 LoadLoad屏障 LoadStore屏障 ... 普通读 ...
2. 汇编层面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class VolatileDemo { private volatile int count = 0 ; public void increase () { count++; } }0x01a3de1d : movb $0 ×0 ,0 ×1104800 (%esi);0x01a3de24 : lock addl $0 ×0 ,(%esp);
3. MESI缓存一致性协议
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 MESI协议保证多核CPU缓存一致性: 状态: M (Modified): 已修改,与主内存不一致,独占 E (Exclusive): 独占,与主内存一致 S (Shared): 共享,多个CPU缓存都有副本 I (Invalid): 无效,需要从主内存重新加载 volatile的读写:1. CPU1写volatile变量: - 缓存行变为M状态 - 通过总线通知其他CPU - 其他CPU的缓存行变为I状态 2. CPU2读volatile变量: - 检测到缓存行为I状态 - 从CPU1的缓存或主内存读取最新值 - 缓存行变为S状态
三、volatile的应用场景
场景1:状态标志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class StatusFlag { private volatile boolean flag = false ; public void waitForFlag () { while (!flag) { } doSomething(); } public void setFlag () { doSomeWork(); flag = true ; } }
场景2:双重检查锁定(Double-Checked Locking)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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; } }
场景3:读写锁的状态标识
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class VolatileExample { private volatile int state = 0 ; public void reader () { int localState = state; } public void writer () { state = 1 ; } }
场景4:一次性安全发布
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class SafePublication { private volatile Map<String, String> config; public void init () { Map<String, String> temp = new HashMap <>(); temp.put("key1" , "value1" ); temp.put("key2" , "value2" ); config = temp; } public String get (String key) { Map<String, String> localConfig = config; return localConfig != null ? localConfig.get(key) : null ; } }
源码分析与实战 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 public class VisibilityDemo { private boolean flag = false ; public void thread1 () { while (!flag) { } System.out.println("线程1结束" ); } public void thread2 () { flag = true ; System.out.println("线程2修改flag为true" ); } public static void main (String[] args) { VisibilityDemo demo = new VisibilityDemo (); new Thread (() -> demo.thread1()).start(); try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } new Thread (() -> demo.thread2()).start(); } }public class AtomicityDemo { private volatile int count = 0 ; public void increase () { count++; } public static void main (String[] args) throws InterruptedException { AtomicityDemo demo = new AtomicityDemo (); Thread[] threads = new Thread [10 ]; for (int i = 0 ; i < 10 ; i++) { threads[i] = new Thread (() -> { for (int j = 0 ; j < 1000 ; j++) { demo.increase(); } }); threads[i].start(); } for (Thread thread : threads) { thread.join(); } System.out.println("count = " + demo.count); } }public void increase () { count++; }public class HappensBeforeDemo { private int a = 0 ; private volatile boolean flag = false ; public void writer () { a = 1 ; flag = true ; } public void reader () { if (flag) { int i = a; System.out.println(i); } } }public class ComparisonDemo { private volatile int volatileCount = 0 ; private int syncCount = 0 ; public void volatileIncrease () { volatileCount++; } public synchronized void syncIncrease () { syncCount++; } public void correctVolatileUsage () { } }public class AtomicDemo { private AtomicInteger count = new AtomicInteger (0 ); public void increase () { count.incrementAndGet(); } }
常见误区 :
误区1 :认为volatile可以替代synchronized
纠正:volatile不保证原子性,复合操作必须用synchronized或Atomic类
误区2 :认为volatile变量的所有操作都是原子的
纠正:只有单个读/写是原子的,i++等复合操作不是原子的
误区3 :过度使用volatile
纠正:volatile有性能开销(内存屏障、缓存失效),只在必要时使用
误区4 :认为volatile可以保证线程安全
纠正:只有在特定场景下(如状态标志)才安全,一般情况需要配合其他机制
追问1:volatile的性能开销有多大?
答:volatile的性能开销主要来自:
内存屏障指令 :禁止CPU优化,影响指令流水线
缓存失效 :写操作导致其他CPU缓存失效,增加缓存未命中率
禁止JIT优化 :编译器不能做某些优化(如寄存器缓存)
性能对比 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class VolatilePerformance { private int normalVar = 0 ; private volatile int volatileVar = 0 ; public void normalReadWrite () { int temp = normalVar; normalVar = temp + 1 ; } public void volatileReadWrite () { int temp = volatileVar; volatileVar = temp + 1 ; } public synchronized void syncReadWrite () { int temp = normalVar; normalVar = temp + 1 ; } }
追问2:为什么双重检查锁定必须用volatile?
答:这是经典的指令重排序问题,详细分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class DCLAnalysis { private static Singleton instance; public static Singleton getInstance () { if (instance == null ) { synchronized (Singleton.class) { if (instance == null ) { instance = new Singleton (); } } } return instance; } }
追问3:volatile能否保证long/double的原子性?
答:这涉及到JVM规范中的一个特殊问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public class LongAtomicityDemo { private long nonVolatileLong = 0L ; private volatile long volatileLong = 0L ; public void writerNonVolatile () { nonVolatileLong = 0x0123456789ABCDEFL ; } public long readerNonVolatile () { return nonVolatileLong; } public void writerVolatile () { volatileLong = 0x0123456789ABCDEFL ; } public long readerVolatile () { return volatileLong; } }
Spring全家桶 一、Spring核心(30题) 🟢⭐ 11. 说说Spring的IoC容器原理 面试官视角 :考察对Spring核心机制的理解,IoC是Spring的基石。
详细答案 :
IoC(Inversion of Control,控制反转)是Spring的核心思想,将对象的创建和依赖关系的管理从应用代码中分离出来,交给IoC容器负责。
一、什么是IoC?
传统方式 :
1 2 3 4 5 6 7 8 9 public class UserService { private UserDao userDao = new UserDaoImpl (); public void addUser (User user) { userDao.save(user); } }
IoC方式 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class UserService { private UserDao userDao; public UserService (UserDao userDao) { this .userDao = userDao; } public void addUser (User user) { userDao.save(user); } }
控制反转的含义 :
控制 :对象的创建、依赖关系的管理
反转 :从应用代码转移到IoC容器
依赖注入(DI) :IoC的实现方式,通过注入方式提供依赖对象。
二、Spring IoC容器架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 IoC 容器层次结构:BeanFactory (顶层接口) ↑ListableBeanFactory HierarchicalBeanFactory ↑ ↑ +--------ApplicationContext --------+ ↑ +------------+------------+ ↑ ↑ConfigurableApplicationContext WebApplicationContext ↑ClassPathXmlApplicationContext AnnotationConfigApplicationContext
1. BeanFactory
最基础的IoC容器,提供基本的依赖注入功能
延迟加载:调用getBean()时才创建对象
适合资源受限的环境
2. ApplicationContext
BeanFactory的子接口,提供更多企业级功能
立即加载:启动时创建所有单例Bean
额外功能:
国际化(MessageSource)
事件发布(ApplicationEventPublisher)
资源访问(ResourceLoader)
AOP集成
常用实现类 :
ClassPathXmlApplicationContext:从classpath加载XML配置
FileSystemXmlApplicationContext:从文件系统加载XML配置
AnnotationConfigApplicationContext:基于注解的配置
WebApplicationContext:Web应用的IoC容器
三、Bean的生命周期 ⭐⭐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Bean生命周期完整流程:1. 实例化(Instantiation) ↓2. 属性赋值(Populate Properties) ↓3. 初始化前(BeanPostProcessor.postProcessBeforeInitialization) ↓4. 初始化(Initialization) - 调用@PostConstruct方法 - 调用InitializingBean.afterPropertiesSet() - 调用init-method指定的方法 ↓5. 初始化后(BeanPostProcessor.postProcessAfterInitialization) ↓6. Bean就绪,可以使用 ↓7. 销毁前(@PreDestroy) ↓8. 销毁(Destruction) - 调用DisposableBean.destroy() - 调用destroy-method指定的方法
源码分析 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 protected Object doCreateBean (String beanName, RootBeanDefinition mbd, Object[] args) { BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args); Object bean = instanceWrapper.getWrappedInstance(); populateBean(beanName, mbd, instanceWrapper); Object exposedObject = initializeBean(beanName, bean, mbd); return exposedObject; }protected Object initializeBean (String beanName, Object bean, RootBeanDefinition mbd) { invokeAwareMethods(beanName, bean); Object wrappedBean = applyBeanPostProcessorsBeforeInitialization(bean, beanName); invokeInitMethods(beanName, wrappedBean, mbd); wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); return wrappedBean; }
完整示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 @Component public class LifecycleBean implements BeanNameAware , BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean { private String name; public LifecycleBean () { System.out.println("1. 构造器:实例化Bean" ); } public void setName (String name) { this .name = name; System.out.println("2. 属性赋值:setName()" ); } @Override public void setBeanName (String name) { System.out.println("3. BeanNameAware:setBeanName()" ); } @Override public void setBeanFactory (BeanFactory beanFactory) { System.out.println("4. BeanFactoryAware:setBeanFactory()" ); } @Override public void setApplicationContext (ApplicationContext applicationContext) { System.out.println("5. ApplicationContextAware:setApplicationContext()" ); } @PostConstruct public void postConstruct () { System.out.println("6. @PostConstruct:postConstruct()" ); } @Override public void afterPropertiesSet () { System.out.println("7. InitializingBean:afterPropertiesSet()" ); } public void initMethod () { System.out.println("8. init-method:initMethod()" ); } @PreDestroy public void preDestroy () { System.out.println("9. @PreDestroy:preDestroy()" ); } @Override public void destroy () { System.out.println("10. DisposableBean:destroy()" ); } public void destroyMethod () { System.out.println("11. destroy-method:destroyMethod()" ); } }@Component public class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization (Object bean, String beanName) { if (bean instanceof LifecycleBean) { System.out.println("BeanPostProcessor:postProcessBeforeInitialization()" ); } return bean; } @Override public Object postProcessAfterInitialization (Object bean, String beanName) { if (bean instanceof LifecycleBean) { System.out.println("BeanPostProcessor:postProcessAfterInitialization()" ); } return bean; } }
四、依赖注入方式
1. 构造器注入(推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Service public class UserService { private final UserDao userDao; public UserService (UserDao userDao) { this .userDao = userDao; } }
2. Setter注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Service public class UserService { private UserDao userDao; @Autowired public void setUserDao (UserDao userDao) { this .userDao = userDao; } }
3. 字段注入(不推荐)
1 2 3 4 5 6 7 8 9 10 11 @Service public class UserService { @Autowired private UserDao userDao; }
五、循环依赖问题 ⭐
什么是循环依赖?
1 2 3 4 5 6 7 8 9 10 11 12 @Service public class ServiceA { @Autowired private ServiceB serviceB; }@Service public class ServiceB { @Autowired private ServiceA serviceA; }
Spring如何解决?
使用三级缓存 机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 private final Map<String, Object> singletonObjects = new ConcurrentHashMap <>();private final Map<String, Object> earlySingletonObjects = new HashMap <>();private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap <>();protected Object getSingleton (String beanName) { Object singletonObject = singletonObjects.get(beanName); if (singletonObject == null ) { singletonObject = earlySingletonObjects.get(beanName); if (singletonObject == null ) { ObjectFactory<?> singletonFactory = singletonFactories.get(beanName); if (singletonFactory != null ) { singletonObject = singletonFactory.getObject(); earlySingletonObjects.put(beanName, singletonObject); singletonFactories.remove(beanName); } } } return singletonObject; }
循环依赖解决过程 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 1. 创建ServiceA: - 实例化ServiceA(构造器) - 将ServiceA的ObjectFactory放入三级缓存 - 填充属性,发现需要ServiceB2. 创建ServiceB: - 实例化ServiceB(构造器) - 将ServiceB的ObjectFactory放入三级缓存 - 填充属性,发现需要ServiceA3. 再次获取ServiceA: - 从三级缓存获取ServiceA的ObjectFactory - 调用getObject(),得到ServiceA的早期引用 - 将ServiceA放入二级缓存,移除三级缓存 - ServiceB注入ServiceA的早期引用4. ServiceB初始化完成: - ServiceB放入一级缓存 - 移除二级、三级缓存中的ServiceB5. ServiceA继续初始化: - ServiceA注入完全初始化的ServiceB - ServiceA初始化完成,放入一级缓存 - 移除二级、三级缓存中的ServiceA
哪些循环依赖无法解决?
构造器循环依赖 :无法解决,因为Bean实例化时就需要依赖
1 2 3 4 5 6 7 8 9 10 @Service public class ServiceA { public ServiceA (ServiceB serviceB) { } }@Service public class ServiceB { public ServiceB (ServiceA serviceA) { } }
prototype作用域的循环依赖 :无法解决,因为不缓存prototype Bean
@Async等代理对象的循环依赖 :可能失败,取决于代理时机
常见误区 :
误区1 :认为IoC就是DI
纠正:DI是IoC的一种实现方式,IoC是更广泛的概念
误区2 :认为ApplicationContext就是BeanFactory
纠正:ApplicationContext是BeanFactory的子接口,功能更强大
误区3 :过度使用字段注入
误区4 :认为循环依赖都能解决
纠正:只有单例的setter/字段注入才能解决,构造器和prototype不行
追问1:三级缓存的作用是什么,为什么需要三级?
答:
为什么不用两级缓存?
理论上两级缓存可以解决循环依赖:
但Spring需要支持AOP代理,问题来了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Service public class ServiceA { @Autowired private ServiceB serviceB; }@Service @Async public class ServiceB { @Autowired private ServiceA serviceA; }
三级缓存的作用 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 singletonFactories.put(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));protected Object getEarlyBeanReference (String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { exposedObject = ((SmartInstantiationAwareBeanPostProcessor) bp) .getEarlyBeanReference(exposedObject, beanName); } } return exposedObject; }
追问2:BeanFactory和FactoryBean的区别?
答:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 BeanFactory factory = new ClassPathXmlApplicationContext ("beans.xml" );Object bean = factory.getBean("myBean" );public interface FactoryBean <T> { T getObject () throws Exception; Class<?> getObjectType(); boolean isSingleton () ; }@Component public class CarFactoryBean implements FactoryBean <Car> { @Override public Car getObject () throws Exception { Car car = new Car (); car.setBrand("BMW" ); car.setPrice(500000 ); return car; } @Override public Class<?> getObjectType() { return Car.class; } @Override public boolean isSingleton () { return true ; } }@Autowired private Car car; Object factoryBean = context.getBean("&carFactoryBean" );
PLACEHOLDER_FOR_MORE_CONTENT_3