异常
throw和throws的区别
throw是用在方法里面的,作用是主动抛出一个具体的异常对象
throws是写在方法声明上的,告诉调用者这个方法可能会抛出哪些类型的异常,需要调用方去处理。ArrayIndexOutOfBoundsException(数组下标越界异常)
ArrayIndexOutOfBoundsException出现的原因:使用不存在的索引访问数组时NullPointerException(空指针异常)
NullPointerException 出现的原因:是一个运行时异常,调用对象上的方法但在运行的时候这个对象引用为 Null 时会引发该异常ClassCastException(类型转换异常)
ClassCastException 出现的原因:是类型不匹配,常见于强制转换、集合操作、泛型和反射等场景try,catch,finally执行结构
try { System.out.println("try"); // throw new RunTimeException("抛出异常"); 若存在这段代码则进入catch代码块 } catch (Exception e) { // 假设捕获 Exception System.out.println("catch1"); return 1; System.out.println("catch2"); // 这行不会执行,因为前面有 return } finally { System.out.println("finally"); }try先执行,打印 “try”。
如果 try 块没有抛出异常:
跳过 catch 块,直接进入 finally 块。
finally 块执行,打印 “finally”。
方法正常结束(如果没有 return 的话)。
如果 try 块抛出异常:
进入 catch 块,打印 “catch1”。
遇到 return 1,但 finally 仍然会执行。
finally 块执行,打印 “finally”。
然后方法返回 1(catch2 不会执行,因为 return 已经终止了当前方法)。
面向对象
Java 三大特性是什么?各自作用?
封装:把属性和方法封装在类里,隐藏内部实现,对外提供get和set;
继承:子类继承父类的属性和方法,减少代码冗余
多态:同一行为的不同表现形式(编译时多态:方法重载;运行时多态:方法重写 + 父类引用指向子类对象)。
重载(Overload)和重写(Override)的区别?
重载是在一个类中多态的体现;方法名相同、参数列表(个数 / 类型 / 顺序)不同,和返回值无关(编译时多态);
重写是父类与子类之间的多态;子类重写父类的方法,方法名、参数、返回值都相同,访问权限不能更严格、异常不能扩大(运行时多态)。
Java中创建对象的方式有几种?何时使用哪种方式?
- 使用
- 使用new关键字
- 使用clone方法
- 反射机制
- 反序列化
- 区别
使用new关键字和反射机制都会明确的显式的调用构造函数
使用clone方法是在内存上对已有对象的拷贝 所以不会调用构造函数
反序列化是从文件中还原类的对象 也不会调用构造函数
Java里可不可以有多继承?
- 不可以,Java中不存在多继承,只存在多实现;多实现必须使用接口
抽象类与接口的区别,包括使用(什么情况使用抽象类,什么情况使用接口)
抽象类:可包含抽象方法和普通方法,有构造方法,单继承;
接口:JDK8 后可含默认方法 /static 方法,无构造方法,多实现;
对象的创建过程
Student s = new Student();
上方代码在内存中做了什么
栈里创建s变量(空的,存 null);
堆里分配空间,创建 Student 对象;
执行构造方法,给对象赋具体值;
把堆里对象的内存地址,赋值给栈里的s;
以后用s的方法,都是通过地址找到堆里的对象操作。
Java 中的类加载过程分为哪几个阶段?
更标准的划分是五个阶段:加载、验证、准备、解析、初始化。
- 加载:是将 class 文件加载到内存;
- 验证:确保 class 文件合法;
- 准备:为类变量分配内存并赋默认值;
- 解析:将符号引用转为直接引用;
- 初始化:执行类构造器
泛型
Java中的泛型是什么
泛型(Generics) 是 Java 语言的一项核心特性,允许开发者通过类型参数编写通用代码,适配多种数据类型,同时确保类型安全。
简单来说,泛型就是让代码在编译时就知道要操作的具体数据类型,而不是在运行时才确定。
泛型的优势
类型安全 List
代码复用 一个 Box
可读性高 processData
性能优化 编译时检查类型,减少运行时开销
Java的泛型的类型擦除 ?
原理:泛型类型在运行时会被擦除为 Object,但编译器通过桥方法(Bridge Methods)保留类型信息
String相关
Java中的String,StringBuilder,StringBuffer三者的区别
String 是不可变的,每次修改会产生新对象,线程安全;
StringBuffer 是可变的,线程安全,效率较低;
StringBuilder 是可变的,线程不安全,效率比 StringBuffer 高
String s = “hello” 和 String s = new String(“hello”)的区别?
String s = “hello”;
- 是一个字符串字面量(”hello”)直接存储在方法区的字符串池中,复用已有的对象。
- 高效:直接复用对象,减少内存分配和垃圾回收压力,省内存:字符串池存储唯一实例
String s = new String(“hello”);
- 高效:直接复用对象,减少内存分配和垃圾回收压力,省内存:字符串池存储唯一实例
- 通过 new 动态创建一个新的 String 对象,存储在堆内存中,无论字符串池是否存在相同内容都会新建对象
- 灵活:明确创建新对象,适用于需要独立修改的场景。
- 可控:可调用 intern() 方法强制复用字符串池对象。 1. 低效:重复创建相同内容的对象会占用更多内存
Collecation集合相关
List、Set、Map 的区别?
List:有序、可重复,按索引访问(ArrayList、LinkedList);
Set:无序、不可重复(HashSet、TreeSet);
Map:键值对,键唯一、值可重复(HashMap、TreeMap、ConcurrentHashMap)。
java arrayList的存储结构,初始化的时候创建多大的数组?
- ArrayList是基于数组实现的,是一个动态数组,容量能自动增长,初始化长度是0,在第一次调用 add 方法添加元素时,容量才会初始化 10 ;扩容规则: 扩容后的大小是原始大小的1.5倍
- ArrayList是线程不安全的,只能用在单线程环境下,多线程环境下可以考虑用
Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发包下的CopyOnWriteArrayList类
ArrayList与LinkedList区别及使用
- 数据结构不同;ArrayList是数组的数据结构,LinkedList是链表的数据结构
- 效率不同
- 查询时
LinkedList是线性的数据存储方式,需要移动指针从前往后依次查找
ArrayList使用数组方式存储数据,根据索引查询数据速度比较快 - 新增或删除时
LinkedList使用链表结构存储数据,每个元素都记录前后元素的指针,所以插入、删除数据时只是更改前后元素的指针指向即可,操作比较快
ArrayList使用数组存储数据,在其中进行增删操作时,新增或删除后会对所有数据的下标索引造成影响;这里还涉及到扩容与缩容的规则:若新增数据时,原数组的大小可以容下新的数据则大小不变;若容不下的话会创建一个更大的数组(原始数组的1.5倍),然后将原数组中的元素复制到新数组中;
删除元素时,直接在原数组上操作,ArrayList 会将待删除位置之后的元素向前移动一位(覆盖待删除元素),最后将数组末尾的元素置为 null(帮助GC回收);但是数组的容量不变
- 查询时
HashMap(哈希相关)
HashMap是Java中基于哈希表 (数组+链表/红黑树) 实现的键值对存储结构
什么是哈希冲突(哈希碰撞)
不同的 key 经过哈希函数计算后,得到了相同的数组下标。
导致多个键值对需要存到同一个哈希桶中,会降低 HashMap存储和检索的效率。
例如:Java 中的 “FB” 和 “Ea”
哈希冲突(哈希碰撞)的解决方案
Java的哈希表(如 HashMap)通过数组+链表(或红黑树)来实现的
数组:存储桶(Bucket),每个桶对应一个链表或红黑树。
哈希:基于 key 的哈希值计算为数组索引(index = hash(key) % capacity)。
冲突解决
- 链地址法(默认):同一索引的键值对存入链表。
- 红黑树法(Java 8+):当链表长度超过阈值(默认8)时,链表转为红黑树,提升查询效率。
HashMap底层实现原理
- HashMap底层是一个Node[]数组,每个元素存储一个链表或红黑树(Java 8+)
- 初始容量:默认16,扩容时,扩容后大小是上次大小的2倍
- 负载因子:默认0.75,当元素数量超过容量×负载因子时触发扩容
哈希函数hash()的作用
将键(Key)的hashCode()值映射到数组索引
目的:均匀分布数据,减少哈希冲突
链表/红黑树 - 链表:解决哈希冲突(同一索引的键值对存入链表)。
- 红黑树(Java 8+):当链表长度≥8时,自动转为红黑树,提升查询效率(O(log n) → O(1)平均)。
优势
O(1)平均时间复杂度:哈希计算快速,链表/红黑树优化冲突。
灵活:支持任意对象作为键(需正确实现hashCode()和equals())。
劣势
非线程安全:需手动同步或用ConcurrentHashMap。
不保证有序:遍历顺序与插入顺序无关。
适用:高频键值存取(如缓存、配置存储)。
不适用:需要有序遍历或范围查询(改用TreeMap)。
put与get实现
- put(k,v)实现原理
第一步先算 key 的哈希值,找到数组里对应的位置;
如果这个位置是空的,直接把 key-value 放进去就行;
如果位置有东西,先看第一个元素是不是和要存的 key 一样(比哈希 + equals),一样就覆盖 value;
不一样的话,要是这位置是红黑树就按树的方式插,不是就插链表末尾;
插完如果链表太长(≥8),就转成红黑树;最后看看元素够不够多,够了就扩容。 - get(k)实现原理
先算 key 的哈希值,找到数组对应的位置;
位置空就返回 null;
不空的话,先比第一个元素,一样就返回 value;
不一样的话,是红黑树就按树找,不是就遍历链表找,找到匹配的就返回 value,没找到就返回 null。
HashSet集合与Map的关系
- HashSet类中有一个全局变量HashMap map,然后在HashSet的构造函数中有一句话map=new HashMap(),说明在创建HashSet类对象的时候底层创建了一个HashMap对象
hashCode的本质
- 帮助HashMap和HashSet集合加快插入的效率,当插入一个数据时,通过hashCode能够快速地计算插入位置,就不需要从头到尾地使用equlas方法进行比较
哈希结构的集合中添加元素
- 先调用hashCode,唯一则存储,不唯一则再调用equals,结果相同则不再存储,结果不同则散列到其他位置。因为hashCode效率更高(仅为一个int值),比较起来更快
HashMap 和 Hashtable 的区别?
线程安全:Hashtable 线程安全(方法加 synchronized),HashMap 不安全;
空值:HashMap 允许键 / 值为空,Hashtable 不允许;
效率:HashMap 效率高,Hashtable 效率低(锁整张表);
容量:HashMap 初始容量 16,扩容 2 倍;Hashtable 初始 11,扩容 2n+1。
ConcurrentHashMap 如何保证线程安全?JDK7 和 JDK8 区别?
JDK7:分段锁(Segment),每个 Segment 是一个 HashMap,锁粒度小,并发高;
JDK8:取消 Segment,用 CAS+synchronized 锁链表 / 红黑树头节点,锁粒度更细,效率更高;
核心:比 Hashtable 并发高,比 HashMap 安全。
进程,线程,多线程
进程 vs 线程
进程:独立运行的程序实例
线程:进程内的执行单元,多个线程共享进程的资源(内存)
多线程
线程状态(Java线程生命周期)
新建状态 线程对象已创建,但还未调用start()方法
就绪(可运行)状态 调用了 start (),等 CPU 调度
阻塞状态 被阻塞,等待获取锁如synchronized,拿到锁就回到就绪态
无限等待 调 wait ()/join () 这些方法,无期限等待,需要别人唤醒
限时等待 限时等待,调用sleep (时间)/wait (时间),等够时间自动醒
结束状态 run()方法执行完毕,线程退出
sleep 和 wait 的区别:
所属类:sleep 是 Thread 类的静态方法;wait 是 Object 类的方法,所有对象都能调;
锁释放:sleep 不会释放锁;wait 会释放锁,让其他线程能抢;
唤醒方式:sleep 睡够时间自动醒,或 interrupt () 打断;wait 要么等 notify ()/notifyAll () 唤醒,要么等超时(有参 wait)。
多线程实现方式
- 继承Thread类
class MyThread extends Thread简单,但无法继承其他类(Java单继承限制) - 实现Runnable接口
class MyTask implements Runnable更灵活,支持多继承(推荐) - 实现Callable接口
class MyJob implements Callable<V>支持返回值和异常处理,需配合Future使用 - 线程池
ThreadPoolExecutor 管理线程生命周期,减少资源消耗(如Executors.newFixedThreadPool())ThreadPoolExecutor( corePoolSize, // 核心线程数(空闲时保留) maximumPoolSize, // 最大线程数 keepAliveTime, // 空闲线程存活时间 TimeUnit unit, // 时间单位 BlockingQueue<Runnable> workQueue, // 任务队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略 )
ThreadPoolExecutor线程池的核心参数
核心线程数:线程池长期维持的最小线程数(即使空闲也不会销毁)。
最大线程数:线程池允许的最大线程数
线程空闲存活时间:非核心线程在空闲时的存活时间。
时间单位:keepAliveTime 的时间单位(如 TimeUnit.SECONDS)。
任务队列:用于存放待执行任务的阻塞队列
线程工厂:用于创建新线程的工厂(可自定义线程名称、优先级等)。
拒绝策略:当任务无法执行时的处理策略(如抛异常、丢弃任务等)
常用线程池:
newFixedThreadPool():固定线程数,无限队列。
newCachedThreadPool():动态调整线程数,适用于短任务
计算密集型(如算法、大数据):核心线程数 = CPU 核心数 + 1;
IO 密集型(如数据库、网络请求):核心线程数 = CPU 核心数 * 2;
数据处理类项目(元数据 / 馆藏管理):用自定义ThreadPoolExecutor(核心线程 = CPU2,有界队列),适配分批 + 断点续传;
高并发类项目(库存 / 电商):用自定义ThreadPoolExecutor(核心线程 = CPU4,同步队列),适配短任务高并发;
定时任务类场景(盘点 / 索引更新):用ScheduledThreadPoolExecutor(核心线程数 = 任务数 + 2),避免任务阻塞;
检索 / 短任务场景:用同步队列 + 少核心线程,提升响应速度;IO 密集型任务用多核心线程 + 有界队列,提升吞吐量。
线程池执行流程
任务提交时,当前线程数 < 核心线程数,直接新建核心线程执行任务。
如果核心线程已满,任务进入阻塞队列排队。
如果队列也满了,且当前线程数 < 最大线程数,继续新建非核心线程执行任务。
如果队列满 + 最大线程也满,触发拒绝策略。
任务执行完,非核心线程空闲超过存活时间,会被回收,最终线程数回到核心线程数。
说一下线程池有哪几种常用拒绝策略?
AbortPolicy(默认) 直接抛 RejectedExecutionException 异常,阻止任务提交。
DiscardPolicy 悄悄丢弃当前任务,不抛异常、不处理。
DiscardOldestPolicy 丢弃队列里最早的那个任务,再尝试提交当前任务。
CallerRunsPolicy 让提交任务的主线程自己去执行,既不抛异常也不丢弃
锁机制
核心分为:乐观锁 vs 悲观锁
乐观锁:
认为线程不会竞争资源,不加锁执行,只在提交时检查是否有冲突(CAS 机制)
作用:读操作多、并发冲突低(如数据查询、缓存更新)
实现方式:AtomicInteger 基于CAS实现乐观锁;数据库更新时检查版本号
常用:如计数器、版本控制
悲观锁:
认为线程一定会竞争资源,先加锁再执行,防止并发冲突
作用:写操作多、并发冲突高(如库存扣减、订单创建)
实现方式:synchronized,ReentrantLock
AQS 原理
问:讲下 AQS,ReentrantLock 怎么实现的?
AQS 是锁的底层框架,核心是一个 state 变量 + 一个 CLH 双向队列。
加锁用 CAS 改 state,抢到就执行;
抢不到就入队列,阻塞等待唤醒。
ReentrantLock 就是基于 AQS 做的独占锁,可重入靠 state 累加实现。
什么是CAS,有什么问题
CAS 就是比较并交换,三个参数:内存值、预期值、新值。
相等就更新,不等就失败,靠 CPU 原子指令保证原子性。
问题有两个:
一是 ABA,值从 A 变 B 又变回 A,CAS 识别不出来,解决用版本号 AtomicStampedReference;
二是自旋空转,高并发下 CPU 会很高。
synchronized锁
synchronized 早期是个 “重量级锁”,Java 优化做了一个“按需升级”
升级机制:
synchronized 锁升级是单向的(无锁→偏向锁→轻量级锁→重量级锁),核心目的是 “能省则省”,减少性能消耗;
偏向锁给 “单线程独占” 用,轻量级锁(CAS +自旋)应对 “少量线程交替抢”,重量级锁解决 “大量线程激烈抢”;
升级的触发点是 “竞争程度”,竞争越激烈,锁越重,性能开销也越大。
可重入锁
核心逻辑:同一线程可重复获取同一把锁,避免死锁(如方法 A 调用方法 B,两者都加锁)。
实现:synchronized(默认可重入)、ReentrantLock(默认可重入)。
公平锁 vs 非公平锁
公平锁:线程按等待顺序获取锁,先等先得(无插队),ReentrantLock(true)。
非公平锁:线程获取锁时直接尝试 CAS,失败再排队(允许插队),synchronized/ReentrantLock(false)(默认)。
适用场景:
公平锁:需保证任务执行顺序(如你馆藏系统的日志写入);
非公平锁:追求高并发性能(如你库存系统的扣减,允许插队提升吞吐量)
读写锁(ReentrantReadWriteLock)
核心逻辑:将锁分为读锁(共享锁)和写锁(排他锁),读 - 读不互斥,读 - 写 / 写 - 写互斥。
适用场景:读多写少(如你青松职考的课程检索、茶悦的商品查询)。
自旋锁
核心逻辑:线程获取锁失败时,不阻塞,而是循环重试(自旋),减少上下文切换开销。
实现:AtomicReference(手动实现)、JVM 轻量级锁内置自旋。
适用场景:锁持有时间短(如你库存系统的高频扣减)
本地锁 vs 分布式锁
本地锁(单机)
实现:synchronized、ReentrantLock、ReadWriteLock。
适用场景:单 JVM 进程内的并发(如你文献项目的单机数据处理)。
局限:多机部署时失效(如你库存系统 100 + 仓库分布式部署,本地锁无法解决超卖)
分布式锁(多机)
核心逻辑:基于中间件实现跨 JVM 的锁,保证分布式系统数据一致性
实现:Redisson
分段锁
核心逻辑:将锁分段,每段独立加锁(如ConcurrentHashMap的 Segment 锁),减少锁竞争。
适用场景:大集合的高并发操作(如你青松职考的 10 万 + 课程数据缓存)
- volatile关键字
特点:
保证变量可见性(修改后立即刷新到主存,其他线程可见)。
不保证原子性(复合操作仍需锁)。
synchronized 和 Lock 的区别?
synchronized:关键字,自动释放锁,非公平锁,不可中断;
Lock:接口(ReentrantLock),手动释放锁(必须 finally),可公平 / 非公平,可中断、可尝试获取锁;
场景:简单同步用 synchronized,复杂场景(超时、中断)用 Lock。
什么是死锁?如何避免?
死锁:多个线程互相持有对方需要的锁,互相等待。
避免:① 按固定顺序加锁;② 加锁时设置超时时间;③ 用 Lock 的 tryLock () 尝试获取锁;④ 减少锁的持有时间。
ThreadLocal 为什么线程安全?会内存泄漏吗?
每个线程自己有一个 ThreadLocalMap,数据存在当前线程里,互不共享,所以安全。
内存泄漏是因为 Entry 的 key 是弱引用,但 value 是强引用,key 被回收后 value 还在。
所以线程池里必须用 remove,不然会串数据、内存泄漏。
死锁与避免
死锁条件:
互斥:资源至少被一个线程持有。
请求与保持:线程A持有资源1,请求资源2,线程B持有资源2,请求资源1。
不剥夺:资源不能被强制回收。
循环等待:线程形成环形等待链。
避免方法:
避免嵌套锁。
使用超时机制(如tryLock(timeout))。
统一锁顺序
总结
选型建议:
简单同步 → synchronized。
复杂锁需求 → ReentrantLock(如公平锁、可中断锁)。
无锁编程 → Atomic类(CAS操作)。
线程管理 → ExecutorService线程池。
核心原则:
减少锁粒度:尽量缩小同步范围。
避免阻塞操作:使用非阻塞IO(如NIO)。
善用并发工具:如CountDownLatch、CyclicBarrier、Semaphore。
JVM内存相关.
JVM 内存区域划分
JVM 运行时内存区域核心分为线程私有区,线程共享区,还有直接内存这三块
直接内存:
不属于 JVM 规范的内存区域:开发中 Redis、NIO 会用到直接内存,读写效率更高
线程私有:
- 程序计数器(记录指令地址);
- 虚拟机栈(存局部变量、方法入参、方法返回地址,易 OOM/StackOverflow);
- 本地方法栈(Native 方法(比如 Java 调用 C/C++ 代码)用的,底层核心库、中间件(Redis/RabbitMQ 客户端)会用到,开发中基本不用手动操作);
线程共享: - 堆:JVM 中最大的内存区域,唯一的对象实例存储区(所有 new 出来的对象都在这),也是 GC 垃圾回收的主要战场(易 OOM)
- 方法区(类的结构、字段、方法信息、常量、静态变量,元空间,元空间直接使用本地内存)。
常量池是方法区的一部分,存字符串常量、字面量等,JDK7 后常量池被移到 Java 堆中
JDK8 元空间(Metaspace)替代永久代,解决了什么问题?
永久代用 JVM 堆内存,容易 OOM;
元空间用本地内存(OS 内存),大小受物理内存限制,不易 OOM;
优化:元空间自动扩容,减少手动配置 PermSize 的麻烦。
内存泄漏与内存溢出(OOM)
内存泄漏:程序中已不再使用的对象由于某些原因未被垃圾回收器(GC)回收,导致内存占用持续增加;
内存溢出(OOM):内存不够用(比如堆满、栈太深),JVM 无法为新对象分配内存空间;
关系:内存泄漏是导致 内存溢出 的常见原因(比如静态集合存大量对象、未关闭连接)。
内存溢出是急性病,需通过代码优化和资源控制预防。
内存泄漏是慢性病,需通过工具分析和编码规范根治。
核心原则:
谁分配,谁释放(如try-finally、Closeable接口)。
避免全局静态集合存储对象。
善用监控工具(如JConsole、VisualVM)及时发现问题
堆
作用:存放所有对象实例,是垃圾回收(GC)的主要区域。
特点:
线程共享:所有线程通过 new 创建的对象均分配在此。
核心问题:频繁触发 GC 或内存泄漏可能导致 OOM。
细分区域(根据垃圾回收器不同划分方式不同):
新生代(Young Generation):Eden 区 + 两个 Survivor 区(S0/S1)。
老年代(Old Generation):长期存活的对象最终晋升至此。
永久代(PermGen,JDK 8 之前)→ 元空间(Metaspace,JDK 8+)。
GC 垃圾回收算法有哪些?
常见的有:标记-清除,标记-复制,标记-整理,分代收集四种算法
- 标记-清除
标记:从 GC Roots 开始遍历所有可用对象并标记。
清除:遍历整个堆,回收未被标记的对象。
缺点:效率较低(需两次遍历)。产生内存碎片,可能导致后续大对象分配失败。
适用场景:常用于老年代回收。 - 标记-复制
将内存分为两块(From 和 To),每次只使用一块。当进行 GC 时,将存活对象复制到另一块,然后清空原块。
缺点:内存利用率低(仅一半内存可用)。
适用场景:新生代。 - 标记-整理
标记:标记所有存活对象。
整理:将存活对象向一端移动,清除剩余的内存。
缺点:需移动对象,效率较低。
适用场景:老年代。 - 分代收集
原理:基于对象的生命周期将内存划分为新生代(Young Generation)和老年代(Old Generation),采用不同算法:
新生代:对象存活时间短,使用 标记-复制算法。
老年代:对象存活时间长,使用 标记-清除 或 标记-整理。
优点:结合多种算法优势,提升整体效率。
适用场景:主流 JVM 实现(如 HotSpot)。
垃圾回收器
Java 中的 垃圾回收器与 垃圾回收算法是紧密相关的,回收器是算法的具体实现
不同回收器会根据设计目标选择一种或组合多种或分阶段使用多种算法;以优化性能(如吞吐量、延迟)或适应特定场景(如大内存、低延迟)
常用 JVM 参数
查看 GC 日志:-Xloggc:
调整新生代与老年代比例:-XX:NewRatio=2(新生代:老年代 = 1:2)
设置 G1 区域大小:-XX:G1HeapRegionSize=
通过理解不同 GC 算法和回收器的特性,可以针对应用场景优化内存管理,平衡吞吐量与延迟,提升系统性能
基础堆配置(示例)
-Xms512m # 初始堆大小(建议设为物理内存的1/4~1/2)
-Xmx1024m # 最大堆大小(避免动态扩容带来的性能抖动)
-XX:NewRatio=2 # 新生代与老年代比例(默认2:1,即新生代占1/3堆)
-XX:SurvivorRatio=8 # Eden:Survivor比例(默认8:1:1)
关键问题与调优
- 本地方法栈溢出
原因:JNI调用耗尽本地栈空间(如sun.misc.Signal.handle(int sig))。
解决:减少本地方法调用或增大栈大小(-Xss2m)。
- 堆内存溢出(java.lang.OutOfMemoryError: Java heap space):
原因:对象过多或内存泄漏。
解决:调整 -Xmx(最大堆大小)和 -Xms(初始堆大小)。 - 方法区溢出(java.lang.OutOfMemoryError: PermGen space 或 Metaspace):
原因:动态生成过多类(如反射、动态代理)。
解决:调整 -XX:MaxPermSize(JDK 8 之前)或 -XX:MaxMetaspaceSize。 - 栈溢出(StackOverflowError):
原因:递归调用过深或无限循环。
解决:调整 -Xss(单个线程栈大小)
垃圾回收(GC)优化
减少GC停顿
调整堆大小:避免堆过大或过小,使用-Xmx/-Xms固定堆。
选择低延迟GC:如G1或ZGC(需JDK 11+)。
缩短GC周期:降低-XX:MaxGCPauseMillis(如设为200ms)。减少内存碎片
启用压缩整理(如Parallel Old使用-XX:+UseCompactOops)。
选择CMS或G1:减少内存碎片产生。GC日志分析(示例)
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
分析工具:性能监控与调优工具
工具 用途 示例命令
jvisualvm 实时监控CPU、内存、线程、类加载。 查看堆内存使用趋势,分析GC停顿。
jstack 导出线程堆栈信息,定位死锁或阻塞线程。 jstack -l> threaddump.txt
jmap 导出堆转储,分析内存泄漏。 jmap -dump:live,format=b,file=heap.hprof
Arthas 运行时诊断,监控方法调用、内存分配。 monitor命令跟踪对象创建和GC情况。
GCViewer:生成GC日志图表,识别高频GC区域。总结
优化原则:
权衡取舍:高吞吐量 vs 低延迟,内存占用 vs 性能。
监控驱动:通过工具定位瓶颈,而非盲目调整参数。
适配场景:根据应用类型(Web/桌面/大数据)选择合适的GC器和配置。
反射
反射是Java的动态特性,允许程序在运行时获取类的元数据(如字段、方法、构造函数),并操作对象的行为,而无需编译时知道具体类名。
应用场景:框架开发(如Spring、Hibernate)、动态代理、注解处理、测试工具等
其它
为什么重写equals
- Object类的equals,比较的是对象的地址值
- 如果希望比较对象的内容是否相同,必须重写equals方法
为什么重写equals就必须重写hashCode?自定义类型的实例对象使用set存储或作为Map的键需要做什么?
- equals()和hashCode()的作用
- equals() 用来比较两个对象是否相等;没有重写比较两个对象的地址值(地址值不会相同),若是想让两个内容相同的对象在equals后得到true,则需重写equals方法
- hashCode() 用来返回对象的hash码值,通常情况下,我们都不会使用到这个方法;本质是为了帮助HashMap和HashSet集合加快插入的效率
- 为什么只要重写了equals方法,就必须重写hashCode
核心原因是 HashMap 的存储规则:
先通过 hashCode 定位桶位,再通过 equals 判断 Key 是否相同;
不重写 hashCode 的话,内容相同的对象会因为内存地址不同,生成不同的 hash 值,被 HashMap 当成不同 Key;
所以必须重写 hashCode,保证 equals 为 true 的对象 hashCode 一定相等。
jdk,jre,jvm的含义以及三者之间的关系?
- JVM: Java虚拟机,是Java程序的运行环境,
- JRE: Java程序的运行时环境,包含JVM和运行时需要的核心类库
- JDK: Java程序的开发工具包,包含JRE和开发使用的工具
运行Java程序,只需安装JRE!!!
开发Java程序,必须安装JDK!!!
i++和++i的区别?
- i++ 先执行,后自增
- ++i 先自增,后执行
super与this代表的含义
- super:代表父类的存储空间标识(通过这个标识可以访问父类的成员和构造,但是其实是不同于父类的引用的)
- this:代表当前对象的引用(谁调用就代表谁)
final和finally的区别
- final是一个安全修饰符,用final修饰的类不能被继承,用final声明的方法不能被重写,使用final声明的变量是常量,不能被修改
- finally是在异常里经常用到的, 是try和cach里的代码执行完以后,必须要执行的代码块,主要执行一些释放资源的操作,比如说关闭数据库连接,或者关闭IO流
Java jdk1.7和1.8的区别
1.8特性新增
- Lambda表达式 支持函数式编程,简化回调逻辑
- Stream API 声明式集合操作,处理集合的流式操作,支持过滤、映射、聚合,简化集合遍历
- Optional类 解决空值问题,减少NullPointerException
- 默认接口方法 允许接口定义默认实现,兼容旧版本代码
- 类型注解(Type Annotations) 支持在类型声明上添加注解(如@NonNull),增强编译时检查。