一、 ThreadLocal
1.1 ThreadLocal是什么
- 本地线程变量,该变量相对于其他线程是封闭且隔离的。
- ThreadLocal 为变量的每个线程中创建一个副本,这样每个线程就可以访问自己内部的副本变量了
- 使用场景
- 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束
- 线程间数据隔离
- 进行事务操作,用于存储线程事务信息。
- 数据库连接,
Session
会话管理。
1.2 ThreadLocal如何使用
public class ThreadLocalTest02 {
public static void main(String[] args) {
ThreadLocal<String> local = new ThreadLocal<>();
IntStream.range(0, 10).forEach(i -> new Thread(() -> {
local.set(Thread.currentThread().getName() + ":" + i);
System.out.println("线程:" + Thread.currentThread().getName() + ",local:" + local.get());
}).start());
}
}
输出结果:
线程:Thread-0,local:Thread-0:0
线程:Thread-1,local:Thread-1:1
线程:Thread-2,local:Thread-2:2
线程:Thread-3,local:Thread-3:3
线程:Thread-4,local:Thread-4:4
线程:Thread-5,local:Thread-5:5
线程:Thread-6,local:Thread-6:6
线程:Thread-7,local:Thread-7:7
线程:Thread-8,local:Thread-8:8
线程:Thread-9,local:Thread-9:9
- 支付项目使用ThreadLocal
使用ThreadLoacal来保存登录用户的信息,以便每个登录用户可以随时随地获取自己的uid
public class UserUtils {
private static AuthConfigProperties authConfigProperties;
private static final ThreadLocal<UserBO> userBoThreadLocal = new NamedThreadLocal("CurrentUserBo");
public UserUtils() {
}
@Autowired
public void setAuthConfigProperties(AuthConfigProperties authConfigProperties) {
UserUtils.authConfigProperties = authConfigProperties;
}
public static UserBO getCurrentUser() {
return (UserBO)userBoThreadLocal.get();
}
public static <T> UserBO<T> getCurrentUserWithExtra() {
UserBO userBO = (UserBO)userBoThreadLocal.get();
Object userBOExtra = userBO.getExtraBO();
if (userBOExtra != null) {
Gson gson = new Gson();
T extra = gson.fromJson(gson.toJson(userBO.getExtraBO()), authConfigProperties.getExtraBOClass());
userBO.setExtraBO(extra);
return userBO;
} else {
return userBO;
}
}
//获取每个用户的uid方法
public static Long getCurrentUserId() {
return getCurrentUser().getId();
}
public static boolean hasUser() {
return userBoThreadLocal.get() != null;
}
//在代码中没有找到设置用户进入ThreadLocal的操作
public static void setUserBo(UserBO userBo) {
userBoThreadLocal.set(userBo);
}
public static void remove() {
userBoThreadLocal.remove();
}
}
1.3 源码分析
- set()方法
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
//首先获取当前线程对象
Thread t = Thread.currentThread();
//获取线程中变量 ThreadLocal.ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果不为空,
if (map != null)
map.set(this, value);
else
//如果为空,初始化该线程对象的map变量,其中key 为当前的threadlocal 变量
createMap(t, value);
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
//初始化线程内部变量 threadLocals ,key 为当前 threadlocal
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap是ThreadLocal的一个静态内部类,里面定义了Entry 来保存数据,Entry 是 继承弱引用的。
在Entry 内部使用ThreadLocal作为key值,使用我们设置的value作为value值。
每个Thread中,都有一个对应ThreadLocal.ThreadLocalMap变量,存取值时,也是从这个容器中获取
- get方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
get()方法中,Thread.currentThread()通过获取当前线程, 再取出线程中的ThreadLocalMap , 从而再将 当前的ThreadLocal传入 获取 对应的 值。
1.4 内存泄漏问题
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
如果Key threadlocal 为null ,这个entry 就可以清楚了
ThreadLocal 是一个弱引用 , 当 为null 时,就会被当成垃圾回收
重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap(thread 的内部属性)生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
-
解决办法:
使用完
ThreadLocal
后,执行remove
操作,避免出现内存溢出情况。
所以 如同 lock
的操作 最后要执行解锁操作一样,ThreadLocal
使用完毕一定记得执行remove 方法,清除当前线程的数值。
如果不remove
当前线程对应的VALUE
,就会一直存在这个值。
使用了线程池,可以达到“线程复用”的效果。但是归还线程之前记得清除ThreadLocalMap
,要不然再取出该线程的时候,ThreadLocal
变量还会存在。这就不仅仅是内存泄露的问题了,整个业务逻辑都可能会出错。
二、 如何辨别一个对象存亡
2.1 GC可达性分析
在主流的商用程序语言的主要实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的,这个算法的基本思路就是通过一系列的称为“GC Roots“的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路程成为引用链(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
在Java语言中,可作为 GC Roots 的对象包括下面几种:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象。(即Java方法中局部变量引用的对象)
- 方法区中类静态属性引用的对象。(即类的静态变量引用的对象)
- 方法区中常量引用的对象。(即类的常量引用的对象)
- 本地方法栈中 JNI 引用的对象。(即 Native 方法中局部变量引用的对象)
此算法的核心思想:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。
注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。
2.2 引用计数法(过时)
- 实现
如果有一个引用,被赋值为某一对象,那么将该对象的引用计数器+1。 如果一个指向某一对象的引用,被赋值为其他值,那么将该对象的引用计数器-1。 需要截获所有的引用更新操作,并且相应地增减目标对象的引用计数器。
- 缺点
一是需要额外的空间来存储计数器,以及繁琐的更新操作。
二是无法处理循环引用对象。
假设对象a与b相互引用,除此之外没有其他引用指向a或者b。 在这种情况下,a和b实际上已经死了,但由于它们的引用计数器皆不为0,在引用计数器的心中,这两个对象还活着。 因此,这些循环引用对象所占据的空间将不可回收,从而造成了内存泄漏。
三、 四种引用类型
3.1 强引用
java默认声明就是强引用
Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
obj = null; //手动置null
只要强引用存在,垃圾回收器永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError。
如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
3.2 软引用
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
MyObject aRef = new MyObject();
SoftReference aSoftRef=new SoftReference(aRef);
aRef = null;
垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软引用对象。
在回收之前
可以通过调用get()方法回去实例的强引用
MyObject anotherRef=(MyObject)aSoftRef.get();
但回收之后,get()方法只能获得到null了
3.3 弱引用
弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。下面是使用示例:
public class test {
public static void main(String[] args) {
WeakReference<People>reference=new WeakReference<People>(new People("zhouqian",20));
System.out.println(reference.get());
System.gc();//通知GVM回收资源
System.out.println(reference.get());
}
}
class People{
public String name;
public int age;
public People(String name,int age) {
this.name=name;
this.age=age;
}
@Override
public String toString() {
return "[name:"+name+",age:"+age+"]";
}
}
输出:
[name:zhouqian,age:20]
null
第二个输出结果是null,这说明只要JVM进行垃圾回收,被弱引用关联的对象必定会被回收掉。
3.4 虚引用
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class Main {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
}
}
四、垃圾收集算法
4.1 标记-清除算法
算法分为“标记” 和 “清除” 两个阶段: 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象
- 缺点
- 效率不高
- 标记清除之后会产生大量不连续的内存碎片
4.2 复制算法
为解决效率问题,出现了复制算法。
内容: 将内存按容量划分为相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象赋值到另一块上面,然后再把已经使用过的内存空间一次性清理掉。
缺点:太浪费内存
改进:因为由IBM公司研究表明,新生代中的对象98%是“朝生夕死”的,所以将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden 和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor。HotSpot 默认 Eden : Survivor = 8 : 1,这样的话之后10%的空间会被浪费。当Survivor空间不够用时,需要依赖老年代的内存区进行分配担保。
4.3 标记-整理算法
算法内容:先进行标记后,将所有存活的对象向一端移动,然后直接清理掉端边界以外的内存
4.4 分代收集算法
新生代用复制算法,老年代用标记-整理算法