mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-29 23:23:43 +00:00
Merge pull request #1499 from CyC2018/update-java-notes-3
Update Java notes
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
# Java 容器 - 进阶
|
||||
<!-- GFM-TOC -->
|
||||
* [Java 容器 - 进阶](#java-容器---进阶)
|
||||
* [三、源码分析](#三源码分析)
|
||||
* [ArrayList](#arraylist)
|
||||
* [HashMap](#hashmap)
|
||||
* [ConcurrentHashMap](#concurrenthashmap)
|
||||
* [WeakHashMap](#weakhashmap)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
## 三、源码分析
|
||||
|
||||
以下源码基于 JDK 1.8,ConcurrentHashMap 的 size 操作基于 JDK 1.7。
|
||||
|
||||
### ArrayList
|
||||
|
||||
#### 序列化
|
||||
|
||||
主线见[Java 容器 · ArrayList 的序列化](Java%20容器.md#4-序列化)。
|
||||
|
||||
ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。
|
||||
|
||||
保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。
|
||||
|
||||
```java
|
||||
transient Object[] elementData; // non-private to simplify nested class access
|
||||
```
|
||||
|
||||
ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
|
||||
|
||||
```java
|
||||
private void readObject(java.io.ObjectInputStream s)
|
||||
throws java.io.IOException, ClassNotFoundException {
|
||||
elementData = EMPTY_ELEMENTDATA;
|
||||
|
||||
// Read in size, and any hidden stuff
|
||||
s.defaultReadObject();
|
||||
|
||||
// Read in capacity
|
||||
s.readInt(); // ignored
|
||||
|
||||
if (size > 0) {
|
||||
// be like clone(), allocate array based upon size not capacity
|
||||
ensureCapacityInternal(size);
|
||||
|
||||
Object[] a = elementData;
|
||||
// Read in all elements in the proper order.
|
||||
for (int i=0; i<size; i++) {
|
||||
a[i] = s.readObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
private void writeObject(java.io.ObjectOutputStream s)
|
||||
throws java.io.IOException{
|
||||
// Write out element count, and any hidden stuff
|
||||
int expectedModCount = modCount;
|
||||
s.defaultWriteObject();
|
||||
|
||||
// Write out size as capacity for behavioural compatibility with clone()
|
||||
s.writeInt(size);
|
||||
|
||||
// Write out all elements in the proper order.
|
||||
for (int i=0; i<size; i++) {
|
||||
s.writeObject(elementData[i]);
|
||||
}
|
||||
|
||||
if (modCount != expectedModCount) {
|
||||
throw new ConcurrentModificationException();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。
|
||||
|
||||
```java
|
||||
ArrayList list = new ArrayList();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
|
||||
oos.writeObject(list);
|
||||
```
|
||||
|
||||
### HashMap
|
||||
|
||||
#### 计算数组容量
|
||||
|
||||
主线见[Java 容器 · 计算数组容量](Java%20容器.md#7-计算数组容量)。
|
||||
|
||||
HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。
|
||||
|
||||
先考虑如何求一个数的掩码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到:
|
||||
|
||||
```text
|
||||
mask |= mask >> 1 11011000
|
||||
mask |= mask >> 2 11111110
|
||||
mask |= mask >> 4 11111111
|
||||
```
|
||||
|
||||
mask+1 是大于原始数字的最小的 2 的 n 次方。
|
||||
|
||||
```text
|
||||
num 10010000
|
||||
mask+1 100000000
|
||||
```
|
||||
|
||||
以下是 HashMap 中计算数组容量的代码:
|
||||
|
||||
```java
|
||||
static final int tableSizeFor(int cap) {
|
||||
int n = cap - 1;
|
||||
n |= n >>> 1;
|
||||
n |= n >>> 2;
|
||||
n |= n >>> 4;
|
||||
n |= n >>> 8;
|
||||
n |= n >>> 16;
|
||||
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
|
||||
}
|
||||
```
|
||||
|
||||
### ConcurrentHashMap
|
||||
|
||||
#### size 操作
|
||||
|
||||
这是 JDK 1.7 的实现,主线见[Java 容器 · size 操作](Java%20容器.md#2-size-操作),JDK 1.8 的计数方式见[JDK 1.8 的改动](Java%20容器.md#3-jdk-18-的改动)。
|
||||
|
||||
每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。
|
||||
|
||||
```java
|
||||
/**
|
||||
* The number of elements. Accessed only either within locks
|
||||
* or among other volatile reads that maintain visibility.
|
||||
*/
|
||||
transient int count;
|
||||
```
|
||||
|
||||
在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。
|
||||
|
||||
ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。
|
||||
|
||||
尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。
|
||||
|
||||
如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
|
||||
|
||||
```java
|
||||
/**
|
||||
* Number of unsynchronized retries in size and containsValue
|
||||
* methods before resorting to locking. This is used to avoid
|
||||
* unbounded retries if tables undergo continuous modification
|
||||
* which would make it impossible to obtain an accurate result.
|
||||
*/
|
||||
static final int RETRIES_BEFORE_LOCK = 2;
|
||||
|
||||
public int size() {
|
||||
// Try a few times to get accurate count. On failure due to
|
||||
// continuous async changes in table, resort to locking.
|
||||
final Segment<K,V>[] segments = this.segments;
|
||||
int size;
|
||||
boolean overflow; // true if size overflows 32 bits
|
||||
long sum; // sum of modCounts
|
||||
long last = 0L; // previous sum
|
||||
int retries = -1; // first iteration isn't retry
|
||||
try {
|
||||
for (;;) {
|
||||
// 超过尝试次数,则对每个 Segment 加锁
|
||||
if (retries++ == RETRIES_BEFORE_LOCK) {
|
||||
for (int j = 0; j < segments.length; ++j)
|
||||
ensureSegment(j).lock(); // force creation
|
||||
}
|
||||
sum = 0L;
|
||||
size = 0;
|
||||
overflow = false;
|
||||
for (int j = 0; j < segments.length; ++j) {
|
||||
Segment<K,V> seg = segmentAt(segments, j);
|
||||
if (seg != null) {
|
||||
sum += seg.modCount;
|
||||
int c = seg.count;
|
||||
if (c < 0 || (size += c) < 0)
|
||||
overflow = true;
|
||||
}
|
||||
}
|
||||
// 连续两次得到的结果一致,则认为这个结果是正确的
|
||||
if (sum == last)
|
||||
break;
|
||||
last = sum;
|
||||
}
|
||||
} finally {
|
||||
if (retries > RETRIES_BEFORE_LOCK) {
|
||||
for (int j = 0; j < segments.length; ++j)
|
||||
segmentAt(segments, j).unlock();
|
||||
}
|
||||
}
|
||||
return overflow ? Integer.MAX_VALUE : size;
|
||||
}
|
||||
```
|
||||
|
||||
### WeakHashMap
|
||||
|
||||
#### ConcurrentCache
|
||||
|
||||
主线见[Java 容器 · WeakHashMap](Java%20容器.md#weakhashmap)。
|
||||
|
||||
Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
|
||||
|
||||
ConcurrentCache 采取的是分代缓存:
|
||||
|
||||
- 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);
|
||||
- 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收;
|
||||
- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收;
|
||||
- 当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。
|
||||
|
||||
```java
|
||||
public final class ConcurrentCache<K, V> {
|
||||
|
||||
private final int size;
|
||||
|
||||
private final Map<K, V> eden;
|
||||
|
||||
private final Map<K, V> longterm;
|
||||
|
||||
public ConcurrentCache(int size) {
|
||||
this.size = size;
|
||||
this.eden = new ConcurrentHashMap<>(size);
|
||||
this.longterm = new WeakHashMap<>(size);
|
||||
}
|
||||
|
||||
public V get(K k) {
|
||||
V v = this.eden.get(k);
|
||||
if (v == null) {
|
||||
v = this.longterm.get(k);
|
||||
if (v != null)
|
||||
this.eden.put(k, v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
public void put(K k, V v) {
|
||||
if (this.eden.size() >= size) {
|
||||
this.longterm.putAll(this.eden);
|
||||
this.eden.clear();
|
||||
}
|
||||
this.eden.put(k, v);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [JDK 8 源码 java.util](https://github.com/openjdk/jdk8u/tree/master/jdk/src/share/classes/java/util)
|
||||
- [JDK 21 源码:ArrayList.java](https://github.com/openjdk/jdk21u/blob/master/src/java.base/share/classes/java/util/ArrayList.java)
|
||||
- [JDK 21 源码:HashMap.java](https://github.com/openjdk/jdk21u/blob/master/src/java.base/share/classes/java/util/HashMap.java)
|
||||
- [探索 ConcurrentHashMap 高并发性的实现机制](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/)
|
||||
- [The principle of ConcurrentHashMap analysis](http://www.programering.com/a/MDO3QDNwATM.html)
|
||||
+38
-240
@@ -9,9 +9,9 @@
|
||||
* [适配器模式](#适配器模式)
|
||||
* [三、源码分析](#三源码分析)
|
||||
* [ArrayList](#arraylist)
|
||||
* [LinkedList](#linkedlist)
|
||||
* [Vector](#vector)
|
||||
* [CopyOnWriteArrayList](#copyonwritearraylist)
|
||||
* [LinkedList](#linkedlist)
|
||||
* [HashMap](#hashmap)
|
||||
* [ConcurrentHashMap](#concurrenthashmap)
|
||||
* [LinkedHashMap](#linkedhashmap)
|
||||
@@ -200,74 +200,13 @@ public E remove(int index) {
|
||||
|
||||
#### 4. 序列化
|
||||
|
||||
ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。
|
||||
|
||||
保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。
|
||||
|
||||
```java
|
||||
transient Object[] elementData; // non-private to simplify nested class access
|
||||
```
|
||||
|
||||
ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
|
||||
|
||||
```java
|
||||
private void readObject(java.io.ObjectInputStream s)
|
||||
throws java.io.IOException, ClassNotFoundException {
|
||||
elementData = EMPTY_ELEMENTDATA;
|
||||
|
||||
// Read in size, and any hidden stuff
|
||||
s.defaultReadObject();
|
||||
|
||||
// Read in capacity
|
||||
s.readInt(); // ignored
|
||||
|
||||
if (size > 0) {
|
||||
// be like clone(), allocate array based upon size not capacity
|
||||
ensureCapacityInternal(size);
|
||||
|
||||
Object[] a = elementData;
|
||||
// Read in all elements in the proper order.
|
||||
for (int i=0; i<size; i++) {
|
||||
a[i] = s.readObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
private void writeObject(java.io.ObjectOutputStream s)
|
||||
throws java.io.IOException{
|
||||
// Write out element count, and any hidden stuff
|
||||
int expectedModCount = modCount;
|
||||
s.defaultWriteObject();
|
||||
|
||||
// Write out size as capacity for behavioural compatibility with clone()
|
||||
s.writeInt(size);
|
||||
|
||||
// Write out all elements in the proper order.
|
||||
for (int i=0; i<size; i++) {
|
||||
s.writeObject(elementData[i]);
|
||||
}
|
||||
|
||||
if (modCount != expectedModCount) {
|
||||
throw new ConcurrentModificationException();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。
|
||||
|
||||
```java
|
||||
ArrayList list = new ArrayList();
|
||||
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
|
||||
oos.writeObject(list);
|
||||
```
|
||||
保存元素的数组 elementData 用 transient 修饰,不参与默认的序列化。ArrayList 实现了 writeObject() 和 readObject(),只序列化前 size 个实际存放的元素,数组中空闲的位置不写出;反序列化时按 size 而不是原来的容量分配数组。源码见[进阶:序列化](Java%20容器%20-%20进阶.md#序列化)。
|
||||
|
||||
#### 5. Fail-Fast
|
||||
|
||||
modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
|
||||
|
||||
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。代码参考上节序列化中的 writeObject() 方法。
|
||||
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。代码参考[进阶:序列化](Java%20容器%20-%20进阶.md#序列化)中的 writeObject() 方法。
|
||||
|
||||
最常见的情况是在 foreach 循环中删除元素。foreach 编译后使用迭代器,list.remove() 改变了 modCount,迭代器下一次调用 next() 时就会抛出异常。应该使用迭代器自己的 remove(),或者 removeIf():
|
||||
|
||||
@@ -285,6 +224,38 @@ list.removeIf(s -> s.equals("a"));
|
||||
```
|
||||
|
||||
|
||||
### LinkedList
|
||||
|
||||
#### 1. 概览
|
||||
|
||||
基于双向链表实现,使用 Node 存储链表节点信息。
|
||||
|
||||
```java
|
||||
private static class Node<E> {
|
||||
E item;
|
||||
Node<E> next;
|
||||
Node<E> prev;
|
||||
}
|
||||
```
|
||||
|
||||
每个链表存储了 first 和 last 指针:
|
||||
|
||||
```java
|
||||
transient Node<E> first;
|
||||
transient Node<E> last;
|
||||
```
|
||||
|
||||
<div align="center"> <img src="pics/jcol-linkedlist.png" width="760px"/> </div><br>
|
||||
|
||||
#### 2. 与 ArrayList 的比较
|
||||
|
||||
ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。ArrayList 和 LinkedList 的区别可以归结为数组和链表的区别:
|
||||
|
||||
- 数组支持随机访问,但插入删除的代价很高,需要移动大量元素;
|
||||
- 链表不支持随机访问,但插入删除只需要改变指针。
|
||||
|
||||
需要注意的是,链表在中间插入前要先遍历到插入位置,这一步也是 O(N) 的,而且要逐个访问分散在内存中的节点;数组移动元素用的是 System.arraycopy(),一次复制一整块连续内存。例如在 10 万个元素的中间插入 1 万次,ArrayList 约 50 ms,LinkedList 约 0.8~1 秒;只有在头部插入时 LinkedList 才明显更快(JDK 21 实测,见[附录 A1](#a1-arraylist-的扩容与常见陷阱))。所以大多数场景下应该使用 ArrayList,需要在头尾频繁插入删除时使用 ArrayDeque 或 LinkedList。
|
||||
|
||||
### Vector
|
||||
|
||||
#### 1. 同步
|
||||
@@ -425,38 +396,6 @@ CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读
|
||||
|
||||
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
|
||||
|
||||
### LinkedList
|
||||
|
||||
#### 1. 概览
|
||||
|
||||
基于双向链表实现,使用 Node 存储链表节点信息。
|
||||
|
||||
```java
|
||||
private static class Node<E> {
|
||||
E item;
|
||||
Node<E> next;
|
||||
Node<E> prev;
|
||||
}
|
||||
```
|
||||
|
||||
每个链表存储了 first 和 last 指针:
|
||||
|
||||
```java
|
||||
transient Node<E> first;
|
||||
transient Node<E> last;
|
||||
```
|
||||
|
||||
<div align="center"> <img src="pics/jcol-linkedlist.png" width="760px"/> </div><br>
|
||||
|
||||
#### 2. 与 ArrayList 的比较
|
||||
|
||||
ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。ArrayList 和 LinkedList 的区别可以归结为数组和链表的区别:
|
||||
|
||||
- 数组支持随机访问,但插入删除的代价很高,需要移动大量元素;
|
||||
- 链表不支持随机访问,但插入删除只需要改变指针。
|
||||
|
||||
需要注意的是,链表在中间插入前要先遍历到插入位置,这一步也是 O(N) 的,而且要逐个访问分散在内存中的节点;数组移动元素用的是 System.arraycopy(),一次复制一整块连续内存。例如在 10 万个元素的中间插入 1 万次,ArrayList 约 50 ms,LinkedList 约 0.8~1 秒;只有在头部插入时 LinkedList 才明显更快(JDK 21 实测,见[附录 A1](#a1-arraylist-的扩容与常见陷阱))。所以大多数场景下应该使用 ArrayList,需要在头尾频繁插入删除时使用 ArrayDeque 或 LinkedList。
|
||||
|
||||
### HashMap
|
||||
|
||||
为了便于理解,以下源码分析以 JDK 1.7 为主。JDK 1.8 主要有以下变化,JDK 21 与 JDK 1.8 相同:
|
||||
@@ -811,36 +750,7 @@ JDK 1.8 的 resize() 就利用了这个性质:遍历旧桶中的链表,按 (
|
||||
|
||||
#### 7. 计算数组容量
|
||||
|
||||
HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。
|
||||
|
||||
先考虑如何求一个数的掩码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到:
|
||||
|
||||
```text
|
||||
mask |= mask >> 1 11011000
|
||||
mask |= mask >> 2 11111110
|
||||
mask |= mask >> 4 11111111
|
||||
```
|
||||
|
||||
mask+1 是大于原始数字的最小的 2 的 n 次方。
|
||||
|
||||
```text
|
||||
num 10010000
|
||||
mask+1 100000000
|
||||
```
|
||||
|
||||
以下是 HashMap 中计算数组容量的代码:
|
||||
|
||||
```java
|
||||
static final int tableSizeFor(int cap) {
|
||||
int n = cap - 1;
|
||||
n |= n >>> 1;
|
||||
n |= n >>> 2;
|
||||
n |= n >>> 4;
|
||||
n |= n >>> 8;
|
||||
n |= n >>> 16;
|
||||
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
|
||||
}
|
||||
```
|
||||
HashMap 构造函数允许传入的容量不是 2 的 n 次方,tableSizeFor() 会把它向上取为大于等于它的最小的 2 的 n 次方,例如传入 17 时容量为 32,传入 16 时仍为 16。这个计算只用移位和或运算,过程见[进阶:计算数组容量](Java%20容器%20-%20进阶.md#计算数组容量)。
|
||||
|
||||
#### 8. 链表转红黑树
|
||||
|
||||
@@ -920,75 +830,7 @@ static final int DEFAULT_CONCURRENCY_LEVEL = 16;
|
||||
|
||||
#### 2. size 操作
|
||||
|
||||
每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。
|
||||
|
||||
```java
|
||||
/**
|
||||
* The number of elements. Accessed only either within locks
|
||||
* or among other volatile reads that maintain visibility.
|
||||
*/
|
||||
transient int count;
|
||||
```
|
||||
|
||||
在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。
|
||||
|
||||
ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。
|
||||
|
||||
尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。
|
||||
|
||||
如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
|
||||
|
||||
```java
|
||||
/**
|
||||
* Number of unsynchronized retries in size and containsValue
|
||||
* methods before resorting to locking. This is used to avoid
|
||||
* unbounded retries if tables undergo continuous modification
|
||||
* which would make it impossible to obtain an accurate result.
|
||||
*/
|
||||
static final int RETRIES_BEFORE_LOCK = 2;
|
||||
|
||||
public int size() {
|
||||
// Try a few times to get accurate count. On failure due to
|
||||
// continuous async changes in table, resort to locking.
|
||||
final Segment<K,V>[] segments = this.segments;
|
||||
int size;
|
||||
boolean overflow; // true if size overflows 32 bits
|
||||
long sum; // sum of modCounts
|
||||
long last = 0L; // previous sum
|
||||
int retries = -1; // first iteration isn't retry
|
||||
try {
|
||||
for (;;) {
|
||||
// 超过尝试次数,则对每个 Segment 加锁
|
||||
if (retries++ == RETRIES_BEFORE_LOCK) {
|
||||
for (int j = 0; j < segments.length; ++j)
|
||||
ensureSegment(j).lock(); // force creation
|
||||
}
|
||||
sum = 0L;
|
||||
size = 0;
|
||||
overflow = false;
|
||||
for (int j = 0; j < segments.length; ++j) {
|
||||
Segment<K,V> seg = segmentAt(segments, j);
|
||||
if (seg != null) {
|
||||
sum += seg.modCount;
|
||||
int c = seg.count;
|
||||
if (c < 0 || (size += c) < 0)
|
||||
overflow = true;
|
||||
}
|
||||
}
|
||||
// 连续两次得到的结果一致,则认为这个结果是正确的
|
||||
if (sum == last)
|
||||
break;
|
||||
last = sum;
|
||||
}
|
||||
} finally {
|
||||
if (retries > RETRIES_BEFORE_LOCK) {
|
||||
for (int j = 0; j < segments.length; ++j)
|
||||
segmentAt(segments, j).unlock();
|
||||
}
|
||||
}
|
||||
return overflow ? Integer.MAX_VALUE : size;
|
||||
}
|
||||
```
|
||||
每个 Segment 用 count 记录自己的键值对个数,size() 要把所有 Segment 的 count 加起来。它先不加锁统计,相邻两次统计时各 Segment 的 modCount 之和相同,就认为结果正确;不加锁尝试 3 次仍不一致,再对所有 Segment 加锁后统计。源码见[进阶:size 操作](Java%20容器%20-%20进阶.md#size-操作)。
|
||||
|
||||
#### 3. JDK 1.8 的改动
|
||||
|
||||
@@ -1153,51 +995,7 @@ WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存
|
||||
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
|
||||
```
|
||||
|
||||
#### ConcurrentCache
|
||||
|
||||
Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
|
||||
|
||||
ConcurrentCache 采取的是分代缓存:
|
||||
|
||||
- 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);
|
||||
- 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收;
|
||||
- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收;
|
||||
- 当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。
|
||||
|
||||
```java
|
||||
public final class ConcurrentCache<K, V> {
|
||||
|
||||
private final int size;
|
||||
|
||||
private final Map<K, V> eden;
|
||||
|
||||
private final Map<K, V> longterm;
|
||||
|
||||
public ConcurrentCache(int size) {
|
||||
this.size = size;
|
||||
this.eden = new ConcurrentHashMap<>(size);
|
||||
this.longterm = new WeakHashMap<>(size);
|
||||
}
|
||||
|
||||
public V get(K k) {
|
||||
V v = this.eden.get(k);
|
||||
if (v == null) {
|
||||
v = this.longterm.get(k);
|
||||
if (v != null)
|
||||
this.eden.put(k, v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
public void put(K k, V v) {
|
||||
if (this.eden.size() >= size) {
|
||||
this.longterm.putAll(this.eden);
|
||||
this.eden.clear();
|
||||
}
|
||||
this.eden.put(k, v);
|
||||
}
|
||||
}
|
||||
```
|
||||
Tomcat 的 ConcurrentCache 就是这样用的:常用的对象放在 ConcurrentHashMap 中,不常用的对象放在 WeakHashMap 中,由垃圾收集器回收,实现见[进阶:ConcurrentCache](Java%20容器%20-%20进阶.md#concurrentcache)。
|
||||
|
||||
|
||||
## 附录:实测
|
||||
|
||||
+143
-33
@@ -7,11 +7,11 @@
|
||||
* [继承 Thread 类](#继承-thread-类)
|
||||
* [实现接口 VS 继承 Thread](#实现接口-vs-继承-thread)
|
||||
* [二、基础线程机制](#二基础线程机制)
|
||||
* [Executor](#executor)
|
||||
* [ThreadPoolExecutor](#threadpoolexecutor)
|
||||
* [Daemon](#daemon)
|
||||
* [sleep()](#sleep)
|
||||
* [yield()](#yield)
|
||||
* [Executor](#executor)
|
||||
* [ThreadPoolExecutor](#threadpoolexecutor)
|
||||
* [三、中断](#三中断)
|
||||
* [InterruptedException](#interruptedexception)
|
||||
* [interrupted()](#interrupted)
|
||||
@@ -82,6 +82,8 @@
|
||||
|
||||
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以理解为任务是通过线程驱动从而执行的。
|
||||
|
||||
这里的线程都是平台线程,每个对应一个操作系统线程。JDK 21 的虚拟线程由 JVM 调度到少量平台线程上运行,见[协程与虚拟线程](计算机操作系统%20-%20进程管理.md#5-协程)。
|
||||
|
||||
### 实现 Runnable 接口
|
||||
|
||||
需要实现接口中的 run() 方法。
|
||||
@@ -157,6 +159,49 @@ public static void main(String[] args) {
|
||||
|
||||
## 二、基础线程机制
|
||||
|
||||
### Daemon
|
||||
|
||||
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
|
||||
|
||||
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
|
||||
|
||||
main() 属于非守护线程。
|
||||
|
||||
在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程。
|
||||
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
Thread thread = new Thread(new MyRunnable());
|
||||
thread.setDaemon(true);
|
||||
}
|
||||
```
|
||||
|
||||
### sleep()
|
||||
|
||||
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
|
||||
|
||||
sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
|
||||
|
||||
```java
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep(3000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### yield()
|
||||
|
||||
对静态方法 Thread.yield() 的调用表示当前线程愿意让出 CPU,可以切换给其它线程来执行。该方法只是对线程调度器的一个提示,调度器可以忽略它,Javadoc 也说明很少有适合使用它的场合,主要用于调试和测试。
|
||||
|
||||
```java
|
||||
public void run() {
|
||||
Thread.yield();
|
||||
}
|
||||
```
|
||||
|
||||
### Executor
|
||||
|
||||
Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
|
||||
@@ -249,48 +294,62 @@ for (int i = 1; i <= 7; i++) {
|
||||
|
||||
估算值只是起点,最终要通过压测确定。上线后监控活跃线程数、队列长度和拒绝次数,必要时调用 setCorePoolSize()、setMaximumPoolSize() 在运行时调整。
|
||||
|
||||
### Daemon
|
||||
|
||||
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
|
||||
|
||||
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
|
||||
|
||||
main() 属于非守护线程。
|
||||
|
||||
在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程。
|
||||
线程池的核心结构并不复杂,常被要求手写一个简化版:固定数量的工作线程循环从阻塞队列中取任务执行,队列满了或者线程池已经关闭时拒绝新任务。
|
||||
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
Thread thread = new Thread(new MyRunnable());
|
||||
thread.setDaemon(true);
|
||||
}
|
||||
```
|
||||
public class SimplePool {
|
||||
private final BlockingQueue<Runnable> queue;
|
||||
private final List<Thread> workers = new ArrayList<>();
|
||||
private volatile boolean shutdown = false;
|
||||
|
||||
### sleep()
|
||||
public SimplePool(int threads, int capacity) {
|
||||
queue = new ArrayBlockingQueue<>(capacity);
|
||||
for (int i = 0; i < threads; i++) {
|
||||
Thread t = new Thread(() -> {
|
||||
// 关闭后也要把队列中剩下的任务执行完再退出
|
||||
while (!shutdown || !queue.isEmpty()) {
|
||||
Runnable task;
|
||||
try {
|
||||
task = queue.poll(100, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
if (task == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
task.run();
|
||||
} catch (RuntimeException e) {
|
||||
// 一个任务抛出异常,不能让工作线程退出
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}, "pool-" + i);
|
||||
workers.add(t);
|
||||
t.start();
|
||||
}
|
||||
}
|
||||
|
||||
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
|
||||
public void execute(Runnable task) {
|
||||
if (shutdown || !queue.offer(task)) {
|
||||
throw new RejectedExecutionException("线程池已关闭或队列已满");
|
||||
}
|
||||
}
|
||||
|
||||
sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
|
||||
|
||||
```java
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep(3000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
public void shutdown() {
|
||||
shutdown = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### yield()
|
||||
例如 2 个线程执行 4 个任务,其中任务 2 抛出异常,其余 3 个任务仍由这两个线程执行完;关闭之后再提交会被拒绝(JDK 21 实测,见[附录 A6](#a6-简化版线程池))。
|
||||
|
||||
对静态方法 Thread.yield() 的调用表示当前线程愿意让出 CPU,可以切换给其它线程来执行。该方法只是对线程调度器的一个提示,调度器可以忽略它,Javadoc 也说明很少有适合使用它的场合,主要用于调试和测试。
|
||||
ThreadPoolExecutor 的结构与它相同,多做了以下几件事:
|
||||
|
||||
```java
|
||||
public void run() {
|
||||
Thread.yield();
|
||||
}
|
||||
```
|
||||
- 线程不是一开始就全部创建,而是按前面的提交顺序按需创建;
|
||||
- 工作线程在 getTask() 中取任务:线程数超过核心线程数时用 poll(keepAliveTime) 等待,超时就退出,否则用 take() 一直等待;
|
||||
- 运行状态和线程数保存在同一个 AtomicInteger(ctl)中,两者可以在一次 CAS 中一起修改;
|
||||
- 用 execute() 提交的任务抛出异常时,执行它的线程退出,线程池再补一个新线程。
|
||||
|
||||
## 三、中断
|
||||
|
||||
@@ -2033,6 +2092,57 @@ for (String u : new String[]{"alice", "bob"}) {
|
||||
线程池中读到:alice
|
||||
```
|
||||
|
||||
### A6 简化版线程池
|
||||
|
||||
环境:JDK 21.0.13,Apple M1 Pro。正文中的 SimplePool 加上 awaitTermination()(依次 join 所有工作线程),用下面的代码运行 3 次:
|
||||
|
||||
```java
|
||||
SimplePool pool = new SimplePool(2, 10);
|
||||
for (int i = 1; i <= 4; i++) {
|
||||
int id = i;
|
||||
pool.execute(() -> {
|
||||
if (id == 2) {
|
||||
throw new IllegalStateException("任务 2 出错");
|
||||
}
|
||||
System.out.println("任务 " + id + " 由 " + Thread.currentThread().getName() + " 执行");
|
||||
});
|
||||
}
|
||||
pool.shutdown();
|
||||
try {
|
||||
pool.execute(() -> {});
|
||||
} catch (RejectedExecutionException e) {
|
||||
System.out.println("关闭后提交:" + e.getMessage());
|
||||
}
|
||||
pool.awaitTermination();
|
||||
System.out.println("全部完成");
|
||||
```
|
||||
|
||||
```text
|
||||
== run 1
|
||||
java.lang.IllegalStateException: 任务 2 出错
|
||||
关闭后提交:线程池已关闭或队列已满
|
||||
任务 3 由 pool-0 执行
|
||||
任务 1 由 pool-1 执行
|
||||
任务 4 由 pool-0 执行
|
||||
全部完成
|
||||
== run 2
|
||||
java.lang.IllegalStateException: 任务 2 出错
|
||||
关闭后提交:线程池已关闭或队列已满
|
||||
任务 1 由 pool-0 执行
|
||||
任务 4 由 pool-0 执行
|
||||
任务 3 由 pool-1 执行
|
||||
全部完成
|
||||
== run 3
|
||||
java.lang.IllegalStateException: 任务 2 出错
|
||||
关闭后提交:线程池已关闭或队列已满
|
||||
任务 3 由 pool-1 执行
|
||||
任务 4 由 pool-1 执行
|
||||
任务 1 由 pool-0 执行
|
||||
全部完成
|
||||
```
|
||||
|
||||
异常的堆栈只保留了第一行。任务执行的先后和线程的分配每次不同,但每次都是 3 个任务完成、关闭后的提交被拒绝。ThreadPoolExecutor 的 ctl、getTask() 中的 poll/take 见 JDK 21 源码 java/util/concurrent/ThreadPoolExecutor.java。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Object (Java SE 21)](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Object.html)
|
||||
|
||||
+41
-40
@@ -29,8 +29,8 @@
|
||||
* [双亲委派模型](#双亲委派模型)
|
||||
* [自定义类加载器实现](#自定义类加载器实现)
|
||||
* [五、线上问题排查](#五线上问题排查)
|
||||
* [JVM 调优](#jvm-调优)
|
||||
* [内存溢出与内存泄漏](#内存溢出与内存泄漏)
|
||||
* [JVM 调优](#jvm-调优)
|
||||
* [CPU 占用过高](#cpu-占用过高)
|
||||
* [死锁](#死锁)
|
||||
* [附录:实测](#附录实测)
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
### 程序计数器
|
||||
|
||||
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
|
||||
记录正在执行的虚拟机字节码指令的地址。如果正在执行的是本地方法,它的值是未定义的(JVM 规范 2.5.1 节)。
|
||||
|
||||
### Java 虚拟机栈
|
||||
|
||||
@@ -438,10 +438,6 @@ CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
|
||||
|
||||
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。JDK 9 起 G1 成为默认的垃圾收集器(JEP 248),CMS 在 JDK 14 中被移除(JEP 363)。
|
||||
|
||||
没有指定收集器时,JVM 按机器的配置选择:JDK 8 在服务器级的机器(至少 2 个处理器、约 2 GB 以上的物理内存)上默认使用 Parallel Scavenge 加 Parallel Old,否则使用 Serial;JDK 9 起把服务器级机器上的默认值改为 G1。
|
||||
|
||||
所以 JDK 21 在只有 1 个处理器的环境中,例如只分配了 1 个 CPU 的容器,默认使用的仍是 Serial(见[附录 A6](#a6-默认的收集器与-g1-的-region-大小))。JDK 27 起在所有环境中都默认使用 G1(JEP 523)。
|
||||
|
||||
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
|
||||
|
||||
<div align="center"> <img src="pics/gc-generations.png" width="740px"/> </div><br>
|
||||
@@ -482,6 +478,10 @@ Region 不区分固定的用途,一个 Region 回收后可以再作为 Eden、
|
||||
|
||||
G1 停顿的次数更多,但每次都很短,适合对响应时间敏感的服务。
|
||||
|
||||
没有指定收集器时,JVM 按机器的配置选择:JDK 8 在服务器级的机器(至少 2 个处理器、约 2 GB 以上的物理内存)上默认使用 Parallel Scavenge 加 Parallel Old,否则使用 Serial;JDK 9 起把服务器级机器上的默认值改为 G1。
|
||||
|
||||
所以 JDK 21 在只有 1 个处理器的环境中,例如只分配了 1 个 CPU 的容器,默认使用的仍是 Serial(见[附录 A6](#a6-默认的收集器与-g1-的-region-大小))。JDK 27 起在所有环境中都默认使用 G1(JEP 523)。
|
||||
|
||||
#### 8. ZGC 收集器
|
||||
|
||||
ZGC 的目标是让停顿时间不随堆的大小增长,JDK 15 起可以用于生产(JEP 377),JDK 21 加入了分代模式(JEP 439),JDK 23 起分代成为默认模式(JEP 474),JDK 24 移除了不分代的模式(JEP 490)。
|
||||
@@ -551,7 +551,7 @@ G1 的新生代回收和混合回收都要在停顿中把存活对象复制到
|
||||
|
||||
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
|
||||
|
||||
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
|
||||
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。需要注意的是,使用 G1 时不要用 -Xmn 固定新生代的大小,因为 G1 正是靠调整新生代的大小来满足停顿时间目标的(Oracle GC 调优指南)。
|
||||
|
||||
#### 3. 空间分配担保失败
|
||||
|
||||
@@ -887,39 +887,6 @@ public class FileSystemClassLoader extends ClassLoader {
|
||||
|
||||
## 五、线上问题排查
|
||||
|
||||
### JVM 调优
|
||||
|
||||
调优之前先明确目标:在线服务关心最长停顿时间,批处理任务关心吞吐量。大多数情况下先使用默认的 G1,开启 GC 日志观察,确实有问题再调整参数;线上的 GC 问题大多是代码造成的,例如内存泄漏、一次查询加载了太多数据,只调参数解决不了。
|
||||
|
||||
常用的参数:
|
||||
|
||||
| 参数 | 作用 |
|
||||
| --- | --- |
|
||||
| -Xms、-Xmx | 堆的初始大小和最大值,线上通常设为相同,避免运行中扩容和收缩 |
|
||||
| -XX:MaxRAMPercentage | 不指定 -Xmx 时,最大堆占物理内存(容器中为容器的内存限制)的比例,默认 25% |
|
||||
| -Xss | 每个线程的栈大小 |
|
||||
| -XX:MaxMetaspaceSize | 元空间的上限,默认不限制 |
|
||||
| -XX:+UseG1GC、-XX:+UseZGC | 选择垃圾收集器,JDK 9 起默认 G1 |
|
||||
| -XX:MaxGCPauseMillis | G1 的停顿时间目标,默认 200 ms |
|
||||
| -Xlog:gc*:file=gc.log | 输出 GC 日志(JDK 9 起;JDK 8 使用 -XX:+PrintGCDetails -Xloggc:gc.log) |
|
||||
| -XX:+HeapDumpOnOutOfMemoryError | 内存溢出时保存堆转储 |
|
||||
|
||||
例如一个程序长期持有约 60 MB 的对象,同时不断创建短命的对象,用 G1 分别以 72 MB 和 256 MB 的堆运行(JDK 21 实测,见[附录 A4](#a4-堆大小对-g1-的影响)):
|
||||
|
||||
| 堆大小 | 新生代回收 | Full GC | 停顿总时长 | 最长停顿 | 运行时间 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 72 MB | 454 次 | 6 次 | 168.1 ms | 5.88 ms | 398 ms |
|
||||
| 256 MB | 43 次 | 0 次 | 42.7 ms | 1.22 ms | 317 ms |
|
||||
|
||||
堆太小时,存活的对象几乎占满了老年代,日志中反复出现 Pause Full (G1 Compaction Pause) 71M->27M(72M):每次 Full GC 后仍有一半以上的空间被占用,很快又满了。
|
||||
|
||||
线上频繁 Full GC 的排查思路:
|
||||
|
||||
1. 用 jstat -gcutil 观察老年代使用率(O 列)和 Full GC 次数(FGC 列);
|
||||
2. Full GC 之后老年代使用率仍然很高,并且越来越高,说明对象一直无法回收,导出堆转储查找内存泄漏;
|
||||
3. Full GC 之后老年代能降下来,说明堆对于正常的负载偏小,或者有大对象、大量对象过早进入老年代,可以增大堆、调整新生代大小,或者从代码上减少一次创建的数据量;
|
||||
4. 元空间不足也会触发 Full GC,日志中的原因为 Metadata GC Threshold,检查是否在动态生成大量的类。
|
||||
|
||||
### 内存溢出与内存泄漏
|
||||
|
||||
OutOfMemoryError 的详细信息指明了是哪一块内存不足,常见的有:
|
||||
@@ -964,6 +931,39 @@ Dumping heap to java_pid24607.hprof ...
|
||||
Heap dump file created [19154891 bytes in 0.007 secs]
|
||||
```
|
||||
|
||||
### JVM 调优
|
||||
|
||||
调优之前先明确目标:在线服务关心最长停顿时间,批处理任务关心吞吐量。大多数情况下先使用默认的 G1,开启 GC 日志观察,确实有问题再调整参数;线上的 GC 问题大多是代码造成的,例如内存泄漏、一次查询加载了太多数据,只调参数解决不了。
|
||||
|
||||
常用的参数:
|
||||
|
||||
| 参数 | 作用 |
|
||||
| --- | --- |
|
||||
| -Xms、-Xmx | 堆的初始大小和最大值,线上通常设为相同,避免运行中扩容和收缩 |
|
||||
| -XX:MaxRAMPercentage | 不指定 -Xmx 时,最大堆占物理内存(容器中为容器的内存限制)的比例,默认 25% |
|
||||
| -Xss | 每个线程的栈大小 |
|
||||
| -XX:MaxMetaspaceSize | 元空间的上限,默认不限制 |
|
||||
| -XX:+UseG1GC、-XX:+UseZGC | 选择垃圾收集器,JDK 9 起默认 G1 |
|
||||
| -XX:MaxGCPauseMillis | G1 的停顿时间目标,默认 200 ms |
|
||||
| -Xlog:gc*:file=gc.log | 输出 GC 日志(JDK 9 起;JDK 8 使用 -XX:+PrintGCDetails -Xloggc:gc.log) |
|
||||
| -XX:+HeapDumpOnOutOfMemoryError | 内存溢出时保存堆转储 |
|
||||
|
||||
例如一个程序长期持有约 60 MB 的对象,同时不断创建短命的对象,用 G1 分别以 72 MB 和 256 MB 的堆运行(JDK 21 实测,见[附录 A4](#a4-堆大小对-g1-的影响)):
|
||||
|
||||
| 堆大小 | 新生代回收 | Full GC | 停顿总时长 | 最长停顿 | 运行时间 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 72 MB | 454 次 | 6 次 | 168.1 ms | 5.88 ms | 398 ms |
|
||||
| 256 MB | 43 次 | 0 次 | 42.7 ms | 1.22 ms | 317 ms |
|
||||
|
||||
堆太小时,存活的对象几乎占满了老年代,日志中反复出现 Pause Full (G1 Compaction Pause) 71M->27M(72M):每次 Full GC 后仍有一半以上的空间被占用,很快又满了。
|
||||
|
||||
线上频繁 Full GC 的排查思路:
|
||||
|
||||
1. 用 jstat -gcutil 观察老年代使用率(O 列)和 Full GC 次数(FGC 列);
|
||||
2. Full GC 之后老年代使用率仍然很高,并且越来越高,说明对象一直无法回收,导出堆转储查找内存泄漏;
|
||||
3. Full GC 之后老年代能降下来,说明堆对于正常的负载偏小,或者有大对象、大量对象过早进入老年代,可以增大堆、调整新生代大小,或者从代码上减少一次创建的数据量;
|
||||
4. 元空间不足也会触发 Full GC,日志中的原因为 Metadata GC Threshold,检查是否在动态生成大量的类。
|
||||
|
||||
### CPU 占用过高
|
||||
|
||||
1. 用 top 找到 CPU 占用高的 Java 进程;
|
||||
@@ -1205,6 +1205,7 @@ $ grep -c "unloading class Plugin" unload.log
|
||||
- [Guide to WeakHashMap in Java](http://www.baeldung.com/java-weakhashmap)
|
||||
- [Tomcat example source code file (ConcurrentCache.java)](https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/el/util/ConcurrentCache.java.shtml)
|
||||
- [Garbage-First (G1) Garbage Collector | Java SE 17 GC Tuning Guide](https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html)
|
||||
- [Garbage-First Garbage Collector Tuning | Java SE 21 GC Tuning Guide](https://docs.oracle.com/en/java/javase/21/gctuning/garbage-first-garbage-collector-tuning.html)
|
||||
- [The Z Garbage Collector | Java SE 21 GC Tuning Guide](https://docs.oracle.com/en/java/javase/21/gctuning/z-garbage-collector2.html)
|
||||
- [JEP 439: Generational ZGC](https://openjdk.org/jeps/439)
|
||||
- [JEP 490: ZGC: Remove the Non-Generational Mode](https://openjdk.org/jeps/490)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
* [三、设计原则](#三设计原则)
|
||||
* [S.O.L.I.D](#solid)
|
||||
* [其他常见原则](#其他常见原则)
|
||||
* [附录:实测](#附录实测)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -135,6 +136,25 @@ Wind is playing...
|
||||
Percussion is playing...
|
||||
```
|
||||
|
||||
编译器并不知道 instrument 实际指向哪个类,循环中的 instrument.play() 被编译成一条 invokevirtual 指令,参数是符号引用 Instrument.play:()V(javap 输出,见[附录 A1](#a1-方法调用的字节码))。运行时 JVM 才根据对象的实际类型选择方法:从对象所属的类开始,沿继承链向上找第一个签名相同的方法(JVM 规范 invokevirtual 一节)。这个过程称为动态分派。
|
||||
|
||||
每次调用都沿继承链查找太慢,HotSpot 为每个类维护一张虚方法表(vtable),按固定的槽位存放可被覆盖的方法的入口:子类的表先复制父类的表,覆盖了的方法替换对应槽位的入口,新增的方法追加在后面。play() 在 Instrument、Wind、Percussion 三张表中是同一个槽位,分别指向三个类自己的实现,调用时按槽位取出入口即可。通过接口调用的方法使用 invokeinterface,HotSpot 为它另外维护接口方法表(itable)。
|
||||
|
||||
重载则在编译时就确定了:编译器根据参数的静态类型选择重载的版本,写进字节码的符号引用中,称为静态分派。例如:
|
||||
|
||||
```java
|
||||
static void show(Instrument i) { System.out.println("show(Instrument)"); }
|
||||
static void show(Wind w) { System.out.println("show(Wind)"); }
|
||||
|
||||
Instrument i = new Wind();
|
||||
show(i); // show(Instrument)
|
||||
show((Wind) i); // show(Wind)
|
||||
```
|
||||
|
||||
i 实际指向 Wind 对象,但它的静态类型是 Instrument,第一次调用编译成 show:(LInstrument;)V,运行时不会再改变。
|
||||
|
||||
不能被覆盖的方法只有一个实现,调用目标在编译时就能确定:静态方法用 invokestatic,构造方法用 invokespecial;private 和 final 方法虽然也用 invokevirtual 调用(JDK 21 javac 的输出),JVM 同样能直接确定调用目标。
|
||||
|
||||
## 二、类图
|
||||
|
||||
每张类图后附有对应的 [PlantUML](https://www.planttext.com/) 代码,更多语法及使用请参考:http://plantuml.com/ 。
|
||||
@@ -362,9 +382,41 @@ Vehicle .. N
|
||||
|
||||
包之间的依赖关系都应该是稳定方向依赖的,包要依赖的包要比自己更具有稳定性。
|
||||
|
||||
## 附录:实测
|
||||
|
||||
### A1 方法调用的字节码
|
||||
|
||||
环境:JDK 21.0.13。在正文 Music 类的基础上加入两个重载的 show() 方法、一个 private 方法和一个 final 方法,运行后用 javap -c 查看 main() 中的调用指令(省略了 println 等无关的行):
|
||||
|
||||
```java
|
||||
Instrument i = new Wind();
|
||||
show(i);
|
||||
show((Wind) i);
|
||||
Music m = new Music();
|
||||
m.secret(); // private void secret()
|
||||
m.fixed(); // public final void fixed()
|
||||
```
|
||||
|
||||
```text
|
||||
Wind is playing...
|
||||
Percussion is playing...
|
||||
show(Instrument)
|
||||
show(Wind)
|
||||
|
||||
63: invokevirtual #54 // Method Instrument.play:()V
|
||||
73: invokespecial #28 // Method Wind."<init>":()V
|
||||
78: invokestatic #57 // Method show:(LInstrument;)V
|
||||
85: invokestatic #63 // Method show:(LWind;)V
|
||||
92: invokespecial #66 // Method "<init>":()V
|
||||
97: invokevirtual #67 // Method secret:()V
|
||||
101: invokevirtual #70 // Method fixed:()V
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- Java 编程思想
|
||||
- [The Java Virtual Machine Specification, Java SE 21 Edition:invokevirtual](https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-6.html#jvms-6.5.invokevirtual)
|
||||
- 周志明. 深入理解 Java 虚拟机:第 3 版 [M]. 机械工业出版社, 2019. 第 8 章“方法调用”。
|
||||
- 敏捷软件开发:原则、模式与实践
|
||||
- [面向对象设计的 SOLID 原则](http://www.cnblogs.com/shanyou/archive/2009/09/21/1570716.html)
|
||||
- [看懂 UML 类图和时序图](http://design-patterns.readthedocs.io/zh_CN/latest/read_uml.html#generalization)
|
||||
|
||||
Reference in New Issue
Block a user