Java面试题整理¶
一、Java基础¶
1. volatile关键字的作用¶
volatile主要有两个核心作用: - 保证可见性:一个线程修改了volatile变量,其他线程能立刻看到最新值,不会读到过期数据 - 禁止指令重排序:编译器和CPU不会对指令进行重排序,防止写操作还没完成就读取,最典型的应用就是单例模式的双重检查
2. 堆和栈的区别¶
简单来说: - 栈:运行时使用,存放局部变量、方法参数、返回地址,每个线程一个栈,自动分配释放,速度快但空间小 - 堆:存放对象实例和数组,所有线程共享,由GC管理,空间大但速度稍慢,容易内存泄漏
3. 8种基本数据类型¶
- 数值型:byte、short、int、long、float、double
- 字符型:char
- 布尔型:boolean
4. ==和equals的区别¶
- 对于基本数据类型,==比较的是对应的值;对于引用数据类型,==比较的是地址值
- equals方法如果未被重写,作用和==一致,但通常会被重写,比如String类型,equals方法用来比较字符串内容
为什么重写equals方法要重写hashcode方法? 这是为了保证对象在基于哈希的集合(如HashMap、HashSet)中能正确工作: 1. 如果两个对象通过equals比较相等,它们的hashCode必须相同 2. 如果两个对象的hashCode相同,它们不一定相等,此时再通过equals进一步比较 如果只重写equals不重写hashCode,会导致两个逻辑上相等的对象在集合中被当作不同对象处理
5. 深拷贝和浅拷贝的区别¶
对于基本类型,两者没区别,都是复制原始数据。主要区别在引用类型: - 浅拷贝:只复制引用,不复制指向的对象,修改原始对象会影响复制对象 - 深拷贝:复制引用指向的对象,修改原始对象不会影响复制对象
6. 接口和抽象类的区别¶
- 接口是行为的抽象(like a关系),抽象类是对类的抽象(is a关系)
- 接口没有构造方法,抽象类有构造方法
- 接口只有定义(Java 1.8后可定义default方法),抽象类可以有实现
- 抽象类只能单继承,接口可以多实现
- 接口成员变量默认为public static final,抽象类中成员变量默认default
7. final关键字的用法¶
- 修饰数据:变量只能赋值一次,成员变量必须在声明或构造方法中赋值
- 修饰方法参数:参数值在生命周期中不能改变
- 修饰方法:方法无法被重写
- 修饰类:类无法被继承
8. 面向对象三大特征¶
- 封装:把属性和方法隐藏起来,只暴露必要的访问接口,比如用private修饰成员变量
- 继承:从已有类派生出新类,新类可以使用父类的属性和方法并扩展新功能
- 多态:父类引用可以指向不同子类对象,三个条件:继承、方法重写、父类引用指向子类对象
9. 方法重载和重写的区别¶
- 重载:发生在同一个类中,方法名相同但参数列表不同(个数、类型、顺序),属于静态绑定
- 重写:发生在子类中,方法名、参数列表、返回值都相同,子类访问范围要大于等于父类,属于动态绑定
10. Java反射机制¶
反射让我们在运行时可以知道任意类的属性和方法,并能调用它们。获取Class对象有三种方式: 1. Object类的getClass()方法 2. 类名.class 3. Class.forName("带包名的类路径")
11. Java异常体系¶
Java异常分为Error和Exception,都继承自Throwable: - Error:程序无法处理的错误,如StackOverFlowError、OutOfMemoryError - Exception:程序可以处理的异常,分为运行时异常(RuntimeException)和非运行时异常(如IOException),非运行时异常必须处理
12. String的hashCode计算¶
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模
二、JVM相关¶
1. JVM中堆栈分别存放什么¶
- 虚拟机栈:每个方法执行时创建栈帧,存放局部变量表、操作数栈、动态链接、方法出口信息
- 堆:几乎所有对象实例和数组都在这里分配,是GC主要回收区域,分为年轻代(Eden+Survivor)和老年代
2. 双亲委派机制¶
类加载时先让父加载器找,父加载器找不到自己才找,保证Java核心库的类不会被替换。好处是防止重复加载、防止核心API被篡改。
JDBC等第三方库怎么加载? 用线程上下文类加载器(Thread.currentThread().getContextClassLoader()),因为启动类加载器找不到classpath下的第三方类,这其实是破坏了双亲委派机制。
3. 如何定位死循环导致的内存溢出¶
- 监控内存使用情况,看是否持续增长
- 生成堆转储文件(Heap Dump)
- 用MAT、JProfiler等工具分析堆转储文件,找出占用内存最多的对象和引用链
- 代码审查,查找死循环、递归、大对象创建等问题
- 调试定位具体原因
三、Java并发¶
1. HashMap是线程安全的吗?¶
HashMap不是线程安全的,多线程环境下可以使用ConcurrentHashMap。
2. 如何用Java实现多线程日志采集?需要注意哪些线程安全问题?¶
(这部分原笔记没展开,面试时可以从以下角度说:使用线程池处理采集任务,用ConcurrentHashMap或CopyOnWriteArrayList存储日志数据,注意日志文件写入的同步问题,可以使用锁或专门的日志库如Log4j2、Logback)
四、Spring相关¶
1. Spring循环依赖怎么解决?¶
用三级缓存解决,但只解决单例bean的setter/属性注入循环依赖,构造器循环依赖解决不了。过程是: 1. A创建完先放到三级缓存暴露自己 2. A注入B,B发现依赖A 3. B从三级缓存拿A(哪怕A还没完全创建好) 4. B创建完成放一级缓存 5. A拿到B创建完成放一级缓存
2. Spring三级缓存的数据结构¶
都是ConcurrentHashMap: - 一级缓存(singletonObjects):放完全创建好的bean - 二级缓存(earlySingletonObjects):放提前暴露的半成品bean(已实例化未注入属性) - 三级缓存(singletonFactories):放bean工厂,用来生成早期引用,解决aop代理的循环依赖
五、数据库相关¶
1. JDBC的使用流程¶
五步走:
1. 加载驱动:Class.forName("com.mysql.cj.jdbc.Driver")
2. 获取连接:DriverManager.getConnection(url, username, password)
3. 创建Statement/PreparedStatement:推荐用PreparedStatement防SQL注入
4. 执行SQL:executeQuery查,executeUpdate增删改
5. 关闭资源:先关结果集,再关Statement,最后关连接
2. Java的SPI机制¶
SPI是服务提供者发现机制,简单说就是"接口+实现",你写接口别人写实现,运行时自动找到实现类。最典型的就是JDBC,Java核心库只定义了java.sql.Driver接口,数据库厂商写自己的实现。
实现方式:在META-INF/services/下放文件,文件名是接口全名,内容写实现类全名,Java用ServiceLoader自动加载。
六、数据结构与算法¶
1. HashMap底层实现原理¶
HashMap底层由数组和链表(JDK 1.8后引入红黑树)组成: - 通过键的hashCode计算哈希值,再通过位运算和取模映射到数组索引 - 存储时如果索引位置为空直接存,否则用链表(红黑树)解决哈希冲突 - 当链表长度超过8且数组长度≥64时,链表转红黑树提高查找效率 - 当键值对数量达到负载因子(默认0.75)时触发扩容
2. 如何解决哈希冲突?¶
- 开放定址法:
- 线性探测:冲突时向后找空位置
- 二次探测:步长以二次方变化,减少聚集
- 链地址法:哈希值相同的元素存到同一个链表中,JDK 1.8后的HashMap用这种方式,链表过长时转红黑树
3. 时间复杂度¶
时间复杂度衡量算法运行时间随数据规模增长的趋势,用大O符号表示,只保留最高次项: - O(1):常数时间,如数组访问 - O(log n):对数时间,如二分查找 - O(n):线性时间,如遍历数组 - O(n log n):线性对数,如快速排序 - O(n²):平方时间,如冒泡排序 - O(2ⁿ):指数时间 - O(n!):阶乘时间
七、设计模式¶
1. 设计模式七大原则¶
- 单一职责原则:一个类只负责一项职责
- 接口隔离原则:使用多个隔离的接口
- 依赖倒转原则:依赖抽象不依赖具体
- 里氏替换原则:子类可扩展但不能改变父类原有含义
- 开闭原则:对扩展开放,对修改关闭
- 迪米特法则:一个类对其他类知道得越少越好
- 合成复用原则:尽量用组合/聚合,不用继承
2. 设计模式分类¶
- 创建型(5种):单例、抽象工厂、原型、建造者、工厂方法
- 结构型(7种):适配器、桥接、装饰、组合、外观、享元、代理
- 行为型(11种):模板方法、命令、访问者、迭代器、观察者、中介者、备忘录、解释器、状态、策略、职责链
3. 单例模式¶
保证某个类只能存在一个对象实例,提供静态方法获取实例。
实现方式: - 饿汉式(静态常量/静态代码块):类加载时就创建 - 懒汉式(线程不安全/同步方法/同步代码块):第一次使用时创建 - 双重检查:两次判空,第二次前加同步,需要volatile禁止指令重排 - 静态内部类:利用静态内部类延迟加载,线程安全 - 枚举:天然线程安全,防止反序列化,推荐使用
使用场景:配置管理类、连接池、日志对象等
4. 观察者模式¶
又叫发布-订阅模式,定义一对多依赖关系,主题状态变化时通知所有观察者。核心角色是Observer(观察者)和Subject(主题)。JDK中提供了java.util.Observable和java.util.Observer。
八、缓存相关¶
什么情况下会发生缓存雪崩?如何解决?¶
发生场景: - 大量缓存key同时过期,导致请求都打到数据库 - 缓存服务器宕机,所有请求直接访问数据库
解决方法: 1. 错开过期时间:给不同key的过期时间加随机值 2. 热点key永不过期:后台异步更新 3. 加锁排队:缓存失效时只允许一个线程重建缓存 4. 缓存高可用:搭建集群避免单点故障 5. 服务降级:缓存失效时返回默认值
九、IO模型¶
常见IO模型有四种: 1. 阻塞IO:调用IO时一直阻塞等待,一个线程处理一个连接 2. 非阻塞IO:没准备好立刻返回,需要轮询,CPU利用率高但忙轮询浪费 3. 多路复用IO:一个线程通过select/poll/epoll同时监听多个socket,哪个好处理哪个,主流方案(NIO、Netty、Redis都用) 4. 异步IO:发起请求后立刻返回,内核完成后通知,真正的异步
十、其他¶
1. Maven依赖冲突怎么解决?¶
先用mvn dependency:tree分析依赖树找出冲突,然后通过以下方式解决:
- 手动排除冲突的依赖
- 升级版本
- 使用<exclusions>标签排除
2. 如果发生数据延迟,怎么从Flink、Kafka、HBase三个组件查找问题?¶
Flink: - 检查并行度、窗口大小配置 - 查看Web UI看任务状态、资源使用 - 检查数据处理逻辑是否复杂
Kafka: - 检查主题分区和副本设置 - 监控生产和消费速率,看是否有积压 - 检查网络情况
HBase: - 检查表结构和配置 - 监控RegionServer负载 - 排查写入读取流程
3. 编程思想:人和门两个类,开门方法设计在哪个类?¶
应该设计在Door类中,因为开门是门的固有行为,门应该拥有开门、关门等共性方法,不同种类的门再扩展额外功能。
Java数据结构¶
※※集合之间的继承关系(集合的类型有哪些)¶
集合分为单列集合Collection接口和双列集合Map接口
- Collection 接口又包括List和Set子接口
- List可以存放重复的元素,List接口有两个实现类,分别是ArrayList和LinkedList
- ArrayList 底层是数组,LinkedList底层是链表
- Set 不能存放重复的元素,Set接口有两个实现类,分别是HashSet和TreeSet
- HashSet是无序的,基于HashMap实现的Set集合,用于存储唯一元素
- TreeSet 是有序的
- PriorityQueue 是 优先队列,可以按照比较器或元素的自然顺序进行排序。
- Map接口有两个实现类,分别是HashMap和TreeMap
- HashMap是无序的
- LinkedHashMap: 基于哈希表和双向链表实现的Map集合,保持插入顺序或访问顺序。
- TreeMap 是有序的 , 基于红黑树实现的有序Map集合,可以按照键的顺序进行排序
※※※String、StringBuilder、StringBuffer区别¶
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 底层存储 | final 修饰的字符数组 | 无final 修饰的字符数组 | StringBuffer 类和 StringBuilder 类基本一样 |
| 是否可变 | 不可变类 | 可变类 | |
| 操作对象 | 修改了String 的值,就会产生新的String对象 | 直接在原对象上进行操作 | |
| 线程安全性 | 方法都是用synchronized修饰的,因此是线程安全的 | ||
| 性能 | 更快(无同步) | 较慢(同步开销) | |
| 应用场景 | 如果需要经常修改字符串,就使用StringBuffer和StringBuilder, 优先选择StringBuilder,效率较高,但是多线程使用共享变量的时候,优先 选择StringBuffer,保证线程安全 |
※※hashmap和hashset区别¶
| 特性 | HashMap | HashSet |
|---|---|---|
| 实现接口 | Map |
Set |
| 存储方式 | 存储 Key-Value 对 | 仅存储 Value(实际用 HashMap 实现) |
| 允许重复 | Key 不可重复,Value 可重复 | 元素不可重复(基于 equals() 和 hashCode()) |
| Null 值 | 允许 1 个 null Key,多个 null Value | 允许 1 个 null 元素 |
| 底层数据结构 | 数组 + 链表/红黑树(JDK 8+) | 内部使用 HashMap 存储(Value 存为 PRESENT 常量) |
| 查找效率 | O(1)(平均情况) | O(1)(基于 HashMap 的 Key 查找) |
| 线程安全 | 非线程安全(可用 ConcurrentHashMap) | 非线程安全(可用 Collections.synchronizedSet) |
※※ConcurrentHashMap怎么实现线程安全的?¶
※※※※HashMap的底层实现,链表里放的是什么?¶
-
jdk1.8以前
-
hashmap由 数组-链表 的数据结构组成
-
创建hashmap对象的时候,同时创建一个长度为16的Entry数组
-
jdk1.8后
-
hashmap 由 数组-链表/红黑树 的数据结构组成
-
在第一次put元素的时候,创建一个长度为16的Node 数组
-
向对象中插入数据时
-
首先调用hashcode方法计算出 key 的hash 值
-
然后对数组长度取余
-
公式:
index = hash & (table.length - 1) -
计算得出向Node数组中存储数据的索引值
-
如果计算出的索引位置处没有数据,则直接将数据存储到数组中
-
如果计算出的索引位置处已经有数据 了,此时会比较两个key的hash值是否相同
-
如果不相同,那么在此位置划出一个节点来存储该数据(拉链法)
-
拉链法是HashMap解决哈希冲突的主要实现方式,也称为"链地址法"或"分离链表法"
-
当不同的键(key)通过哈希函数计算得到相同的数组下标(即发生哈希冲突)时,拉链法的处理方式是:
-
数组+链表结构:HashMap内部维护一个Node数组(table),每个数组元素称为一个"桶"(bucket)
-
链表存储冲突元素:当多个键映射到同一个数组索引时,这些键值对会以链表形式存储在该桶中
-
新元素插入链表头部:JDK 1.7将新元素插入链表头部,JDK 1.8改为尾部
-
优点:
-
- 实现简单
- 有效处理冲突
-
哈希表空间利用率高
-
缺点:
-
- 链表过长会影响性能(JDK 1.8通过树化优化)
- 需要额外的空间存储指针
-
-
如果相同,此时发生hash碰撞,那么底层就会调用equals方法比较两个key的内容是否相同
-
如果相同,就将后 添加的数据的value覆盖之前的value
- 如果不相同,就继续向下和其他数据 的key进行比较
- 如果都不相等,就划出一个节点存储数据
-
-
当 链表长度大于阈值8(链表长度符合泊松分布,而长度为8个命中概率很小),并且 数组长度大于64,则将链表变为红黑树
-
当 链表长度长度小于等于6(不选择7 是防止频繁的发生转换)的时候将红黑树退化为链表
-
目的:优化极端情况下(大量哈希冲突)的查询效率,从O(n)提升到O(log n)
-
※※※※HashMap的扩容机制¶
- 什么时候需要扩容
- 负载因子0.75,超过75%就扩容:当hashmap中的元素个数超过 数组长度*负载因子 时,就会进行数组扩容。默认情况下当hashmap中的元素个数超过 16*0.75=12的时候
- 当hashmap中的其中一个链表的对象个数如果超过了8个,此时如果数组长度没有达到64,那么hashmap会先扩容解决。如果已经达到了64, 那么这个链表会变为红黑树,节点类型由Node变成TreeNode类型。
- 扩容机制
- 把原有的数组扩大为原来的两倍,然后重新计算每个元素在新数组的位置
- 这个过程本来是比较耗时的,但是因为每次扩容都是翻倍,与原来计算
hash & (table.length - 1)的结果相比,只是多了一个bit位,所以只需要看该bit 对应的hash值是1还是0就可以了。是0的话,索引不变,如果是1的 话,就变为原位置+旧容量的下标位置
HashMap中为啥用红黑树不用平衡树?¶
- 平衡二叉树追求“完全平衡”,要求太严格,导致每次插入或删除节点时,都花费更多的时间去调整。
- 红黑树不追求“完全平衡”,而是追求一种“弱平衡”状态,牺牲一定的查找效率,换取维持树平衡状态的成本。在插入或删除时不会进行频繁地调整。
hashmap、hashtable、ConcurrentHashmap(提到并发)¶
HashMap线程不安全,
HashTable线程安全但锁全表效率低,
ConcurrentHashMap使用了锁分段技术效率高
hashmap线程不安全体现在哪些方面¶
- 数组扩容进行rehash的过程中会死循环(链表:头会变成尾)
- 数据覆盖,比如有多个线程向hashmap中插入数据,恰好他们对应的下标相同,这时候就有可能出现数据覆盖的问题
※※※ConcurrentHashMap 是如何保证线程安全的¶
- jdk1.7【分段锁机制segment】
- 限制【锁粒度是 segment】,底层是
Segment<K,V>(一个hashmap对象)数组,将整个哈希表分成多个Segment(默认16个),每个Segment继承自ReentrantLock,相当于一个独立的锁- 向 concurrenthashmap 添加元素的时候
- 首先根据key计算出segment数组对应的下标
- 然后调用lock方法将该位置锁住
- 然后调用segment对象的put方法进行插入
- 最后调用unlock方法释放该位置的锁
- jdk1.8
- 限制【锁粒度是每个元素】,底层是数组-链表/红黑树
- 首先根据key计算数组对应的下标
- 如果下标没有元素,就利用CAS来保 证线程安全
- 如果有元素,那么采用的是synchronize同步锁的方式来保证线 程安全。
※※※ArrayList和LinkedList区别¶
ArrayList和LinkedList如何选择?它们是线程安全的吗?怎么保证线程安全?有哪些可以替代的线程安全类?
| 名称 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 基于动态数组(实际上是新建一个新的符合要求大小的数组) | 基于链表 |
| 内存空间 | 存储元素在内存空间是连续的 | 存储元素在内存空间是不连续的 |
| 适用操作 | 适合随机访问(下标的方式),不适合插入删除操作,因为这样会移动大量的元素 | 不适合随机访问,适合插入和删除操作 |
| 是否线程安全 | 两者都是线程不安全的 | |
ArrayList变成线程安全的方式有:用 Collections 类的 synchronizedList方法进行包装 List<String> syncList = Collections.synchronizedList(new ArrayList<>()); List<String> syncLinkedList = Collections.synchronizedList(new LinkedList<>());代替CopyOnWriteArrayList 类代替 ArrayList读多写少:CopyOnWriteArrayList写多读少:Collections.synchronizedListVector 类代替ArrayList(性能较差,不推荐使用)其他并发集合(根据场景选择)(优先考虑并发容器而非同步包装器)队列场景:ConcurrentLinkedQueue、LinkedBlockingQueue映射场景:ConcurrentHashMap集合场景:CopyOnWriteArraySet |
※※ArrayList 扩容过程¶
- 创建ArrayList对象,调用空参构造方法的时候,首先初始化的数组 大小为0
-
第一次添加元素的时候,会将数组的长度扩充为10
-
当添加的元素超过10个的时候,首先会扩容到15,依次类推,每次扩充为原长度的1.5 倍
- 创建新数组,容量为原来的1.5倍,并检查是否超过最大容量限制
int newCapacity = oldCapacity - (oldCapacity >> 1);
- 最大能扩容的长度为
Integer.MAX_VALUE = 2的31次方-1 - 将元素逐个复制到新的数组
- 将 ArrayList 内部指向原数组的引用改成指向新的数组
- 完成扩容,可以继续添加新的元素
数据结构中数组和链表的区别?各自适合在什么场景下使用?Java中有什么具体的实现吗?¶
- 区别
- 数组在内存中连续,链表不连续
- 数组使用前要先固定长度,不能动态改变大小,链表支持动态增加或者删除元素
- 数组有越界风险,链表没
- 适用场景
- 数组:查多改少
- 链表:查少改多
- Java中具体的实现
- ArrayList 和 LinkedList,分别对应数组和链表
阻塞队列一般有哪几种呢?¶
Java并发包(java.util.concurrent)提供了多种阻塞队列实现,它们都实现了
BlockingQueue接口
-
ArrayBlockingQueue
-
特点:基于数组的有界阻塞队列
- 排序规则:FIFO(先进先出)
- 锁机制:使用单个ReentrantLock实现线程安全
- 适用场景:固定大小的线程池(如Executors.newFixedThreadPool)
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(100);
-
LinkedBlockingQueue
-
特点:基于链表的可选有界/无界阻塞队列
- 排序规则:FIFO
- 锁机制:采用"两锁队列"算法(putLock和takeLock分离)
- 优势:比ArrayBlockingQueue有更高的吞吐量
// 无界队列
BlockingQueue<String> unbounded = new LinkedBlockingQueue<>();
// 有界队列
BlockingQueue<String> bounded = new LinkedBlockingQueue<>(1000);
冒泡排序空间复杂度会不会随数据量变大而变大¶
几种结果的存储形式,读写优势等
冒泡排序和快速排序对比¶
AVL树和红黑树两种结构解释一下¶
排序有哪几种¶
希尔排序有了解吗?¶
解释一下快排的原理吧,快排复杂度,为什么快排不稳定,举例¶
归并和堆排序为什么稳定¶
栈和堆两种数据结构解释一下¶
HashMap的底层结构?¶
hashmap是否线程安全?hashmap为什么不安全?(2)¶
如果频繁哈希冲突,有什么方法解决呢?¶
哈希表实现方式,处理冲突方式¶
对一个几乎有序的数据,选取哪几种排序比较合适¶
TreeMap查询写入的时间复杂度多少?¶
求二叉树深度的算法呢?说说看?要求时间和空间尽可能的小¶
JVM¶
JVM的内存结构,哪些是共享的,哪些是线程私有的?¶
JVM内存结构总体分为线程私有和线程共享两大部分:
线程私有(每个线程独立一份,随线程创建而创建,随线程结束而销毁)¶
- 程序计数器:存储当前线程执行的字节码指令地址,是线程私有的,不存在OOM,也没有GC
- 虚拟机栈:存储方法执行时的栈帧(局部变量表、操作数栈、动态链接、方法出口等),每个线程一个独立虚拟机栈,栈深度溢出会抛出
StackOverflowError,扩展失败会抛出OOM - 本地方法栈:为Native方法服务,和虚拟机栈作用类似,HotSpot虚拟机将其与虚拟机栈合二为一,也是线程私有
线程共享(整个JVM进程内所有线程共享,随JVM启动而创建)¶
- 堆:存放所有创建的对象和数组,是垃圾回收的主要区域,可分为新生代(Eden、From Survivor、To Survivor)和老年代,内存不足时抛出OOM
- 元空间(方法区):存储虚拟机加载的类信息、常量、静态变量、运行时常量池等数据。JDK1.8之前叫永久代,JDK1.8之后使用本地内存实现叫元空间,内存不足时抛出OOM
总结:程序计数器、虚拟机栈、本地方法栈是线程私有;堆、元空间是线程共享。
JVM内存模型¶
Java内存模型(JMM,Java Memory Model)是定义了共享内存系统中多线程读写共享变量规则的一种抽象概念,屏蔽了各种硬件和操作系统的内存访问差异,目的是解决多线程环境下内存可见性、有序性、原子性问题。
核心结构¶
- 主内存:所有共享变量都存储在主内存中,所有线程都可以访问
- 工作内存:每个线程都有自己私有的工作内存,保存了该线程使用的共享变量的副本,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量
- 交互规则:线程不能直接操作主内存,只能先把主内存中的变量读到自己的工作内存,修改后再写回主内存
解决的三大问题¶
- 原子性:保证一个操作要么全部执行成功,要么全部不执行
-
JMM只能保证基本的原子性(变量读写),如果需要更大范围的原子性,需要用
synchronized或Lock -
可见性:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改
- 通过
volatile关键字保证,每次修改都会强制刷回主存,并使其他线程工作内存中的缓存失效 -
synchronized和final也能保证可见性 -
有序性:程序执行的顺序按照代码的先后顺序执行
- 处理器和编译器可能会对指令进行重排序优化,多线程下重排序可能会影响结果正确性
- 通过
volatile关键字禁止指令重排序,synchronized也能保证有序性 - 先行发生原则(Happens-Before):JMM定义了一些天然的先行发生关系,无需任何同步手段就能保证成立
关键概念¶
- 指令重排序:编译器为了提高性能,会对指令顺序进行重新排序,在单线程下不影响结果,但多线程下可能导致问题
- happens-before原则:JMM规定的先行发生规则,只要满足这个规则,就能保证可见性
注意:JVM内存结构 ≠ JVM内存模型 - JVM内存结构:指的是JVM运行时数据区划分(堆、栈、方法区等),是物理内存划分 - JMM:指的是多线程并发访问共享变量的内存访问规则,是抽象规范
描述一下JVM加载class文件的原理机制?¶
JVM中类的装载是由ClassLoader和它的子类来实现的,Java ClassLoader是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件的类。
JVM 一个类的加载过程¶
- 加载:通过一个类的全限定名来获取此类的二进制字节流,然后在内存中生成一个代表这个类的Class对象
- 验证:确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证虚拟机的安全
- 准备:为类变量(即静态变量,被
static修饰的变量)赋默认初始值,int为0,long为0L,boolean为false,引用类型为null;常量(被static final修饰的变量)赋真实值 - 解析:把符号引用翻译为直接引用
- 初始化:执行类构造器
<clinit>()方法,真正初始化类变量和其他资源 - 使用:使用这个类
- 卸载:一般情况下JVM很少会卸载类,如果卸载类需要满足以下三个条件:
- 该类所有的实例都已经被垃圾回收,也就是JVM中不存在该类的任何实例
- 加载该类的类加载器已经被垃圾回收
- 该类的Class对象没有在任何地方被引用
※ JVM 中的垃圾回收算法¶
标记-清除算法¶
- 原理:分为标记和清除两个阶段,标记就是标记出所有需要回收的对象,也可以反过来标记出所有存活的对象,标记的时候是基于可达性分析算法来判断对象是否可以回收;清除就是标记之后,对所有未被标记的对象进行回收
- 优点:实现简单
- 缺点:
- 执行效率不稳定,如果堆中包含大量对象,并且大部分是需要回收的,这时必须进行大量标记和清除的动作,效率就会降低
- 内存空间的碎片化问题,标记清除之后会产生大量不连续的内存碎片,碎片太多可能会导致在需要分配较大对象时无法找到足够的连续内存,而不得不触发一次GC
标记-复制算法¶
- 原理:将可用内存按容量分为大小相等的两块,每次只使用其中一块,当这一块的内存用完了,就将还存活的对象复制到另外一块内存上,然后再把已使用过的内存空间一次清理掉
- 优点:实现简单,效率高,解决了标记清除算法导致的内存碎片问题
- 缺点:
- 将可分配内存缩小了一半,空间浪费太多了
- 当对象存活率比较高时,就要进行较多的复制操作,效率将会降低
标记-整理算法¶
- 原理:分为标记和整理两个阶段,标记就是基于可达性分析算法对需要回收的对象进行标记;整理就是根据存活对象进行整理,让存活对象都向一端移动,然后直接清理掉边界以外的内存
- STW:在对存活对象进行整理,移动存活对象的时候,必须暂停用户应用程序
- 优点:
- 提高了空间利用率(和标记复制算法相比)
- 不会产生不连续的内存碎片
- 缺点:
- 效率变低了,因为移动存活对象是一个很耗时的操作,并且会停止其他用户程序的运行
分代收集算法¶
- 现在一般虚拟机的垃圾收集都是采用分代收集算法
- 根据对象存活周期的不同将内存划分为几块,一般把Java堆分为新生代和老年代,JVM根据各个年代的特点采用不同的收集算法:
- 新生代中,每次进行垃圾回收都会发现大量对象死去,只有少量存活,因此采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集
- 老年代中,因为对象存活率较高,采用标记清理、标记整理算法来进行回收
JVM 垃圾收集器¶
Serial(Client 模式下默认的垃圾回收器)¶
- 特点:单线程的,收集时需要暂停所有用户线程的工作,所以有卡顿现象,效率不高
- 优点:简单,不会有线程切换的开销
- 参数:
-XX:-UseSerialGC
ParNew¶
- 特点:Serial收集器的多线程版本,大部分基本一样,单CPU下,ParNew还需要切换线程,可能还不如Serial
- 配合:Serial 或者 ParNew 收集器可以配合 CMS 收集器,前者收集新生代,后者收集老年代
- 参数:
-XX:-UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代垃圾回收器-XX:-UseParNewGC:强制指定使用ParNew
Parallel Scavenge(简称 Parallel,JVM 默认的新生代垃圾回收器)¶
- 特点:基于复制算法,并行的多线程收集器,侧重于达到一个可控的吞吐量(虚拟机运行100分钟,垃圾收集花1分钟,则吞吐量为99%),有时候也叫做吞吐量垃圾回收器或者吞吐量优先的垃圾回收器
- 参数:
-XX:MaxGCPauseMills:最大停顿时间(不是设置的越小越好,GC暂停时间越短,那么GC的次数就会变得更多)-XX:-UseAdaptiveSizePolicy:自适应新生代大小策略(默认开启),当这个参数开启之后,就不需要人工指定新生代的大小,eden与survivor的比例、晋升老年代对象大小等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间获得最大的吞吐量,这种调节方式称为垃圾收集的自适应的调节策略-XX:-UseParallelGC:指定使用Parallel Scanvenge垃圾回收器