Java基础
Double和BigDecimal的区别
Double使用二进制浮点表示法,存储小数点会丢失精度
BigDecimal采用字符串存储小数值,用整数运算模拟浮点运算,用BigInteger存储数值,scale存小数位数
用BigDecimal要避免double直接转换
为什么要有包装类型
泛型只能使用引用类型,基本类型无法转成引用类型,集合不存基本类型
怎么深拷贝
- 实现Cloneable接口并重写clone()方法
- 序列化和反序列话
- 手动递归复制
HashMap原理
底层使用数组+链表+红黑树
数组(Node<K,V>[] table)存储HashMap中的所有键值对,当哈希冲突发生时,多个元素会以链表的方式存储在同一个桶中,当链表长度超过8时,链表会转化为红黑树,提高查找效率。
当调用put时,通过key.hashCode()计算哈希值,通过(h^(h>>>16))进行扰动,减少哈希冲突,计算索引位置:index=(n-1)&hash(n是数组长度)。
HashMap默认初始容量是16,负载因子默认是0.75。当元素个数超过容量负载因子(默认160.75=12)时,触发扩容,将容量翻倍(newCapacity=oldCapacity*2),并重新计算hash存储位置。
ConcurrentHashMap实现
jdk1.7 segment数组+HashEntry数组+链表 segment继承ReentrantLock,采用分段锁(默认16),每一把锁只锁一个segment
jdk1.8 node数组+链表+红黑树 初始化node使用cas,node值用volatile修饰,放数据使用cas+synchronized
ConcurrentHashMap的put过程
- 延迟初始化table
- 使用CAS插入,避免同一个槽位插入不同数据(槽位为null)
- 发现正在扩容时(MOVED),帮助扩容
- 如果槽位有数据,用synchronized锁住链表头节点,key存在则更新,不存在则使用尾插入新节点。
如果是红黑树,用putTreeVal插入 - 如果是链表长度大于8会转红黑树,但数组长度小于64会优先扩容
- 当前存储数量+1,判断是否需要扩容
NIO
buffer channel seletor
client → buffer → channel → seletor → thread → server
channel类似于流,每个channel对应一个buffer缓冲区,channel注册到seletor
seletor会根据channel上发生的读写事件,将请求交由某个空闲的线程处理,seletor对应一个或多个线程
buffer和channel都是可读可写的
String为什么是final修饰的
- 保证不可变性:final使得String类不能被继承
- 提升安全性:如果String是可变的,攻击者可能会修改字符串的值,从而影响程序逻辑。
- 提升哈希码缓存效率:String类重写了hashCode()方法,并且使用了缓存机制。如果String是可变的,那么哈希值就不能缓存。
- 保证字符串池(String Pool)的有效性:字符串池存储了字符串常量,如果String是可变的,那么某个字符串被修改后,其他引用相同字符串的变量也会受到影响。
- 避免恶意继承:如果String不是final,开发者可以创建String的子类,并篡改字符串的行为,这可能会引发难以排查的bug。
想在运行jar包之前,做一些打其他处理
使用 Java 代理(-javaagent)
1 | java -javaagent:myagent.jar -jar myapp.jar |
使用 ClassLoader 代理
创建一个 自定义的 ClassLoader,在main方法执行前加载A类,并在main方法前后插入代码。
1 | java -cp bootloader.jar:myapp.jar BootLoader |
数据库驱动指定类名后是怎么加载的
JDBC 连接数据库时,我们通常通过 Class.forName() 手动加载驱动类,其本质是让 JVM 加载指定的类,这样类的静态代码块就会执行,其中通常会调用DriverManager.registerDriver() 方法,将该驱动注册到 JVM 中
1 | META-INF/services/java.sql.Driver |
当JVM启动并加载这个驱动类jar时,SPI(Service Provider Interface)机制会自动把驱动类加载进来并注册
在Spring Boot中,driver-class-name 可以不写,因为Spring Boot会根据你配置的url自动推断驱动类名。这个推断过程依赖DatabaseDriver.fromJdbcUrl()方法
java是真泛型吗
Java 泛型采用的是类型擦除机制,也就是说泛型信息只在编译期有效,运行时 JVM 并不知道泛型的实际类型
T被擦成了Object或父类
java spi
提供了一种服务发现机制,允许在程序外部动态指定具体实现。
通过URL工具类从jar包的/META-INF/services目录下面找到对应的文件,读取这个文件的名称找到对应的spi接口,通过InputStream流将文件里面的具体实现类的全类名读取出来,根据获取到的全类名,先判断跟spi接口是否为同一类型,如果是的,那么就通过反射的机制构造对应的实例对象,将构造出来的实例对象添加到Providers的列表中。
Java多线程
ThreadPoolExecutor参数
int corePoolSize 核心线程池大小
int maximumPoolSize 最大线程池大小
long keepAliveTime 超时了没有调用就释放线程
TimeUnit unit 超时单位
BlockingQueue
ThreadFactory threadFactory 线程工厂,创建线程
RejectedExecutionHandler handle 拒绝策略
4个拒绝策略
- AbortPolicy:队列满了,丢弃任务,抛异常
- CallerRunsPolicy:哪来的,去哪,提交任务的线程执行
- DiscardPolicy:队列满了,丢掉,不抛异常
- DiscardOldestPolicy:队列满了,尝试去和最早的竞争,丢弃队列最前面的任务,提交拒绝的任务
死锁条件
- 互斥条件 不用互斥锁,用AtomicInteger、CAS
- 请求与保持条件 同步获取需要操作的资源,其他线程等待
- 不剥夺条件 设置超时时间
- 循环等待条件 有序的获资源
synchronized锁升级
无锁
偏向锁:有线程访问时,对象头中的线程id设置为当前id
轻量级锁:当多个线程访问时,转成轻量级锁,使用CAS竞争锁
重量级锁:超过10次自旋
synchronized和ReentrantLock区别
synchronized | ReentrantLock |
---|---|
自动释放,会阻塞,底层使用monitorenter/monitorexit | 手动加锁、解锁,可中断,底层使用AQS实现阻塞队列、锁管理 |
都可重入
AQS原理
AQS 全称是 AbstractQueuedSynchronizer
通过一个共享资源状态state和FIFO双向队列来实现线程的排队、阻塞、唤醒
独占模式:同一时间只能有一个线程持有
共享模式:允许多个线程共享资源
CountDownLatch
递减 多少个线程完成后,执行下一步操作
countDown() 数量-1
await() 等待计数器归零,向下执行
CyclicBarrier
递增 等待多个线程就绪后,执行下一步操作
初始化,给与达到条件的数量和达到条件后的执行操作
await:等待线程达到数量后,往下执行
Semaphore
控制能够获得资源的线程数量
acquire() 获得,假如已经满了,等待被释放
release() 释放,会将当前的信号量释放+1,然后唤醒等待的线程
作用:多个共享资源互斥的使用,并发限流,控制最大的线程数
volatile
Java虚拟机提供轻量级的同步机制
- 内存屏障,写入时,会强制刷新到主内存,并使其他 CPU 缓存中的副本失效
- MESI缓存一致性协议,确保变量最新值
- 禁止指令重排 编译器优化、指令并行、内存系统重排
- 不能保证原子性
CAS
CAS(Compare And Swap,比较并交换)是一种无锁并发编程技术,主要用于乐观锁(Optimistic Locking)
比较当前工作内存中的值,如果这个值是期望的,那么则执行操作,如果不是就一直循环
伪代码:
1 | while (true) { |
优点
- 无锁并发:避免了线程加锁带来的上下文切换,提高吞吐量。
- 高效:适用于竞争不激烈的场景,减少线程阻塞,提高系统性能。
- 底层依赖CPU指令,操作原子性,无需额外加锁。
缺点
- 循环时间过长
- 只能保证一个变量的原子性
- ABA问题,增加版本号
CAS 与 Synchronized 的对比
对比项 | CAS | Synchronized |
---|---|---|
锁类型 | 无锁(乐观锁) | 互斥锁(悲观锁) |
性能 | 高(无线程阻塞) | 低(线程竞争时切换开销大) |
适用场景 | 读多写少 | 竞争激烈的场景 |
并发控制方式 | 自旋(不断尝试) | 阻塞(线程挂起等待) |
问题 | ABA 问题、自旋消耗 CPU | 线程上下文切换开销大 |
线程状态
NEW、RUNNABLE、BLOCKED、WATING、TIMED_WATING、TERMINATED
线程池状态
状态 | 描述 |
---|---|
running | 运行,接收任务 |
shutdown | 处于正在关闭,不接受任务,队列中的任务继续处理 |
stop | 停止状态,不接受任务,不处理队列中的任务 |
tidying | 没有线程运行 |
terminated | 线程池关闭 |
Jvm
对jvm的理解
JVM(Java Virtual Machine)主要负责加载字节码、执行代码、内存管理和垃圾回收
JVM内存
线程私有
- 程序计数器(PC 寄存器):存储当前线程正在执行的字节码行号。
- 虚拟机栈(JVM Stack):存储方法调用的局部变量表、操作数栈、方法返回地址等。每个方法执行时都会创建一个栈帧,方法执行完毕后,栈帧被销毁(典型的栈数据结构,先进后出)。
- 本地方法栈(Native Method Stack):用于执行本地方法(native 方法)。
线程共享
堆(Heap):存储对象实例,是GC(垃圾回收)的主要区域。按生命周期分为:
- 新生代(Young Generation):包含Eden区和Survivor区(S0、S1),新对象主要分配在Eden区。
- 老年代(Old Generation):存储生命周期较长的对象(从新生代晋升过来的)。
- 元空间(Metaspace,JDK 1.8+):替代方法区(Method Area),用于存储类元数据(class metadata)。
类加载机制
JVM采用懒加载(Lazy Loading),类在首次使用时才会被加载。
类加载过程包括5个阶段:
- 加载(Loading):将.class文件加载到内存,并创建Class对象。
- 验证(Verification):检查字节码是否合法,如格式、符号引用正确性等。
- 准备(Preparation):为静态变量分配内存,并赋默认值(不是初始化值)。
- 解析(Resolution):将符号引用转换为直接引用。
- 初始化(Initialization):执行类的
方法(静态变量赋值+静态代码块)。
类加载器(ClassLoader):
- 启动类加载器(BootstrapClassLoader):加载java.lang.*核心类库,如String、Thread等。
- 扩展类加载器(ExtClassLoader):加载ext目录中的类。
- 应用类加载器(AppClassLoader):加载classpath下的类。
- 自定义类加载器:开发者可以自定义类加载器,实现不同的类加载逻辑。
JVM 执行引擎
- 解释执行(Interpreter):逐行解析执行,速度较慢,但启动快。
- JIT(Just-In-Time,动态编译):将热点代码直接编译为机器码,提高执行效率。JVM主要使用C1(Client编译器)和C2(Server编译器)进行优化。
垃圾回收(GC 机制)
如何判断对象是否可以被回收
- 引用计数法(Reference Counting)→ 存在循环引用问题。
- 可达性分析(GC Roots)(JVM 采用):
GC Roots 包括:
方法区中静态变量引用的对象。
栈帧中引用的对象(局部变量)。
本地方法栈中 JNI(Native 方法)引用的对象。
垃圾回收算法
- 标记-清除(Mark-Sweep):标记存活对象,清除不可达对象,会产生内存碎片。
- 复制(Copying):将对象复制到新区域,减少碎片,但浪费一半内存。
- 标记-整理(Mark-Compact):标记存活对象,然后整理内存,适用于老年代。
垃圾回收器
- Serial GC:单线程 GC,适用于小型应用(默认用于-client模式)。
- Parallel GC(吞吐量优先):多线程收集新生代,适用于多核CPU,默认GC。
- CMS GC(低延迟):并发执行,适用于响应时间敏感的应用(如Web服务器)。
- G1 GC(JDK 9+ 默认):分区式收集,减少STW(Stop-The-World)时间,适用于大内存应用。
- ZGC(JDK 11+) & Shenandoah(JDK 12+):更低延迟,适用于超大内存应用。
JVM 调优
堆大小
1 | -Xms512m -Xmx1024m # 设置最小/最大堆内存 |
GC 相关参数
1 | -XX:+UseG1GC # 使用 G1 垃圾回收器 |
类加载
加载->验证->准备->解析->初始化->使用->卸载
加载:通过类的全限定名来获取类的二进制字节流,将这个字节流所代表的静态存储结构转化为元空间的运行时数据结构,再内存中生成一个代表这个类的Class对象,作为元空间这个类的各种数据的访问入口。
验证:确保Class文件的字节流中包含的信息符合当前虚拟机要求(文件格式验证、元数据验证、字节码验证、符号引用验证)
准备:正式为类变量分配内存并设置类变量初始值的阶段
解析:常量池内的符号引用替换成直接引用(类和接口的解析、字段解析、类方法解析、接口方法解析)
初始化:开始执行类中定义的Java程序代码
OOM定位
top 找内存大的进程号
jmap -histo pid
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=
导出文件 jmap -dump:format=b,file=dump.hprof 进程号
CPU高定位
top 找到cpu高的进程号
top -H -p 进程号 找CPU高的线程号
printf ‘0x%x\n’ 线程号 转成16进制
jstack 进程号 | grep 16进制线程号 -A 20
高并发系统jvm优化
- 内存预估,4核8G服务器,300-400并发
- 内存分配,8G内存服务器,jvm分配4g
- 内存占用动态推算,1秒占用多少内存空间,多少时间新生代Eden占满触发minor gc
- 新生代内存不足时,提高新生代内存比例,避免对象直接进入老年代。
- 系统中的@Service和@Controller的类尽快进入老年代,调低年龄进入老年代(-XX:MaxTenuringThreshold=5)。
- 大对象直接进入老年代(-XX:PretenureSizeThreshold=1M)。
- 指定合适的垃圾回收器。5.推算每隔几分钟Minor GC后有多少MB进入老年代,多少时间后触发Full GC,Full GC后内存整理。
- 尽可能让对象在新生代分配和回收,给系统足够内存空间