WeakHashMap原理分析

WeakHashMap总体介绍

​ WeakHashMap继承自AbstractMap,实现了Map接口,拥有了Map的基础功能。但是与常用的Map(比如HashMap)不同的是,WeakHashMap底层的存储单元Entry采用WeakReference,在GC的时候,会回收K,V。所以WeakHashMap天然比较适合用作缓存,即使K,V丢失,也不会对业务造成影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
V value;
final int hash;
Entry<K,V> next;

/**
* Creates new entry.
*/
Entry(Object key, V value,
ReferenceQueue<Object> queue,
int hash, Entry<K,V> next) {
super(key, queue);
this.value = value;
this.hash = hash;
this.next = next;
}

...
}

​ 如上代码可以看到Entry继承了WeakReference,在构造函数中super(key, queue)传入了key和ReferenceQueue。

​ 在调用get(),put(),size()等方法的时候,会执行一个私有方法expungeStaleEntries,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void expungeStaleEntries() {
//迭代弱引用队列中要被回收的对象
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
//操作链表
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
//敲黑板,这行代码很重要,如果不执行e.value = null,map的value会泄漏。
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}

#图解WeakHashMap原理

img

​ 图1

​ 如图1所示,key1,key2,key3虚引用指向Entry1,Entry2,Entry3,每个entry又分别指向v1,v2,v3。GC之后,key2虚引用被垃圾回收。通过执行expungeStaleEntries方法,使得Entry2变成游离态,Entry2也和v2脱离了引用关系。所以Entry2,v2都变成了可被垃圾回收的状态。

img

​ 图2

java引用详解

一般情况下,java开发中用的都是强引用。例如如下代码所示。

1
Object ref = new Object();

实际上在java中,还有一种不太常用的“弱引用”类型。弱引用中分为了软引用(SoftReference),弱引用(WeakReference),虚引用(PhantomReference)。所谓的强引用,其实就是FinalReference。

问题随之而来,为什么引用要分为强弱?弱引用又适用于何种场景?本文结合源码,带大家了解java引用的世界。

引用强弱之分

如果大家对JVM内存回收(GC)有一定了解,一定知道内存回收的前提是对象引用“不可达‘’,才能在回收阶段真正的释放掉对象占用的内存。强引用下,如果对象的引用一直处于可达状态,这块内存是没有办法回收的。在内存资源充沛的情况下还好,但是如果内存资源吃紧,为了服务的可用性,一些“不必要”,或者说“不那么重要的”内存,是不是可以释放掉?这个时候,弱引用就派上了用场。各个引用类的对比,请参考下标

引用类型 回收方式 必须配合ReferenceQueue 说明
FinalReference 不回收 默认情况下使用强引用
SoftReference 内存不充足情况下,GC之后回收,如果使用了queue同WeakReference一样的处理 非必要对象,为了内存空间
WeakReference GC立刻回收,如果使用了queue,需手动把要回收的对象置为null 非必要对象,为了内存空间
PhantomReference(幽灵引用) GC立刻回收 get默认返回null,使用虚引用的目的是通过queue监听对象回收

弱引用典型应用

WeakHashMap,ThreadLocal,JVM缓存实现。

源码分析

下图是java.lang.ref包下的类图,四种引用全部继承自Reference。

img

JDK把引用分为了四个状态。

状态 判定条件 说明 源码注释
Active (queue==ReferenceQueue\ \ ReferenceQueue.NULL) && next == null 新创建的引用对象是这个状态。在GC检测到引用对象已经到达合适的Reachability(可达性)时,GC会根据引用对象在创建时是否指定ReferenceQueue参数进行状态转移,如果指定则转移到pending,否则直接转移到Inactive。 Active: Subject to special treatment by the garbage collector. Some time after the collector detects that the reachability of the referent has changed to the appropriate state, it changes the instance’s state to either Pending or Inactive, depending upon whether or not the instance was registered with a queue when it was created. In the former case it also adds the instance to the pending-Reference list. Newly-created instances are Active.
Pending queue == RefrenceQueue && next == this (jvm设置) pending-Refence链表中引用都是这个状态,它们等着被内部线程ReferenceHandler处理入队(调用RefenceQueue.enqueue方法),没有注册的实例不会进入此状态。 An element of the pending-Reference list, waiting to be enqueued by the Reference-handler thread. Unregistered instances are never in this state.
Enqueued queue == ReferenceQueue.ENQUEUED && next == 下一个要处理的Reference对象,或者链表中最后一个next == this 相应的对象已经为待回收并放到queue中,准备由外部线程来询问queue获取相应的数据。调用ReferenceQueue.enqued方法后的Reference对象处于这个状态,当Reference实例从队列中移除之后,它的状态改为Inactive,没有注册的实例不会进入该状态。 Enqueued: An element of the queue with which the instance was registered when it was created. When an instance is removed from its ReferenceQueue, it is made Inactive. Unregistered instances are never in this state.
Inactive queue == ReferenceQueue.NULL && next == this 即此Reference对象已由外部queue中获取到,并且已经处理掉了。即意味着此对象是可以被回收的,并且对内部封装的对象也可以被回收掉了(具体的回收运行,取决于Clear动作是否被调用,可以理解为进入此状态的Reference对象是应该被回收掉的)一旦Reference对象变成Inactive,它的状态就不会再变化。 Nothing more to do. Once an instance becomes Inactive its state will never change again.

基于上表的讲解,画出了如下引用状态流程图。

img

java类加载之ClassLoader

何为类加载

大家知道Java是一门编译型语言,源代码文件是以.java结尾的。通过编译器编译之后,会形成一个字节码文件,也就是class文件。在一个应用中,不同的class文件中封装着不同的功能,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

类加载器

BootstrapClassLoader:称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:

1
2
3
4
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}

如下是BootstrapClassLoader加载的类:

file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home/jre/classes

ExtClassLoader

称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。

AppClassLoader

称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。可以说应用程序自己定义的类,都是由AppClassLoader负责加载。

CustomClassLoader

除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。

ClassLoader加载类原理

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

##为何需要双亲委派

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

JDK如何判定Class相同

JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。

如何定义CustomClassLoader

如下代码,展示了本地目录的类加载器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class FileSystemClassLoader extends ClassLoader {

private String rootDir;

public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}

private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

private String classNameToPath(String className) {
return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
}
}