JVM内存模型与垃圾回收
在Java面试中,JVM原理几乎是必问环节。理解JVM的内存结构是掌握Java性能调优的基础。JVM内存区域主要分为线程私有区域和线程共享区域。
1. 运行时数据区
- 程序计数器:线程私有,记录当前执行的字节码地址。
- Java虚拟机栈:线程私有,存储栈帧,用于方法调用和数据交换。
- 本地方法栈:线程私有,为Native方法服务。
- Java堆:线程共享,是GC管理的主要区域,存放对象实例。
- 方法区:线程共享,存储类信息、常量、静态变量。
2. 垃圾回收算法
- 标记-清除:产生内存碎片,效率较低。
- 标记-复制:解决碎片问题,适合新生代(如Serial, ParNew)。
- 标记-整理:适合老年代,减少碎片。
- 分代收集:结合上述算法,针对不同代际采用不同策略。
常见面试题:如何判断对象是否存活?
判断对象存活主要有两种方法:
- 引用计数法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1。任何时刻计数器为0的对象就是不可能再被使用的。缺点是难以解决对象之间相互循环引用的问题。
- 可达性分析算法:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。在Java中,可作为GC Roots的对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。
集合框架源码深度解析
集合框架是Java开发中最常用的工具之一。面试中常考HashMap、ArrayList、LinkedList等核心类的底层实现。
HashMap底层结构与扩容机制
HashMap在JDK1.7中由数组+链表组成,在JDK1.8中由数组+链表+红黑树组成。当链表长度超过8且数组长度超过64时,链表会转换为红黑树,以提高查询效率(从O(n)提升到O(log n))。
核心问题:
- 为什么HashMap是线程不安全的?因为在并发环境下进行resize操作时,可能导致链表成环,造成CPU 100%。
- HashMap的默认负载因子是多少?0.75。这是在时间和空间成本上权衡的结果。
- 如何避免HashMap的Hash冲突?使用合理的哈希算法(如扰动函数)。
// HashMap put 操作关键逻辑伪代码
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 处理哈希冲突
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
}
ArrayList动态数组实现
ArrayList底层基于数组实现,支持随机访问,查询效率高(O(1)),但插入和删除效率低(需要移动元素)。默认初始容量为10,扩容时变为原来的1.5倍。
注意事项:
- 预分配容量:如果知道数据规模,建议指定初始容量,避免频繁扩容带来的性能损耗。
- 删除元素:使用Iterator.remove()或ArrayList.remove(Object)时,注意数组元素的移动。
LinkedList双向链表实现
LinkedList底层基于双向链表实现,插入和删除效率高(O(1),已知节点位置),但查询效率低(O(n))。它同时实现了List和Deque接口,可以作为队列使用。
与ArrayList对比:
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 | 快 (O(1)) | 慢 (O(n)) |
| 插入/删除 | 慢 (需移动元素) | 快 (O(1), 已知节点) |
| 内存占用 | 较低 (仅存数据) | 较高 (存前后指针) |
并发编程(JUC)核心机制
并发编程是Java进阶的必经之路。理解线程池、锁机制、原子类、AQS等概念是解决高并发问题的关键。
理解Thread与Runnable的区别,线程的生命周期(新建、就绪、运行、阻塞、终止),以及synchronized关键字的用法。synchronized是JVM层面的锁,具有互斥性和可见性。
java.util.concurrent.locks包提供了更灵活的锁机制,如ReentrantLock、ReadWriteLock。其核心是AbstractQueuedSynchronizer (AQS),通过一个int类型的state变量表示锁状态,利用CLH队列管理等待线程。
Executors工具类提供了四种线程池,但阿里巴巴开发手册推荐使用ThreadPoolExecutor显式创建。核心参数包括:corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler。
volatile关键字详解
volatile是Java提供的轻量级同步机制,具有以下特性:
- 保证可见性:当一个变量被volatile修饰后,对其进行写操作时,JMM会将该线程本地内存中的共享变量值刷新到主内存,其他线程读取时会清空自己的工作内存,重新从主内存读取。
- 禁止指令重排序:通过插入内存屏障来实现,确保指令执行的顺序符合预期。
- 不保证原子性:如i++操作,volatile无法保证原子性,需结合synchronized或AtomicInteger使用。
IO与NIO模型演进
Java IO经历了从BIO到NIO再到AIO的演进过程,核心在于处理高并发连接的能力提升。
BIO (Blocking IO)
同步阻塞模型。一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进行处理。如果连接不做任何事情,就会造成不必要的线程开销。适用于连接数目小且固定的架构。
NIO (Non-blocking IO)
同步非阻塞模型。一个线程可以处理多个请求。核心组件包括:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。Selector可以检测多个注册的通道上是否有事件发生,如果有事件发生就获取事件通知当前线程处理。
AIO (Asynchronous IO)
异步非阻塞模型。引入异步通道概念,操作完成后回调通知。适用于连接数目多且连接比较长(重操作)的架构,如照片服务器。
Spring框架底层原理
Spring作为Java生态的基石,其核心是IOC(控制反转)和AOP(面向切面编程)。
IOC容器初始化流程
- Resource定位:找到Bean的定义信息(XML、注解或Java Config)。
- BeanDefinition加载:将资源信息解析为BeanDefinition对象,存储在Map中。
- BeanDefinition注册:将BeanDefinition注册到BeanFactory中。
- 实例化与依赖注入:通过反射创建Bean实例,并注入依赖属性。
AOP实现原理
Spring AOP基于动态代理实现。如果目标对象实现了接口,默认使用JDK动态代理;如果目标对象没有实现接口,使用CGLIB动态代理。AOP的核心概念包括:Aspect(切面)、Advice(通知)、Pointcut(切入点)、Joinpoint(连接点)。
数据库与MySQL优化
虽然本题聚焦JDK,但数据库是Java后端不可或缺的一部分。MySQL的索引、事务隔离级别、锁机制是面试高频点。
| 索引类型 | 数据结构 | 特点 |
|---|---|---|
| B+树 | 多路平衡查找树 | MySQL InnoDB默认索引,适合范围查询,数据存储在叶子节点 |
| Hash | 哈希表 | 等值查询极快,不支持范围查询,MyISAM可选 |
分布式系统基础
随着微服务架构的普及,分布式相关技术栈如Redis、MQ、Zookeeper等也成为Java面试的重要内容。
例如,Redis作为高性能缓存,其持久化机制(RDB和AOF)、数据结构、分布式锁实现(Redisson)等都是考察重点。理解CAP理论和BASE理论是设计分布式系统的基础。