主要内容:
1、引用类型简述
2、对象的可达性
3、软引用的垃圾回收分析
4、WeakHashMap分析
5、ThreadLocal内存泄漏分析
1、引用类型简述
在Java语言中除了基本数据类型外,其他的都是指向各类对象的对象引用;Java中根据其生命周期的长短,将引用分为5类。
1) 强引用
特点:我们平常典型编码Object obj = new Object()中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。 当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,具体回收时机还是要看垃圾收集策略。
2) 软引用
特点:软引用通过SoftReference类实现。 软引用的生命周期比强引用短一些。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。
应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
3) 弱引用
弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
应用场景:弱应用同样可用于内存敏感的缓存。
4) 虚引用
特点:虚引用也叫幻象引用,通过PhantomReference类来实现。无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。因为PhantomReference的get()一直返回null, 如果没配合ReferenceQueue队列使用的话,将永远无法访达, 因此虚引用必须和引用队列 (ReferenceQueue)联合使用。
ReferenceQueue queue = new ReferenceQueue();
PhantomReference pr = new PhantomReference (object, queue);
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。
应用场景:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。
5) FinalReference
特点:与FinalReference类紧密相关的是java.lang.ref.Fianlizer类。查看JVM线程会看到一个Fianlizer线程, 主要作用是进行的资源关闭最后环节(finalize机制), 然而也是内存泄漏的重灾区。Fianlizer继承自FinalReference类,访问权限是package级别,因此应用程序不可进行拓展,全权受JVM实现控制。该类实现的目是垃圾回收之前进行资源回收。以下为使用jmap打印的JVM线程快照:
"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00002b88f00e7000 nid=0x6a38 in Object.wait() [0x00002b8920080000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
- locked <0x000000072a662190> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
Locked ownable synchronizers:
- None
2、对象的可达性
- 强可达(Strongly Reachable): An object is strongly reachable if it can be reached by some thread without traversing any reference objects. A newly-created object is strongly reachable by the thread that created it.
- 软可达状态(Softly Reachable) : An object is softly reachable if it is not strongly reachable but can be reached by traversing a soft reference.
- 弱可达状态(Weakly Reachable) : An object is weakly reachable if it is neither strongly nor softly reachable but can be reached by traversing a weak reference. When the weak references to a weakly-reachable object are cleared, the object becomes eligible for finalization.
- 幻象可达状态(Phantom Reachable):An object is phantom reachable if it is neither strongly, softly, nor weakly reachable, it has been finalized, and some phantom reference refers to it.
- 不可达状态(Unreachable): an object is unreachable, and therefore eligible for reclamation, when it is not reachable in any of the above ways.
除了虚引用(PhantomReference)的get()一直返回null意外,其他几个引用都会返回被引用的实际对象,转换为强可达状态。因此对于弱引用、软引用回收,垃圾回收机制会进行二次确认, 是否已经变为强引用。
负面影响如果转换强可达状态不当,例如赋值给static状态的变量,此时无法变回弱引用状态,即产生内存泄漏。
3、软引用的垃圾回收分析
SoftReference通常使用场景实现内存敏感缓存框架,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。JVM回收机制SoftReference:
- 在OutOfMemoryError之前,Java 虚拟机一定会回收 SoftReference 对象;
- Java不保证SoftReference 对象何时被清除,相关的机制是JVM实现相关的(有关 SoftReference 的一些事实);
- Java提供SoftReference的期望是更好的实现缓存。
正由于SoftReference回收时机不定, 引发一系列问题:
- 如果你的进程所占的内存不是满到要抛 OutOfMemoryError 的程度,JVM 根本不清理 SoftReference 占用的内存。
- 软引用对象占用了一大堆内存,更糟糕的是它们都会进入 Old-Gen。这样你的进程会频繁触发 Full GC,但即使这样,JVM 也不一定会清理 SoftReference 占用的内存。
- 因为 Old-Gen 现在是满负荷工作,你会发现一次 FullGC 的时间变得异常的长。
回收SoftReference的工作是全权由JVM进行控制, 我们是无法进行干预. 但是可以利用小手段进行, 暴力回收(How to make the java system release Soft References?):
-Xms200m -Xmx200m
1 public class SoftReferenceGc {
2 public static void main(String[] args) {
3 final SoftReference<Object> reference = new SoftReference<Object>(new Object());
4
5 // Sanity check
6 if(reference.get() == null){
7 throw new IllegalArgumentException("not empty") ;
8 }
9
10 // Force an OOM
11 try {
12 Object[] ignored = new Object[(int) Runtime.getRuntime().maxMemory()];
13 } catch( OutOfMemoryError e ) {
14 System.out.println("OutOfMemoryError");
15 // great!
16 }
17
18 // Verify object has been garbage collected
19 if(reference.get() != null){
20 throw new IllegalArgumentException(" not gc") ;
21 }else {
22 System.out.println("SoftReference have been gc");
23 }
24 }
25 }
输出结果:
OutOfMemoryError
SoftReference have been gc
此时验证了上述, 在OutOfMemoryError之前,Java 虚拟机一定会回收 SoftReference 对象
4、WeakHashMap分析
1) WeakHashMap声明如下, 实质上逻辑与HashMap一致, 都是使用[数组+链表]的形式存储数据 以及rehash过程.
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>
2) 与HashMap不同在于 Entry类继承自WeakReference<Object> 即key为弱引用类型。 因此着即使Entry持有key的引用, 也不能保证它不被回收, 除非key被其他的强类型引用。
1 /**
2 * The entries in this hash table extend WeakReference, using its main ref
3 * field as the key.
4 */
5 private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
6 V value;
7 final int hash;
8 Entry<K,V> next;
9
10 /**
11 * Creates new entry.
12 */
13 Entry(Object key, V value,
14 ReferenceQueue<Object> queue,
15 int hash, Entry<K,V> next) {
16 super(key, queue);
17 this.value = value;
18 this.hash = hash;
19 this.next = next;
20 }
21
22 @SuppressWarnings("unchecked")
23 public K getKey() {
24 return (K) WeakHashMap.unmaskNull(get());
25 }
26
27 public V getValue() {
28 return value;
29 }
30
31 public V setValue(V newValue) {
32 V oldValue = value;
33 value = newValue;
34 return oldValue;
35 }
36
37 public boolean equals(Object o) {
38 if (!(o instanceof Map.Entry))
39 return false;
40 Map.Entry<?,?> e = (Map.Entry<?,?>)o;
41 K k1 = getKey();
42 Object k2 = e.getKey();
43 if (k1 == k2 || (k1 != null && k1.equals(k2))) {
44 V v1 = getValue();
45 Object v2 = e.getValue();
46 if (v1 == v2 || (v1 != null && v1.equals(v2)))
47 return true;
48 }
49 return false;
50 }
51
52 public int hashCode() {
53 K k = getKey();
54 V v = getValue();
55 return Objects.hashCode(k) ^ Objects.hashCode(v);
56 }
57
58 public String toString() {
59 return getKey() + "=" + getValue();
60 }
61 }
3) Entry持有的key会被先回收, 而意味无法查找对应value并Entry持有该value不被回收 最终造成内存泄漏。核心函数expungeStaleEntries就是清除已经被回收的key对应的value 防止内存泄漏
1 /**
2 * Expunges stale entries from the table.
3 */
4 private void expungeStaleEntries() {
5 // 被回收的key进入ReferenceQueue 循还非阻塞式清除
6 for (Object x; (x = queue.poll()) != null; ) {
7 synchronized (queue) {
8 @SuppressWarnings("unchecked")
9 Entry<K,V> e = (Entry<K,V>) x;
10 int i = indexFor(e.hash, table.length);
11
12 Entry<K,V> prev = table[i];
13 Entry<K,V> p = prev;
14 // 解决hash冲突的链表结构, 清除对应value Help GC
15 while (p != null) {
16 Entry<K,V> next = p.next;
17 if (p == e) {
18 if (prev == e)
19 table[i] = next;
20 else
21 prev.next = next;
22 // Must not null out e.next;
23 // stale entries may be in use by a HashIterator
24 e.value = null; // Help GC
25 size--;
26 break;
27 }
28 prev = p;
29 p = next;
30 }
31 }
32 }
33 }
4) 从expungeStaleEntries调用链图中看到增、删、改、查 都会调用该函数, 进行null key对应到value处理
5) WeekHashMap特点特别适用于需要缓存的场景。但使用的时候也需要谨慎, 如果缓存的对应过多而GC未能及时回收, 极易造成OOM等问题.
5、ThreadLocal内存泄漏分析
1) ThreadLocal即为线程本地变量,利用线程绑定实现变量隔离解决并发共享的问题. 简单示例:
public class ThreadLocalAppli {
ThreadLocal<Integer> threadLocal = new ThreadLocal<>() ;
public static void main(String[] args) throws InterruptedException {
ThreadLocalAppli app = new ThreadLocalAppli() ;
for (int i = 0; i < 4; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 4; j++) {
app.threadLocal.set(j);
System.out.println(Thread.currentThread().getName()+" :" +app.threadLocal.get());
}
}
}, "Thread-" + i).start();
}
Thread.sleep(1000L);
}
}
执行结果: 可以看到线程之前变量互不影响, 实现了正确的计算.
Thread-0 :0
Thread-0 :1
Thread-0 :2
Thread-0 :3
Thread-1 :0
Thread-1 :1
Thread-1 :2
Thread-1 :3
Thread-2 :0
Thread-2 :1
Thread-2 :2
Thread-2 :3
Thread-3 :0
Thread-3 :1
Thread-3 :2
Thread-3 :3
2) ThreadLocal源码分析
ThreadLocal原理是利用内部类ThreadLocalMap实现, Thread会持有ThreadLocal.ThreadLocalMap对象, ThreadLocal并不实际存数据, 而是检索value的key 对应的value即为时机存储的值. 实质上ThreadLocal.ThreadLocalMap 和 WeakHashMap都是散列表 在解决哈希冲突方式上, 前者使用开放寻址而后者选用拉链式.
ThreadLocal.ThreadLocalMap.Entry的结构如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry结构与WeakHashMap的Entry类似, key都为弱引用类.
针对ThreadLocal.ThreadLocalMap核心源码分析:
- getEntry()
1 // 使用开放寻址方式查询: 如果hash值恰好为对应的值, 直接返回. 否则由该位置继续往后查找.
2 private Entry getEntry(ThreadLocal<?> key) {
3 int i = key.threadLocalHashCode & (table.length - 1);
4 Entry e = table[i];
5 if (e != null && e.get() == key)
6 return e;
7 else
8 return getEntryAfterMiss(key, i, e);
9 }
10
11 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
12 Entry[] tab = table;
13 int len = tab.length;
14
15 // 循环查询, 并清除过时的Entry
16 while (e != null) {
17 ThreadLocal<?> k = e.get();
18 if (k == key)
19 return e;
20 // 说明key已经被回收, 直接清除该槽位
21 if (k == null)
22 expungeStaleEntry(i);
23 else
24 i = nextIndex(i, len);
25 e = tab[i];
26 }
27 return null;
28 }
- set()
1 private void set(ThreadLocal<?> key, Object value) {
2 Entry[] tab = table;
3 int len = tab.length;
4 int i = key.threadLocalHashCode & (len-1);
5
6 // 使用开放寻址方式解决散列表冲突
7 for (Entry e = tab[i];
8 e != null;
9 e = tab[i = nextIndex(i, len)]) {
10 ThreadLocal<?> k = e.get();
11
12 // 说明key已经存在, 直接覆盖原来的值
13 if (k == key) {
14 e.value = value;
15 return;
16 }
17
18 // 说明key已经被回收,直接占用改槽位
19 if (k == null) {
20 replaceStaleEntry(key, value, i);
21 return;
22 }
23 }
24
25 // 当key不存在 找到空的槽位插入
26 tab[i] = new Entry(key, value);
27 int sz = ++size;
28 // 如果未对key已被回收的Entry清除 且 数量已经超过阀值则进行rehash
29 if (!cleanSomeSlots(i, sz) && sz >= threshold)
30 rehash();
31 }
- remove()
1 // 使用开放寻址方式查找, 并清除符合的槽位
2 private void remove(ThreadLocal<?> key) {
3 Entry[] tab = table;
4 int len = tab.length;
5 int i = key.threadLocalHashCode & (len-1);
6 for (Entry e = tab[i];
7 e != null;
8 e = tab[i = nextIndex(i, len)]) {
9 if (e.get() == key) {
10 e.clear();
11 expungeStaleEntry(i);
12 return;
13 }
14 }
15 }
与WeakHashMap类似在进行增、删、改、查的时候都会进行过时key的清理, 以防value内存泄漏.
3) ThreadLocal内存泄漏根源
如下图为引用关系,虚线为ThreadLocal的弱引用。 ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如遇到线程池的情况),这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
那么既然知道是ThreadLocal的弱引用造成内存泄漏, 为什么不使用强应用类型呢?
- 假设key 使用强引用:如果没有手动删除,实际已经不在使用该ThreadLocal, 在每次GC时候, 由于ThreadLocalMap一直持有ThreadLocal的强引用,ThreadLocal不会被回收,导致Entry内存泄漏。
- 假设key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove等操作的时候会被清除。
由此可见key使用弱引用影响的范围更小, 同时在设计之初时机已经考虑到内存泄漏的问题, 故而增、删、改、查操作的会对过时Entry进行处理。导致内存泄漏的原因在于使用不当, 因此强调每次使用ThreadLocal之后, 必须手动remove() 。