mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-08-01 16:28:03 +08:00
[docs update]jvm 类初始化内容完善
This commit is contained in:
parent
bb824b6d9c
commit
db40b886a5
@ -108,44 +108,54 @@ ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
|
||||
|
||||
`ThreadLocal` 内存泄漏的根本原因在于其内部实现机制。
|
||||
|
||||
源码版本:opebjdk8
|
||||
内部主要围绕着threadlocalMap、threadlocal两个变量进行记录,同时需要理解的是该内存泄漏问题已经被解决,并不需要手动操作。
|
||||
通过上面的内容我们已经知道:每个线程维护一个名为 `ThreadLocalMap` 的 map。 当你使用 `ThreadLocal` 存储值时,实际上是将值存储在当前线程的 `ThreadLocalMap` 中,其中 `ThreadLocal` 实例本身作为 key,而你要存储的值作为 value。
|
||||
|
||||
threadlocalMap是线程级别变量,存储于线程变量Thread的成员变量引用。
|
||||
threadlocal是作为threadlocalMap的key,是我们手动为类添加的成员变量。
|
||||
|
||||
以threadlocal的set源码为例子:
|
||||
`ThreadLocal` 的 `set()` 方法源码如下:
|
||||
|
||||
```java
|
||||
public void set(T value) {
|
||||
Thread t = Thread.currentThread();
|
||||
ThreadLocalMap map = getMap(t);
|
||||
Thread t = Thread.currentThread(); // 获取当前线程
|
||||
ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
|
||||
if (map != null) {
|
||||
map.set(this, value);
|
||||
map.set(this, value); // 设置值
|
||||
} else {
|
||||
createMap(t, value);
|
||||
createMap(t, value); // 创建新的 ThreadLocalMap
|
||||
}
|
||||
}
|
||||
在map.set和createMap中,并没有真正存储key,而是通过threadlocal的hash值进行离散计算得到坐标,最终存储于类型为static class Entry extends WeakReference<ThreadLocal< ? > >的数组中。
|
||||
`int i = key.threadLocalHashCode & (len-1);`
|
||||
```
|
||||
|
||||
key不同于Map<K,V>接口的一般实现,使用node<k,V>将key和value一起存储,也就是说key只是作为钥匙,但并不存储于map当中,如果threadlocal变量的外部引用被抹除是可以正常被回收的。
|
||||
`ThreadLocalMap` 的 `set()` 和 `createMap()` 方法中,并没有直接存储 `ThreadLocal` 对象本身,而是使用 `ThreadLocal` 的哈希值计算数组索引,最终存储于类型为`static class Entry extends WeakReference<ThreadLocal<?>>`的数组中。
|
||||
|
||||
```java
|
||||
int i = key.threadLocalHashCode & (len-1);
|
||||
```
|
||||
|
||||
这也是Entry为何是弱引用的原因,因为可能会出现《null,v》的情况。这里虽然解决了内存泄漏的问题,但是似乎可能会导致Entry提前被回收?
|
||||
`ThreadLocalMap` 的 `Entry` 定义如下:
|
||||
|
||||
查询得知可达性分析会有一条链路从threadlocal.get()方法所获得的变量,也就是间接可达,换句话说会考虑方法上的引用链条,而并非单纯是变量的直接引用。
|
||||
```java
|
||||
static class Entry extends WeakReference<ThreadLocal<?>> {
|
||||
Object value;
|
||||
|
||||
那么此时k存在v就一定存在,但是问题并没有结束,因为会出现k不存在,但是v存在的情况!
|
||||
这是由于k实际上是类、方法的变量ThreadLocal,如果该变量被回收也就意味着内部的V永远不会被找到,成为了内存泄漏的原因。
|
||||
|
||||
|
||||
**重点在于key并非是弱引用,只是v作为map的直接引用无法被回收,因此使其成为了弱引用类型,并在set、get方法中做了一系列安全措施。**
|
||||
Entry(ThreadLocal<?> k, Object v) {
|
||||
super(k);
|
||||
value = v;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ThreadLocalMap` 的 `key` 和 `value` 引用机制:
|
||||
|
||||
- **key 是弱引用**:`ThreadLocalMap` 中的 key 是 `ThreadLocal` 的弱引用 (`WeakReference<ThreadLocal<?>>`)。 这意味着,如果 `ThreadLocal` 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 `ThreadLocalMap` 中对应的 key 变为 `null`。
|
||||
- **value 是强引用**:即使 `key` 被 GC 回收,`value` 仍然被 `ThreadLocalMap.Entry` 强引用存在,无法被 GC 回收。
|
||||
|
||||
当 `ThreadLocal` 实例失去强引用后,其对应的 value 仍然存在于 `ThreadLocalMap` 中,因为 `Entry` 对象强引用了它。如果线程持续存活(例如线程池中的线程),`ThreadLocalMap` 也会一直存在,导致 key 为 `null` 的 entry 无法被垃圾回收,机会造成内存泄漏。
|
||||
|
||||
也就是说,内存泄漏的发生需要同时满足两个条件:
|
||||
|
||||
1. `ThreadLocal` 实例不再被强引用;
|
||||
2. 线程持续存活,导致 `ThreadLocalMap` 长期存在。
|
||||
|
||||
虽然 `ThreadLocalMap` 在 `get()`, `set()` 和 `remove()` 操作时会尝试清理 key 为 null 的 entry,但这种清理机制是被动的,并不完全可靠。
|
||||
|
||||
**如何避免内存泄漏的发生?**
|
||||
|
||||
|
@ -109,16 +109,14 @@ tag:
|
||||
|
||||
对于初始化阶段,虚拟机严格规范了有且只有 6 种情况下,必须对类进行初始化(只有主动去使用类才会初始化类):
|
||||
|
||||
1. 当遇到 `new`、 `getstatic`、`putstatic` 或 `invokestatic` 这 4 条字节码指令时,比如 `new` 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
|
||||
- 当 jvm 执行 `new` 指令时会初始化类。即当程序创建一个类的实例对象。
|
||||
- 当 jvm 执行 `getstatic` 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
|
||||
- 当 jvm 执行 `putstatic` 指令时会初始化类。即程序给类的静态变量赋值。
|
||||
- 当 jvm 执行 `invokestatic` 指令时会初始化类。即程序调用类的静态方法。
|
||||
1. 遇到 `new`、`getstatic`、`putstatic` 或 `invokestatic` 这 4 条字节码指令时:
|
||||
- `new`: 创建一个类的实例对象。
|
||||
- `getstatic`、`putstatic`: 读取或设置一个类型的静态字段(被 `final` 修饰、已在编译期把结果放入常量池的静态字段除外)。
|
||||
- `invokestatic`: 调用类的静态方法。
|
||||
2. 使用 `java.lang.reflect` 包的方法对类进行反射调用时如 `Class.forName("...")`, `newInstance()` 等等。如果类没初始化,需要触发其初始化。
|
||||
3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
|
||||
4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 `main` 方法的那个类),虚拟机会先初始化这个类。
|
||||
5. `MethodHandle` 和 `VarHandle` 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,
|
||||
就必须先使用 `findStaticVarHandle` 来初始化要调用的类。
|
||||
5. `MethodHandle` 和 `VarHandle` 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用,就必须先使用 `findStaticVarHandle` 来初始化要调用的类。
|
||||
6. **「补充,来自[issue745](https://github.com/Snailclimb/JavaGuide/issues/745 "issue745")」** 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
|
||||
|
||||
## 类卸载
|
||||
|
Loading…
x
Reference in New Issue
Block a user