这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战
引用
引用部分的总结参考 juejin.cn/post/684490…
强引用
默认的引用,jvm宁可oom也不会被垃圾回收器回收
不再使用时,手动置为null弱化引用
Object strongObject = new Object();
object = null
复制代码
方法中的强引用,引用在栈中,实际变量在堆中
ArrayList
中的 clear
方法,是对其中的数组逐个设置为 null
,而不是直接把整个数组置为 null
public void clear() {
modCount++;
final Object[] es = elementData;
for (int to = size, i = size = 0; i < to; i++)
es[i] = null;
}
复制代码
软引用
一般用于内存缓存,特点是平常可以有,内存不足时可以被回收
内存充足不回收,内存不足则可能被回收
可以与一个引用队列关联,当所引用的对象被回收时,会将软引用放进队列中
ReferenceQueue<String> queue = new ReferenceQueue<>();
String abc = "123";
SoftReference<String> soft = new SoftReferenct<>(abc, queue);
复制代码
队列用于在oom时,jvm根据队列回收长时间不使用的对象。回收时会将所引用的对象置为 null
,再通知垃圾收集器收集
注意:调用 System.gc()
不一定会触发垃圾回收,因为这个只是通知,具体啥时候执行gc由具体的垃圾收集器自己决定
ReferenceQueue
存在的意义在于,进行回收时优先回收长时间不使用的
弱引用
被gc扫描到了就回收,但是gc是一个优先级很低的线程,不一定很快就发现
String str = "123";
WeakReference<String> weakReference = new WeakReferenct<>(str);
复制代码
其他行为与软引用类似
用途
一个对象偶尔使用,希望在使用时随时就能获取到,但又不影响此对象的垃圾回收,那么就应该使用Weak Referenct来记住此对象
虚引用
虚引用拿不到具体数据,其 get
方法直接返回 null
public T get() {
return null;
}
复制代码
主要被用来跟踪对象被垃圾收集器回收的情况
NIO中使用虚引用管理堆外内存
String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);
复制代码
ThreadLocal
ThreadLocal基础
基本信息
用于为每个线程保存一份独有的同类型数据,其实现在于其内部独有的一个 ThreadLocalMap
,并在每一个 Thread
中存有 ThreadLocalMap
的变量
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
复制代码
因为这里的map每一个都是线程内部独有的,因此不存在线程安全问题
常用方法
ThreadLocal
中的主要方法如下
public T get();
boolean isPresent();
public void set(T value);
public void remove();
private T setInitialValue();
复制代码
同时包含一个静态工厂方法
提供一个初始值的
supplier
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier);
复制代码
有意思的hashCode
ThreadLocal
在 ThreadLocalMap
作为key使用,具体计算时使用其一个hashCode属性
- key设计成弱引用,随时可能被gc,因此会留下更多空槽
- hash碰撞成,通过线性探测的开放地址法解决冲突
- 通过魔数减少hash冲突
// hashCode相关属性和方法
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode =
new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 一个计算hashCode的魔数
private static final int HASH_INCREMENT = 0x61c88647;
复制代码
这里有意思的就是 0x61c88647
搜了多篇文章,基本都是说该数得到的hash值较为分散,在于黄金分割比例,但是基本都是一笔带过,没有细说
这里贴一篇推荐的文章 ThreadLocal的hash算法(关于 0x61c88647) - 掘金 (juejin.cn)
ThreadLocalMap
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
复制代码
- 初始大小为16
- 负载因子为2/3
- 各种操作的时候,需要考虑
Entry
的key
为null
的情况(即弱引用被回收)
Thread.exit()
正如其注释如言
该方法由系统调用,给了线程在真正退出之前进行清理操作的机会
其中 threadLocals = null;
相当于对于 ThreadLocalMap
进行了垃圾回收,因此其生命周期与 Thread
的生命周期一致
/**
* This method is called by the system to give a Thread
* a chance to clean up before it actually exits.
*/
private void exit() {
if (threadLocals != null && TerminatingThreadLocal.REGISTRY.isPresent()) {
TerminatingThreadLocal.threadTerminated();
}
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
// 这里设置
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
复制代码
但是如果一直没有退出呢?则可能导致内存泄露!
内存泄露问题
问题核心原因
ThreadLocalMap
的key
可能被回收,但是value
和entry
本身只有在set
get
remove
操作时发现key
为空才进行删除回收,即value
采取的是懒删除策略
问题触发
线程被放回线程池不再使用,或者再次使用时不再调用
set
get
remove
方法
其中的删除单个格子的方法
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 删除value以及entry本身
tab[staleSlot].value = null;
tab[staleSlot] = null;
// 缩小size
size--;
// 对后续的数据重新进行hash,直到碰到null
// 因为删除一个之后会导致后面的hash改变(后面的hash可能是冲突后线性探测得到的)
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
// ......
}
}
return i;
}
复制代码
如何解决泄露问题
参考 Netty
实现,对 Runnable
进行封装,执行完即进行清理动作,使用Runnable的地方都以此来代替
final class FastThreadLocalRunnable implements Runnable {
private final Runnable runnable;
@Override
public void run() {
try {
runnable.run();
} finally {
// 这里进行清理
FastThreadLocal.removeAll();
}
}
}
复制代码
普通的业务最佳实践
try {
// 其它业务逻辑
} finally {
threadLocal.remove();
}
复制代码
Scope
github地址: github.com/PhantomThie…
是对ThreadLocal的一种高级封装
其特性如下:
- 显示的声明Scope的范围
- 强类型
- 可以在线程池中安全的使用,并防止泄露
- 只支持jdk1.8
注意:其本身并没有解决
ThreadLocal
的内存泄露问题
核心定义
ScopeKey
public final class ScopeKey<T> {
// 默认数值
private final T defaultValue;
// 数值初始化Supplier
private final Supplier<T> initializer;
// 是否开启null保护(为null时是否可以返回)
private final boolean enableNullProtection;
}
复制代码
Scope
public final class Scope {
// ThreadLocal的Value为Scope
private static final SubstituteThreadLocal<Scope> SCOPE_THREAD_LOCAL = MyThreadLocalFactory.create();
// 两个线程安全的HashMap,一个保存具体的数值,另一个保存是否null保护
private final ConcurrentMap<ScopeKey<?>, Object> values = new ConcurrentHashMap<>();
private final ConcurrentMap<ScopeKey<?>, Boolean> enableNullProtections = new ConcurrentHashMap<>();
}
复制代码
确保Scope范围的方法
本质上就是在新的Scope上下文环境中执行runnable或者supplier方法
执行前保存旧的scope,执行结束重设scope
public static <X extends Throwable> void runWithExistScope(@Nullable Scope scope,
ThrowableRunnable<X> runnable) throws X {
supplyWithExistScope(scope, () -> {
runnable.run();
return null;
});
}
public static <T, X extends Throwable> T supplyWithExistScope(
@Nullable Scope scope, ThrowableSupplier<T, X> supplier
) throws X {
Scope oldScope = SCOPE_THREAD_LOCAL.get();
SCOPE_THREAD_LOCAL.set(scope);
try {
return supplier.get();
} finally {
if (oldScope != null) {
SCOPE_THREAD_LOCAL.set(oldScope);
} else {
SCOPE_THREAD_LOCAL.remove();
}
}
}
复制代码
另一组控制范围方法
public static <X extends Throwable> void runWithNewScope(@Nonnull ThrowableRunnable<X> runnable)
throws X {
supplyWithNewScope(() -> {
runnable.run();
return null;
});
}
public static <T, X extends Throwable> T
supplyWithNewScope(@Nonnull ThrowableSupplier<T, X> supplier) throws X {
beginScope();
try {
return supplier.get();
} finally {
endScope();
}
}
// 依赖于以下两个方法
public static Scope beginScope() {
Scope scope = SCOPE_THREAD_LOCAL.get();
if (scope != null) {
throw new IllegalStateException("start a scope in an exist scope.");
}
scope = new Scope();
SCOPE_THREAD_LOCAL.set(scope);
return scope;
}
// 这里要注意,该方法没有重设旧的scope,因此应该用于没有旧scope的场景
public static void endScope() {
SCOPE_THREAD_LOCAL.remove();
}
复制代码
使用方法
private static final ScopeKey<T> scope = allocate();
复制代码
其内部的 get
set
方法均会去尝试从 ThreadLocal
中获取 Scope
,再将 ScopeKey
本身作为key从 Scope
的 Map
取得/设置 值