Android 图片预览库封装

基于 PhotoView,增加手势旋转、拖动退出预览功能。

GitHub:PhotoViewEx

依赖

在项目 build.gradle 中添加依赖

1
2
3
4
5
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}

module 的 build.gradle 中添加依赖

1
2
3
dependencies {
implementation 'com.github.zywudev:PhotoViewEx:1.0.0'
}

使用方法

使用下面的方式即可实现缩放、双指旋转功能。

1
2
3
4
<com.wuzy.photoviewex.PhotoView
android:id="@+id/iv_photo"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
1
2
PhotoView photoView = (PhotoView) findViewById(R.id.iv_photo);
photoView.setImageResource(R.drawable.image);

拖动关闭使用方法

1、Activity 主题设为透明

1
<item name="android:windowIsTranslucent">true</item>

2、初始化

1
DragCloseHelper mDragCloseHelper = new DragCloseHelper(this);

3、设置需要拖拽的 View 以及背景 ViewGroup

1
mDragCloseHelper.setDragCloseViews(mConstraintLayout,mPhotoView);

4、设置监听

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
mDragCloseHelper.setOnDragCloseListener(new OnDragCloseListener() {
@Override
public void onDragBegin() {

}

@Override
public void onDragging(float percent) {

}

@Override
public void onDragEnd(boolean isShareElementMode) {
if (isShareElementMode) {
onBackPressed();
}
}

@Override
public void onDragCancel() {

}

@Override
public boolean intercept() {
// 默认false
return false;
}
});

5、处理 Touch 事件

1
2
3
4
5
6
7
8
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mDragCloseHelper.handleEvent(ev)) {
return true;
} else {
return super.dispatchTouchEvent(ev);
}
}

更多使用方法参见 sample

参考

OkHttp 源码分析(三):连接机制

前面两篇文章分别介绍了 OkHttp 的请求流程和缓存机制,最后这篇文章介绍 OkHttp 的连接机制,作为 OkHttp 源码分析的收尾。

建议将 OkHttp 的源码下载下来,使用 IDEA 编辑器可以直接打开阅读。我这边也将最新版的源码下载下来,进行了注释说明,有需要的可以直接从 Android open framework analysis 查看。

创建连接

OkHttp 连接的创建是通过 StreamAllocation 对象统筹完成。

它主要用来管理两个角色:

  • RealConnection:真正建立连接的对象,利用 Socket 建立连接。
  • ConnectionPool:连接池,用来管理和复用连接。

StreamAllocation 是在 RetryAndFollowUpInterceptor 中被创建,此时并未发起连接。

1
2
3
4
// RetryAndFollowUpInterceptor .intercept()

StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);

真正的连接是在处理完 Header 和缓存之后,调用 ConnectInterceptor 进行的。

1
2
3
4
5
6
7
8
// ConnectInterceptor.intercept()

StreamAllocation streamAllocation = realChain.streamAllocation();

// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();

这里创建了两个对象:

  • HttpCodec:用来编码 http request 和解码 http response
  • RealConnection:上文介绍了。

调用 streamAllocation 的 newStream 方法经过一系列判断最终会走到 findConnection 方法

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;
RealConnection result = null;
Route selectedRoute = null;
Connection releasedConnection;
Socket toClose;
synchronized (connectionPool) {
if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");


// 1、尝试使用已分配的连接
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
if (this.connection != null) {
// 当前连接可用.
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
// If the connection was never reported acquired, don't report it as released!
releasedConnection = null;
}

// 2、尝试从连接池中获取一个连接
if (result == null) {
// Attempt to get a connection from the pool.
Internal.instance.acquire(connectionPool, address, this, null);
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
selectedRoute = route;
}
}
}
closeQuietly(toClose);

if (releasedConnection != null) {
eventListener.connectionReleased(call, releasedConnection);
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
}
if (result != null) {
// 如果从连接池中获取到了一个连接,就将其返回.
return result;
}

// If we need a route selection, make one. This is a blocking operation.
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
routeSelection = routeSelector.next();
}


synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");

if (newRouteSelection) {
// Now that we have a set of IP addresses, make another attempt at getting a connection from
// the pool. This could match due to connection coalescing.
// 根据一系列的 IP地址从连接池中获取一个链接
List<Route> routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
// 从连接池中获取一个连接
Internal.instance.acquire(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}

// 3、如果连接池中没有可用连接,则创建一个
if (!foundPooledConnection) {
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}

// Create a connection and assign it to this allocation immediately. This makes it possible
// for an asynchronous cancel() to interrupt the handshake we're about to do.
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result, false);
}
}

// If we found a pooled connection on the 2nd time around, we're done.
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}

//4、 开始TCP以及TLS握手操作
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());

Socket socket = null;
synchronized (connectionPool) {
reportedAcquired = true;

// 5、将新创建的连接,放在连接池中.
Internal.instance.put(connectionPool, result);

// If another multiplexed connection to the same address was created concurrently, then
// release this connection and acquire that one.
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);

eventListener.connectionAcquired(call, result);
return result;
}

整个流程是:

  • 1、判断当前的连接是否可以使用:输入输出流没有关闭,Socket 未关闭等
  • 2、如果当前连接不可用,尝试从连接池中获取一个可用连接
  • 3、如果连接池中没有可用连接,则创建一个连接
  • 4、开始 TCP 连接以及 TLS 握手操作
  • 5、将新创建的连接加入到连接池中

连接池

网络请求时频繁地进行 Socket 连接和断开 Socket 非常消耗网络资源和浪费时间,连接复用可以提升网络访问的效率。这里就引入了连接池的概念。

OKHttp 的连接池由 ConnectionPool 实现。

ConnetionPool 内部维护了一个线程池,负责清理无效的连接。

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
public final class ConnectionPool {
private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS, new SynchronousQueue<>(), Util.threadFactory("OkHttp ConnectionPool", true));

void put(RealConnection connection) {
assert (Thread.holdsLock(this));

if (!cleanupRunning) {
cleanupRunning = true;
// 使用线程池执行清理任务
executor.execute(cleanupRunnable);
}
// 将新建的连接插入到双端队列中
connections.add(connection);
}

private final Runnable cleanupRunnable = () -> {
while (true) {
// 清理操作,返回下次需要清理的时间
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
};
}

ConnectionPool 维护一个线程池用于清理无效的连接,清理任务由 cleanup方法完成,首先执行清理,返回下次需要清理的间隔时间,然后调用 wait 方法释放锁。等到了时间,再次进行清理操作,返回下一次清理的时间,循环往复下去。

具体看一下 cleanup 方法:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;

// Find either a connection to evict, or the time that the next eviction is due.
synchronized (this) {
// 遍历所有的连接
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();

// 1、连接正在使用,即StreanAllocation的引用数量大于0
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}

idleConnectionCount++;

// If the connection is ready to be evicted, we're done.
// 2、如果找到了一个可以被清理的连接,会尝试去寻找闲置时间最久的连接来释放
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}

// maxIdleConnections 表示最大允许的闲置的连接的数量,keepAliveDurationNs表示连接允许存活的最长的时间。
// 默认空闲连接最大数目为5个,keepalive 时间最长为5分钟
// 3、如果空闲连接超过5个或者keepalive时间大于5分钟,则将该连接清理
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// 4、闲置的连接的数量大于0,停顿指定的时间(等会儿会将其清理掉,现在还不是时候)
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we run again.
///5、所有的连接都在使用中,5分钟后再清理
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
//6、没有连接
cleanupRunning = false;
return -1;
}
}

closeQuietly(longestIdleConnection.socket());

// Cleanup again immediately.
return 0;
}

整体流程如下:

  • 1、遍历所有连接,查询每个连接的内部的 StreamAllocation 的引用数量,如果大于 0,表示连接正在使用,无需清理,执行下一次循环。
  • 2、如果找到了一个可以被清理的连接,会尝试去寻找闲置时间最久的连接来释放。
  • 3、如果空闲连接超过 5 个或者 keepalive 时间大于 5 分钟,则将该连接清理。
  • 4、闲置的连接的数量大于 0,返回该连接的到期时间(等会儿会将其清理掉,现在还不是时候)。
  • 5、全部都是活跃连接,5 分钟后再进行清理。
  • 6、没有任何连接,跳出循环。

RealConnection 内有一个 SteamAllocation 虚引用列表,每次创建的 StreamAllocation,都会被添加到这个列表中,如果流关闭后就将 SteamAllocation 对象从该列表中移出去,也正是利用这种计数方式判定一个连接是否为空闲连接。

查询引用计数是在 pruneAndGetAllocationCount 方法中实现。

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
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
// 虚引用列表
List<Reference<StreamAllocation>> references = connection.allocations;
// 遍历虚引用列表
for (int i = 0; i < references.size(); ) {
Reference<StreamAllocation> reference = references.get(i);
//如果虚引用StreamAllocation正在被使用,则跳过进行下一次循环
if (reference.get() != null) {
i++;
continue;
}

// We've discovered a leaked allocation. This is an application bug.
StreamAllocation.StreamAllocationReference streamAllocRef =
(StreamAllocation.StreamAllocationReference) reference;
String message = "A connection to " + connection.route().address().url()
+ " was leaked. Did you forget to close a response body?";
Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);

// 移除引用
references.remove(i);
connection.noNewStreams = true;

// If this was the last allocation, the connection is eligible for immediate eviction.
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs;
return 0;
}
}

return references.size();
}

参考

OkHttp 源码分析(二):缓存机制

上一篇文章我们主要介绍了 OkHttp 的请求流程,这篇文章讲解一下 OkHttp 的缓存机制。

建议将 OkHttp 的源码下载下来,使用 IDEA 编辑器可以直接打开阅读。我这边也将最新版的源码下载下来,进行了注释说明,有需要的可以直接从 Android open framework analysis 查看。

在网络请求的过程中,一般都会使用到缓存,缓存的意义在于,对于客户端来说,使用缓存数据能够缩短页面展示数据的时间,优化用户体验,同时降低请求网络数据的频率,避免流量浪费。对于服务端来说,使用缓存能够分解一部分服务端的压力。

在讲解 OkHttp 的缓存机制之前,先了解下 Http 的缓存理论知识,这是实现 OkHttp 缓存的基础。

Http 缓存

Http 的缓存机制如下图:

http-cache

Http 的缓存分为两种:强制缓存和对比缓存。强制缓存优先于对比缓存。

强制缓存

客户端第一次请求数据时,服务端返回缓存的过期时间(通过字段 Expires 与 Cache-Control 标识),后续如果缓存没有过期就直接使用缓存,无需请求服务端;否则向服务端请求数据。

Expires

服务端返回的到期时间。下一次请求时,请求时间小于 Expires 的值,直接使用缓存数据。

由于到期时间是服务端生成,客户端和服务端的时间可能存在误差,导致缓存命中的误差。

Cache-Control

Http1.1 中采用了 Cache-Control 代替了 Expires,常见 Cache-Control 的取值有:

  • private: 客户端可以缓存
  • public: 客户端和代理服务器都可缓存
  • max-age=xxx: 缓存的内容将在 xxx 秒后失效
  • no-cache: 需要使用对比缓存来验证缓存数据,并不是字面意思
  • no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发

对比缓存

对比缓存每次请求都需要与服务器交互,由服务端判断是否可以使用缓存。

客户端第一次请求数据时,服务器会将缓存标识(Last-Modified/If-Modified-Since 与 Etag/If-None-Match)与数据一起返回给客户端,客户端将两者备份到缓存数据库中。

当再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,返回 304 状态码,通知客户端可以使用缓存数据,服务端不需要将报文主体返回给客户端。

Last-Modified/If-Modified-Since

Last-Modified 表示资源上次修改的时间,在第一次请求时服务端返回给客户端。

客户端再次请求时,会在 header 里携带 If-Modified-Since ,将资源修改时间传给服务端。

服务端发现有 If-Modified-Since 字段,则与被请求资源的最后修改时间对比,如果资源的最后修改时间大于 If-Modified-Since,说明资源被改动了,则响应所有资源内容,返回状态码 200;否则说明资源无更新修改,则响应状态码 304,告知客户端继续使用所保存的缓存。

**Etag/If-None-Match **

优先于 Last-Modified/If-Modified-Since。

Etag 是当前资源在服务器的唯一标识,生成规则由服务器决定。当客户端第一次请求时,服务端会返回该标识。

当客户端再次请求数据时,在 header 中添加 If-None-Match 标识。

服务端发现有 If-None-Match 标识,则会与被请求资源对比,如果不同,说明资源被修改,返回 200;如果相同,说明资源无更新,响应 304,告知客户端继续使用缓存。

OkHttp 缓存

为了节省流量和提高响应速度,OkHttp 有自己的一套缓存机制,CacheInterceptor 就是用来负责读取缓存以及更新缓存的。

我们来看 CacheInterceptor 的关键代码:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
@Override
public Response intercept(Chain chain) throws IOException {
// 1、如果此次网络请求有缓存数据,取出缓存数据作为候选
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;

long now = System.currentTimeMillis();

// 2、根据cache获取缓存策略
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
// 通过缓存策略计算的网络请求
Request networkRequest = strategy.networkRequest;
// 通过缓存策略处理得到的缓存响应数据
Response cacheResponse = strategy.cacheResponse;

if (cache != null) {
cache.trackResponse(strategy);
}

// 缓存数据不能使用,清理此缓存数据
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body());
}

// 3、不进行网络请求,而且没有缓存数据,则返回网络请求错误的结果
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}

// 4、如果不进行网络请求,缓存数据可用,则直接返回缓存数据.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}

// 5、缓存无效,则继续执行网络请求。
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}

if (cacheResponse != null) {
// 6、通过服务端校验后,缓存数据可以使用(返回304),则直接返回缓存数据,并且更新缓存
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();

// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}

// 7、读取网络结果,构造response
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();

// 对数据进行缓存
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}

if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}

return response;
}

整个方法的流程如下:

  • 1、读取候选缓存。

  • 2、根据候选缓存创建缓存策略。

  • 3、根据缓存策略,如果不进行网络请求,而且没有缓存数据时,报错返回错误码 504。

  • 4、根据缓存策略,如果不进行网络请求,缓存数据可用,则直接返回缓存数据。

  • 5、缓存无效,则继续执行网络请求。

  • 6、通过服务端校验后,缓存数据可以使用(返回 304),则直接返回缓存数据,并且更新缓存。

  • 7、读取网络结果,构造 response,对数据进行缓存。

OkHttp 通过 CacheStrategy 获取缓存策略,CacheStrategy 根据之前缓存结果与当前将要发生的 request 的Header 计算缓存策略。规则如下:

networkRequest cacheResponse CacheStrategy
null null only-if-cached(表明不进行网络请求,且缓存不存在或者过期,一定会返回 503 错误)
null non-null 不进行网络请求,而且缓存可以使用,直接返回缓存,不用请求网络
non-null null 需要进行网络请求,而且缓存不存在或者过期,直接访问网络。
non-null not-null Header 中含有 ETag/Last-Modified 标识,需要在条件请求下使用,还是需要访问网络。

CacheStrategy 通过工厂模式构造,CacheStrategy.Factory 对象构建以后,调用它的 get 方法即可获得具体的CacheStrategy,CacheStrategy.Factory 的 get方法内部调用的是 CacheStrategy.Factory 的 getCandidate 方法,它是核心的实现。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
private CacheStrategy getCandidate() {
// 1、没有缓存,直接返回包含网络请求的策略结果
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}

// 2、如果握手信息丢失,则返返回包含网络请求的策略结果
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}

// 3、如果根据CacheControl参数有no-store,则不适用缓存,直接返回包含网络请求的策略结果
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}

// 4、如果缓存数据的CacheControl有no-cache指令或者需要向服务器端校验后决定是否使用缓存,则返回只包含网络请求的策略结果
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}

CacheControl responseCaching = cacheResponse.cacheControl();

long ageMillis = cacheResponseAge();
long freshMillis = computeFreshnessLifetime();

if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}

long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}

long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
// 5. 如果缓存在过期时间内则可以直接使用,则直接返回上次缓存
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}

//6. 如果缓存过期,且有ETag等信息,则发送If-None-Match、If-Modified-Since等条件请求
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
return new CacheStrategy(request, null); // No condition! Make a regular request.
}

Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}

整个函数的逻辑就是按照上面的 Http 缓存策略流程图来实现的,这里不再赘述。

我们再简单看下 OkHttp 是如何缓存数据的。

OkHttp 具体的缓存数据是利用 DiskLruCache 实现,用磁盘上的有限大小空间进行缓存,按照 LRU 算法进行缓存淘汰。

Cache 类封装了缓存的实现,缓存操作封装在 InternalCache 接口中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface InternalCache {
// 获取缓存
@Nullable
Response get(Request request) throws IOException;

// 存入缓存
@Nullable
CacheRequest put(Response response) throws IOException;

// 移除缓存
void remove(Request request) throws IOException;

// 更新缓存
void update(Response cached, Response network);

// 跟踪一个满足缓存条件的GET请求
void trackConditionalCacheHit();

// 跟踪满足缓存策略CacheStrategy的响应
void trackResponse(CacheStrategy cacheStrategy);
}

Cache 类在其内部实现了 InternalCache 的匿名内部类,内部类的方法调用 Cache 对应的方法。

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
public final class Cache implements Closeable, Flushable {

final InternalCache internalCache = new InternalCache() {
@Override public @Nullable Response get(Request request) throws IOException {
return Cache.this.get(request);
}

@Override public @Nullable CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}

@Override public void remove(Request request) throws IOException {
Cache.this.remove(request);
}

@Override public void update(Response cached, Response network) {
Cache.this.update(cached, network);
}

@Override public void trackConditionalCacheHit() {
Cache.this.trackConditionalCacheHit();
}

@Override public void trackResponse(CacheStrategy cacheStrategy) {
Cache.this.trackResponse(cacheStrategy);
}
};
}

总结

  • OkHttp 的缓存机制是按照 Http 的缓存机制实现。
  • OkHttp 具体的数据缓存逻辑封装在 Cache 类中,它利用 DiskLruCache 实现。
  • 默认情况下,OkHttp 不进行缓存数据。
  • 可以在构造 OkHttpClient 时设置 Cache 对象,在其构造函数中指定缓存目录和缓存大小。
  • 如果对 OkHttp 内置的 Cache 类不满意,可以自行实现 InternalCache 接口,在构造 OkHttpClient 时进行设置,这样就可以使用自定义的缓存策略了。

参考

OkHttp 源码分析(一):请求流程

这篇文章主要梳理一下 OkHttp 的请求流程,对 OkHttp 的实现原理有个整体的把握,再深入细节的实现会更加容易。

建议将 OkHttp 的源码下载下来,使用 IDEA 编辑器可以直接打开阅读。我这边也将最新版的源码下载下来,进行了注释说明,有需要的可以直接从 Android open framework analysis 查看。

基本使用

我们先看一下 OkHttp 的基本使用。

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
// 1、创建 Request
Request request = new Request.Builder()
.get()
.url("xxx")
.build();

// 2、创建 OKHttpClient
OkHttpClient client = new OkHttpClient();

// 3、创建 Call
Call call = client.newCall(request);

try {
// 4、同步请求
Response response = call.execute();
} catch (IOException e) {
e.printStackTrace();
}

// 5、异步请求
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {

}

@Override
public void onResponse(Call call, Response response) throws IOException {

}
});

上面的代码中,首先构建一个请求 Request 和一个客户端 OkHttpClient,然后 OkHttpClient 对象根据 request 调用 newCall 方法创建 Call 对象,再调用 execute 或者 enqueue 方法进行同步或者异步请求。

接下来我们看一看关键类和关键流程的具体实现。

Request

Request 类封装了一次请求需要传递给服务端的参数:请求 method 如 GET/POST 等、一些 header、RequestBody 等等。

Request 类未对外提供 public 的构造函数,所以构建一个 Request 实例需要使用构造者模式构建。

1
2
3
4
5
6
7
Request(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.headers = builder.headers.build();
this.body = builder.body;
this.tags = Util.immutableMap(builder.tags);
}

OkHttpClient

OkHttpClient 支持两种构造方式。

一种是默认的构造方式:

1
OkHttpClient client = new OkHttpClient();

看一下构造函数:

1
2
3
public OkHttpClient() {
this(new Builder());
}

这里 OkHttpClient 内部默认配置了一些参数。

1
OkHttpClient(Builder builder) {...}

另一种是通过 Builder 配置参数,最后通过 build 方法构建一个 OkHttpClient 对象。

1
2
3
4
5
OkHttpClient client = new OkHttpClient.Builder().build();

public OkHttpClient build() {
return new OkHttpClient(this); // 这里的 this 是 Builder 实例
}

我们看一下 OkHttpClient 可配置哪些参数:

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
final Dispatcher dispatcher;    // 调度器
final @Nullable
Proxy proxy; // 代理
final List<Protocol> protocols; // 协议
final List<ConnectionSpec> connectionSpecs; // 传输层版本和连接协议
final List<Interceptor> interceptors; // 拦截器
final List<Interceptor> networkInterceptors; // 网络拦截器
final EventListener.Factory eventListenerFactory;
final ProxySelector proxySelector; // 代理选择器
final CookieJar cookieJar; // cookie
final @Nullable
Cache cache; // 缓存
final @Nullable
InternalCache internalCache; // 内部缓存
final SocketFactory socketFactory; // socket 工厂
final SSLSocketFactory sslSocketFactory; // 安全套接层 socket 工厂,用于 https
final CertificateChainCleaner certificateChainCleaner; // 验证确认响应证书 适用 HTTPS 请求连接的主机名
final HostnameVerifier hostnameVerifier; // 主机名字验证
final CertificatePinner certificatePinner; // 证书链
final Authenticator proxyAuthenticator; // 代理身份验证
final Authenticator authenticator; // 本地身份验证
final ConnectionPool connectionPool; // 连接池
final Dns dns; // 域名
final boolean followSslRedirects; // 安全套接层重定向
final boolean followRedirects; // 本地重定向
final boolean retryOnConnectionFailure; // 重试连接失败
final int callTimeout;
final int connectTimeout;
final int readTimeout;
final int writeTimeout;
final int pingInterval;

Call

Call 是一个接口,是请求的抽象描述,具体实现类是 RealCall,通过Call.Factory 创建。

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
public interface Call extends Cloneable {
// 返回当前请求
Request request();

// 同步请求方法
Response execute() throws IOException;

// 异步请求方法
void enqueue(Callback responseCallback);

// 取消请求
void cancel();

// 请求是否在执行(当execute()或者enqueue(Callback responseCallback)执行后该方法返回true)
boolean isExecuted();

// 请求是否被取消
boolean isCanceled();

Timeout timeout();

// 创建一个新的一模一样的请求
Call clone();

interface Factory {
Call newCall(Request request);
}
}

OkHttpClient 实现了 Call.Factory,负责根据 Request 创建新的 Call:

1
Call call = client.newCall(request);

看一下 newCall 方法。

1
2
3
4
@Override
public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}

这里我们发现实际上调用了 RealCall 的静态方法 newRealCall, 不难猜测 这个方法就是创建 Call 对象。

1
2
3
4
5
6
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
// Safely publish the Call instance to the EventListener.
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}

同步请求

从上面的分析我们知道,同步请求调用的实际是 RealCall 的 execute 方法。

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
@Override public Response execute() throws IOException {
synchronized (this) {
// 每个 call 只能执行一次
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
timeout.enter();
eventListener.callStart(this);
try {
// 请求开始, 将自己加入到runningSyncCalls队列中
client.dispatcher().executed(this);
// 通过一系列拦截器请求处理和响应处理得到最终的返回结果
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
e = timeoutExit(e);
eventListener.callFailed(this, e);
throw e;
} finally {
// 请求完成, 将其从runningSyncCalls队列中移除
client.dispatcher().finished(this);
}
}

这里主要做了这几件事:

  • 检测这个 call 是否已经执行了,保证每个 call 只能执行一次。
  • 通知 dispatcher 已经进入执行状态,将 call 加入到 runningSyncCalls 队列中。
  • 调用 getResponseWithInterceptorChain() 函数获取 HTTP 返回结果。
  • 最后还要通知 dispatcher 自己已经执行完毕,将 call 从 runningSyncCalls 队列中移除。

这里涉及到了 Dispatcher 这个类,我们在异步请求这一节中再介绍。

真正发出网络请求以及解析返回结果的是在 getResponseWithInterceptorChain 方法中进行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Response getResponseWithInterceptorChain() throws IOException {
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}

interceptors.add(new CallServerInterceptor(forWebSocket));

Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());

return chain.proceed(originalRequest);

getResponseWithInterceptorChain 方法的代码量并不多,但是却完成了所有的请求处理过程。

这里先是创建了一个 Interceptor 的集合,然后将各类 interceptor 全部加入到集合中,包含以下 interceptor:

  • interceptors:配置 OkHttpClient 时设置的 inteceptors

  • RetryAndFollowUpInterceptor:负责失败重试以及重定向

  • BridgeInterceptor:负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应

  • CacheInterceptor:负责读取缓存直接返回、更新缓存

  • ConnectInterceptor:负责和服务器建立连接

  • networkInterceptors:配置 OkHttpClient 时设置的 networkInterceptors

  • CallServerInterceptor:负责向服务器发送请求数据、从服务器读取响应数据

添加完拦截器后,创建了一个 RealInterceptorChain 对象,将集合 interceptors 和 index(数值0)传入。接着调用其 proceed 方法进行请求的处理,我们来看 proceed方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
...
// 创建下一个RealInterceptorChain,将index+1(下一个拦截器索引)传入
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
// 获取当前的拦截器
Interceptor interceptor = interceptors.get(index);
// 通过Interceptor的intercept方法进行处理
Response response = interceptor.intercept(next);
...
return response;
}

我们来看一些关键代码:

RealInterceptorChain 的 proceed 方法先创建 RealInterceptorChain 的对象,将集合 interceptors 和 index + 1 传入。从前面的分析知道,初始 index 为 0。

然后获取当前 index 位置上的 Interceptor,将创建的 RealInterceptorChain 对象 next 传入到当前拦截器的 intercept 方法中,intercept 方法内部会调用 next 的 proceed 方法,一直递归下去,最终完成一次网络请求。

所以每个 Interceptor 主要做两件事情:

  • 拦截上一层拦截器封装好的 Request,然后自身对这个 Request 进行处理,处理后向下传递。
  • 接收下一层拦截器传递回来的 Response,然后自身对 Response 进行处理,返回给上一层。

异步请求

异步请求调用的是 RealCall 的 enqueue 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void enqueue(Callback responseCallback) {
synchronized(this) {
if (this.executed) {
throw new IllegalStateException("Already Executed");
}

this.executed = true;
}

this.captureCallStackTrace();
this.eventListener.callStart(this);
this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}

与同步请求一样,异步请求也涉及了一个重要的参与者 Dispatcher,它的作用是:控制每一个 Call 的执行顺序和生命周期。它内部维护了三个队列:

  • readyAsyncCalls:等待的异步请求队列
  • runningAsyncCalls:正在运行的异步请求队列
  • runningSyncCalls:正在运行的同步请求队列

对于同步请求,由于它是即时运行的, Dispatcher 只需要运行前请求前存储到 runningSyncCalls,请求结束后从 runningSyncCalls 中移除即可。

对于异步请求,Dispatcher 是通过启动 ExcuteService 执行,线程池的最大并发量 64,异步请求先放置在 readyAsyncCalls,可以执行时放到 runningAsyncCalls 中,执行结束从runningAsyncCalls 中移除。

我们看一下具体实现细节,下面是 Dispatcher 的 enqueue 方法,先将 AsyncCall 添加到 readyAsyncCalls。

1
2
3
4
5
6
7
void enqueue(AsyncCall call) {
// 将AsyncCall加入到准备异步调用的队列中
synchronized (this) {
readyAsyncCalls.add(call);
}
promoteAndExecute();
}

再看 promoteAndExecute 方法:

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
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));

List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();

if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.

i.remove();
executableCalls.add(asyncCall);
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}

for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService());
}

return isRunning;
}

这里主要的工作有:

  • 从准备异步请求的队列中取出可以执行的请求(正在运行的异步请求不得超过64,同一个host下的异步请求不得超过5个),加入到 executableCalls 列表中。
  • 循环 executableCalls 取出请求 AsyncCall 对象,调用其 executeOn 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void executeOn(ExecutorService executorService) {
assert (!Thread.holdsLock(client.dispatcher()));
boolean success = false;
try {
executorService.execute(this);
success = true;
} catch (RejectedExecutionException e) {
InterruptedIOException ioException = new InterruptedIOException("executor rejected");
ioException.initCause(e);
eventListener.callFailed(RealCall.this, ioException);
responseCallback.onFailure(RealCall.this, ioException);
} finally {
if (!success) {
client.dispatcher().finished(this); // This call is no longer running!
}
}
}

可以看到 executeOn 方法的参数传递的是 ExecutorService 线程池对象,方法中调用了线程池的 execute方法,所以 AsyncCall 应该是实现了 Runnable 接口,我们看看它的 run 方法是怎样的。

AsyncCall 继承自 NamedRunnable 抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class NamedRunnable implements Runnable {
protected final String name;

public NamedRunnable(String format, Object... args) {
this.name = Util.format(format, args);
}

@Override public final void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
execute();
} finally {
Thread.currentThread().setName(oldName);
}
}

protected abstract void execute();
}

所以当线程池执行 execute 方法会走到 NamedRunnable 的 run 方法,run 方法中又调用了 抽象方法 execute,我们直接看 AsyncCall 的 execute 方法。

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
@Override
protected void execute() {
boolean signalledCallback = false;
timeout.enter();
try {
// 请求网络获取结果
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
// 回调结果
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
e = timeoutExit(e);
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
eventListener.callFailed(RealCall.this, e);
responseCallback.onFailure(RealCall.this, e);
}
} finally {
// 调度完成,移出队列
client.dispatcher().finished(this);
}
}
}

这里我们又看到了熟悉的 getResponseWithInterceptorChain 方法。

这样看来,同步请求和异步请求的原理是一样的,都是在 getResponseWithInterceptorChain() 函数中通过 Interceptor 链条来实现的网络请求逻辑。

总结

以上便是 OkHttp 整个请求的具体流程,流程图如下。

okhttp

简述 OkHttp 的请求流程:

  • OkhttpClient 实现了 Call.Fctory,负责为 Request 创建 Call。

  • RealCall 是 Call 的具体实现,它的异步请求是通过 Dispatcher 调度器利用 ExcutorService 实现,而最终进行网络请求时和同步请求一样,都是通过 getResponseWithInterceptorChain 方法实现。

  • getResponseWithInterceptorChain 方法中采用了责任链模式,每一个拦截器各司其职,主要做两件事。

    • 拦截上一层拦截器封装好的 Request,然后自身对这个 Request 进行处理,处理后向下传递。
    • 接收下一层拦截器传递回来的 Response,然后自身对 Response 进行处理,返回给上一层。

参考

Java 内部类总结

Java 中,可以将一个类定义在另一个类或者一个方法里面,这样的类称为内部类。

一般包含四种内部类:成员内部类、匿名内部类、局部内部类和静态内部类。

成员内部类

成员内部类的定义位于另一个类的内部,形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Outer {

private String name;

class Inner {
Inner(){
name = "wuzy";
}

private void displayName() {
System.out.println(name);
}
}

public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.displayName(); // wuzy
}
}

成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括 private 成员和静态成员)。具体是如何实现的呢?通过反编译字节码看个究竟。

先对 Outer 类进行编译 javac Outer.java ,编译器在编译的时候,会将成员内部类 Inner 单独编译成一个字节码文件 Outer$Inner.class

反编译 Outer$Inner.class 文件得到下面的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
E:\project\JavaExample\src\innerclassexample>javap -c Outer$Inner.class
Compiled from "Outer.java"
class innerclassexample.Outer$Inner {
final innerclassexample.Outer this$0;

innerclassexample.Outer$Inner(innerclassexample.Outer);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field this$0:Linnerclassexample/Outer;
5: aload_0
6: invokespecial #3 // Method java/lang/Object."<init>":()V
9: aload_1
10: ldc #4 // String wuzy
12: invokestatic #5 // Method innerclassexample/Outer.access$002:(Linnerclassexample/Outer;Ljava/lang/String;)Ljava/lang/String;
15: pop
16: return

static void access$100(innerclassexample.Outer$Inner);
Code:
0: aload_0
1: invokespecial #1 // Method displayName:()V
4: return
}

可以看到这两行关键信息

1
2
3
final innerclassexample.Outer this$0;

innerclassexample.Outer$Inner(innerclassexample.Outer);

这就很明显了,编译器会默认为成员内部类添加了一个指向外部类对象的引用,这个引用的赋值默认是在构造函数中进行。因此可以在成员内部类中任意的访问外部类的成员。

此外也说明了成员内部类是依赖于外部类的,如果没有创建外部类,则无法对 Outer this$0 引用赋值,也就无法创建内部类的对象了。

匿名内部类

匿名内部类也就是没有名字的内部类,通常用来简化代码。

使用匿名内部类的前提条件:必须继承一个父类或者实现一个接口。形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Outer {

private void test(final int b) {
final int a = 1;
new Thread() {
@Override
public void run() {
super.run();
System.out.println(a);
System.out.println(b);
}
}.start();
}

public static void main(String[] args) {
Outer outer = new Outer();
outer.test(2);
}
}

在 jdk 1.8 之前,匿名内部类访问方法局部变量或方法形参时,局部变量和形参必须以 final 修饰。

为什么?

以下是分析过程。

当 外部类的 test 方法执行完毕,局部变量 a 和 形参 b 的都会出栈,生命周期也就结束了,但此时 Thread 对象的生命周未必就结束了,那么 run 方法中访问 a 或者 b 就不可能了,但是又要实现这种效果,Java 采取了 复制 的手段解决了这个问题。

对以上代码进行编译,编译器会将匿名内部类编译成 Outer$1.class 文件,再对这个字节码文件进行反编译。

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
E:\project\JavaExample\src\innerclassexample>javap -c Outer$1.class
Compiled from "Outer.java"
class innerclassexample.Outer$1 extends java.lang.Thread {
final int val$b;

final innerclassexample.Outer this$0;

innerclassexample.Outer$1(innerclassexample.Outer, int);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Linnerclassexample/Outer;
5: aload_0
6: iload_2
7: putfield #2 // Field val$b:I
10: aload_0
11: invokespecial #3 // Method java/lang/Thread."<init>":()V
14: return

public void run();
Code:
0: aload_0
1: invokespecial #4 // Method java/lang/Thread.run:()V
4: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
7: iconst_1
8: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
11: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
14: aload_0
15: getfield #2 // Field val$b:I
18: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
21: return
}

run 方法中有一条指令:

1
iconst_1

这条指令表示将操作数 1 压入栈中(当 int 取值-1~5 时,JVM 采用 iconst 指令将常量压入栈中),表示使用的是一个本地局部变量。

还有这三行信息:

1
2
3
4
5
final int val$b;

final innerclassexample.Outer this$0;

innerclassexample.Outer$1(innerclassexample.Outer, int);

this$0 是指向外部类的引用,val$b 是形参 b 的拷贝,都是由编译器在构造函数中赋值初始化的。

从上面可以看出,如果局部变量的值在编译期间就可以确定,则直接在匿名类内部里面创建一个拷贝,如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。

这就导致了一个新的问题,数据不一致run 方法访问的 a 压根不是 test 方法的局部变量 a,当在 run 方法改变变量 a 时候,test 方法的局部变量 a 并没有改变。

为了解决这个问题,Java 采取了粗暴的方式,限定必须将变量 a 限制为 final 变量,不允许对变量 a 进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。

这也就解释了为什么匿名内部类只能访问局部 final 变量了。

在 JDK 1.8 以后,匿名内部类可以访问到非 final 变量了。以下这种写法完全没问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Outer {

private void test(int b) {
int a = 1;
new Thread() {
@Override
public void run() {
super.run();
System.out.println(a);
System.out.println(b);
}
}.start();
}

public static void main(String[] args) {
Outer outer = new Outer();
outer.test(2);
}
}

对其反编译下:

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

E:\project\JavaExample\src\innerclassexample>javap -c Outer$1.class
Compiled from "Outer.java"
class innerclassexample.Outer$1 extends java.lang.Thread {
final int val$a;

final int val$b;

final innerclassexample.Outer this$0;

innerclassexample.Outer$1(innerclassexample.Outer, int, int);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Linnerclassexample/Outer;
5: aload_0
6: iload_2
7: putfield #2 // Field val$a:I
10: aload_0
11: iload_3
12: putfield #3 // Field val$b:I
15: aload_0
16: invokespecial #4 // Method java/lang/Thread."<init>":()V
19: return

public void run();
Code:
0: aload_0
1: invokespecial #5 // Method java/lang/Thread.run:()V
4: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
7: aload_0
8: getfield #2 // Field val$a:I
11: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
14: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
17: aload_0
18: getfield #3 // Field val$b:I
21: invokevirtual #7 // Method java/io/PrintStream.println:(I)V
24: return
}

可以看到这四行

1
2
3
4
5
6
7
final int val$a;

final int val$b;

final innerclassexample.Outer this$0;

innerclassexample.Outer$1(innerclassexample.Outer, int, int);

JVM 编译器会在匿名内部类的构造函数中对局部变量 a 和 形参 b 进行拷贝赋值。而且, run 方法是无法修改变量 a 和 形参 b 的值的。

局部内部类

定义在方法体或者代码块里的类称为局部内部类。形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Outer {

private int num1 = 1;

private void test() {
class Inner {
private int num2 = 2;
private void display() {
System.out.println(num1);
System.out.println(num2);
}
}
Inner inner = new Inner();
inner.display();
}

public static void main(String[] args) {
Outer outer = new Outer();
outer.test();
}
}

注意,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。

静态内部类

静态内部类与成员内部类的定义方式类似,也是定义在另一个类的内部,只不过在类的前面多了一个 static 关键字。形式如下:

1
2
3
4
5
public class Outer {

static class Inner {
}
}

静态内部类与类的静态属性类似,不依赖于对象,无法访问外部类的非静态成员,因为外部类的非静态成员依附于具体的对象。从下面的反编译结果也能看出,静态内部类是不持有外部类对象的引用的。

1
2
3
4
5
6
7
8
9
E:\project\JavaExample\src\innerclassexample>javap -c Outer$Inner.class
Compiled from "Outer.java"
class innerclassexample.Outer$Inner {
innerclassexample.Outer$Inner();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
}

内部类的使用场景和好处

1、内部类使得多继承的解决方案变得完整。内部类(除去用 static 修饰的 )可以直接使用其外部类的成员变量以及成员函数,达到一个继承的效果,再加上自身继承基类来达到一个多重继承的效果。

2、方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。

3、方便编写事件驱动程序。比如 Android 里面的事件监听。

参考

https://www.cnblogs.com/dolphin0520/p/3811445.html

Java 类加载器

类的生命周期

classloader_lifecycle
包含七个阶段:

加载、验证、准备、解析、初始化、使用、卸载。

  • 验证、准备、解析三个阶段统称为连接。

  • 加载、验证、准备、初始化和卸载的顺序是确定的,解析和初始化的前后顺序不一定,主要是为了实现动态绑定。

类加载过程

类加载过程主要包含加载、验证、准备、解析和初始化这 5 个阶段。

加载

加载是类加载的第一个阶段,注意不要混淆。

加载过程要完成的三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

其中二进制字节流可以从以下方式获取:

  • 从 ZIP 包读取,称为 JAR、EAR、WAR 格式的基础。
  • 从网络获取,最典型的应用是 Applet。
  • 运行时计算,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
  • 由其他文件生成,典型场景是JSP应用,即由JSP文件生成对应的 Class 类。

验证

验证的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证阶段主要完成四个阶段的校验动作:

  • 文件格式验证。验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
  • 元数据验证。对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
  • 字节码验证。通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证。主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。确保解析动作能正常执行。

准备

为类变量分配内存并设置类变量初始值的阶段。类变量是指被 static 修饰的变量。

这里的初始值一般是数据类型的零值。比如:

1
public static int value = 123;

变量 value 在准备阶段的初始值是 0 而不是 123。

如果类变量是常量,初始值是所定义的值而不是零值。例如下面的常量 value 被初始化为 123 而不是 0。

1
public static final int value = 123;

解析

将常量池的符号引用替换为直接引用的过程。

其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。

  • 符号引用:符号引用就是字符串,这个字符串包含足量的信息,以供实际使用时可以找到相应的位置。
  • 直接引用:直接引用就是偏移量,通过便宜量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。

初始化

初始化是类加载过程中的最后一步,这个阶段才真正开始执行类中定义的 Java 程序代码。

初始化阶段是虚拟机执行类构造器 <clinit>() 方法的过程。

在准备阶段,已经为类变量分配了系统所需的初始值,并且在初始化阶段,根据程序员通过程序进行的主观计划来初始化类变量和其他资源。<clinit>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:

1
2
3
4
5
6
7
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}

由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static class Parent {
public static int A = 1;
static {
A = 2;
}
}

static class Sub extends Parent {
public static int B = A;
}

public static void main(String[] args) {
System.out.println(Sub.B); // 2
}

接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。

虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

类加载器

两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。

这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。

类加载器分类

从 Java 虚拟角度,只存在两种不同的类加载器:

  • 启动型类加载器(Bootstrap ClassLoader),C++ 语言实现,是虚拟机自身的一部分。
  • 其他的类加载器:Java 语言实现,独立于虚拟机外部,全部继承自抽象类 java.lang.ClassLoader。

从 Java 开发人员的角度,类加载器大致分为以下三类:

  • 启动类加载器:Bootstrap ClassLoader,跟上面相同。它负责加载存放在 JDK\jre\lib 下,或被 -Xbootclasspath 参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的 java.* 开头的类均被 Bootstrap ClassLoader 加载)。启动类加载器是无法被 Java 程序直接引用的。

  • 扩展类加载器:Extension ClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 JDK\jre\lib\ext 目录中,或者由 java.ext.dirs 系统变量指定的路径中的所有类库(如 javax.* 开头的类),开发者可以直接使用扩展类加载器。

  • 应用程序类加载器:Application ClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

双亲委托模型

应用程序是由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。

下图展示了类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。类加载器之间的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
parents_delegation

工作过程

一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。

好处

使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。

例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。

实现方式

实现双亲委派的代码都集中在java.lang.ClassLoader的 loadClass() 方法之中。

先检查是否已经被加载过,若没有加载则调用父加载器的 loadClass() 方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的 findClass() 方法进行加载。

自定义 ClassLoader

继承 java.lang.ClassLoader 类,重写 findClass 方法。比如:

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
40
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;
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";
}
}

参考

理解 ThreadLocal

ThreadLocal 是什么

ThreadLocal 提供了线程局部变量。它和普通变量的区别在于,普通变量可以被任何一个线程访问并修改,而使用 ThreadLocal 创建的变量只能被当前线程访问,也就是线程私有,其他线程无法访问和修改。

ThreadLocal 用法

创建:

1
ThreadLocal<Boolean> threadLocal = new ThreadLocal<>();

set 方法:

1
threadLocal.set(false);

get 方法:

1
threadLocal.get()

我们来看一个完整的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义一个 ThreadLocal 对象
private ThreadLocal<Boolean> threadLocal = new ThreadLocal<>();

// 分别在主线程、子线程1和子线程2中设置和访问它的值
threadLocal.set(true);
Log.e(TAG, "[Thread#main]threadLocal=" + threadLocal.get() );
new Thread("Thread#1"){
@Override
public void run() {
threadLocal.set(false);
Log.e(TAG, "[Thread#1]threadLocal=" + threadLocal.get() );
}
}.start();

new Thread("Thread#2"){
@Override
public void run() {
Log.e(TAG, "[Thread#2]threadLocal=" + threadLocal.get() );
}
}.start();

在上面的代码中,在主线程中设置 threadLocal 的值为 true,子线程1中设置 threadLocal 的值为 false,子线程2中未设置 threadLocal 的值。

输出结果如下,可以看到,虽然在不同线程中访问的是同一个 ThreadLocal 对象,但是它们通过 ThreadLocal 获取的值却是不一样的。

1
2
3
[Thread#main]threadLocal=true
[Thread#1]threadLocal=false
[Thread#2]threadLocal=null

Android 中应用

在 Android 中,Looper 类就是利用了 ThreadLocal 的特性,保证了每个线程只存在一个 Looper 对象。

1
2
3
4
5
6
7
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

ThreadLocal 原理

ThreadLocal 内部是如何实现的,我们从源码中一探究竟。

set 方法开始,主要工作是

  • 获取当前线程
  • 获取或当前线程的 ThreadLocalMap 对象
  • 如果 ThreadLocalMap 不为空,设置值;否则创建 ThreadLocalMap 对象并设置值
1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

getMap 方法中获取 ThreadLocalMap 的方法

1
2
3
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

这个方法获取得实际是 Thread 对象的 threadLocals 变量

1
ThreadLocal.ThreadLocalMap threadLocals = null;

如果是初次调用 set 方法,则 ThreadLocalMap 对象为空,会去创建 ThreadLocalMap,并设置初始值。

1
2
3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

所以可以总结下 ThreadLocal 的设计思路:

每个 Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 TreadLocal 实例本身,value 是真正存储的值,ThreadLocalMap 只被持有它的线程访问,其他线程也就无法访问和修改。

我们具体看一下 ThreadLocalMap。

构造 ThreadLocalMap 的主要过程:

  • 初始化存放 Entry 对象的数组
  • 通过 key(ThreadLocal 类型)的 hashcode 计算存储的索引位置
  • 在指定索引位置存放 Entry 对象
  • 记录数组中 Entry 对象的个数
  • 设定数组扩展阈值
1
2
3
4
5
6
7
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);
}

下面来看一下 Entry 的结构:

1
2
3
4
5
6
7
8
9
10
11
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {

Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}

Entry 是 ThreadLocalMap 的静态内部类,继承自 WeakReference<ThreadLocal>,从super(k) 可以看出 Entry 是一个对 ThreadLocal 的弱引用。另外,Entry 包含了对 value 的强引用。

ThreadLocal 内存泄漏的问题

首先绘制了 ThreadLocal 相关的对象引用内存图(实线代表强引用,虚线代表弱引用):

threadlocal

图中我们看到 Entry 中的 value 一直有一条从 ThreadRef 连接过来的强引用,只有当前 Thread 结束时,ThreadRef 不在栈中,强引用断开, Thread、ThreadLocalMap、value 都会被 GC 回收。

但是,如果使用的是线程池,那么之前的线程实例处理完之后出于复用的目的依然存活,这就发生了真正意义上的内存泄漏了。

为了最小化减少内存泄露的可能性和影响,ThreadLocal 的设计中加入了一些防护措施。

getEntry 方法:

首先从索引位置获取 Entry,如果 Entry 不为空且 key 相同则返回 Entry,否则调用 getEntryAfterMiss 方法向下一个位置查询。

1
2
3
4
5
6
7
8
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

getEntryAfterMiss 方法:

整个过程中,如果遇到 key 为空的情况,会调用 expungeStaleEntry 方法进行擦除 Entry(Entry 中的 value 对象没有了强引用,自然会被回收)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
// 如果key值为null,则擦除该位置的Entry
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

expungeStaleEntry 方法:

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
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// 设置value为null
tab[staleSlot].value = null;
// 设置entry为null
tab[staleSlot] = null;
size--;

Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

set 方法:

set 方法也有同样的操作,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏。

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
private void set(ThreadLocal<?> key, Object value) {

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

replaceStaleEntry 方法:

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
40
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;

int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;

for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;

tab[i] = tab[staleSlot];
tab[staleSlot] = e;

if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}

if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}

tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);

if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

但是,以上的设计思路依赖一个前提条件:必须调用 ThreadLocalMap 的 getEntry 或者 set 方法。

如果这个前提条件不成立,还是会发生内存泄漏。所以,很多情况下需要手动去调用 ThreadLocal 的 remove 方法,手动删除不再需要的 ThreadLocal,进而释放 Entry,避免内存泄漏。此外,JDK 推荐 ThreadLocal 变量定义为 private static ,这样 ThreadLocal 的生命周期会更长,ThreadLocal 在线程运行中不会被回收,也就能保证任何时候都能够通过 ThreadLocal 的弱引用访问到 Entry 的 value 值,然后执行 remove 操作,防止内存泄漏。

总结

1、ThreadLocal 通过隐式在不同的线程中创建实例副本,避免了实例线程安全的问题。

2、ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收问题。

3、如果使用的是线程池,那么之前的线程实例处理完之后出于复用的目的依然存活,这时可能会出现内存泄漏。

4、ThreadLocal 为避免上述的内存泄漏,在 get 和 set 方法中都做了防护措施,但前提是这两个方法得到了执行。因此很多情况下还需要手动调用 ThreadLocal 的 remove 方法,避免内存泄漏。

5、当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用 ThreadLocal。

Java 内存分配和回收策略

Java 的内存分配主要是在程序运行时给对象在堆上分配内存。通常将堆内存结构按新生代和老年代进行划分,堆内存结构图如下:

jvm_heap_allocation

新生代

大部分对象创建和销毁的区域。

内部包含 Eden 区域,作为对象初始分配的区域;两个 Survivor,也叫 from、to 区域,用来放置从 Minor GC 中生存下来的对象。

TLAB

对 Eden 区域再进行划分, Hotspot JVM 还有一个概念叫着 Thread Local Allocation(TLAB),这是 JVM 为每个线程分配的一个私有缓存区域。多线程同时分配内存时,为了避免操作同一地址,可能需要使用加锁机制,进而影响分配速度。TLAB 能够解决这个问题。

jvm_tlab

start、end 就是每个 TLAB 的起始结束指针,top 则表示已经分配到哪里。所以在分配新对象时,移动 top,当 top 与 end 相遇,表示该缓存已经满了, JVM 会试图再从 Eden 里分配一块。

老年代

大对象直接进入老年代

对象先在 TLAB 上分配内存,如果 TLAB 空间不足,会在 Eden 区域给对象分配空间,但是如果对象太大,无法在新生代找到足够长的连续空闲空间, JVM 会直接将对象分配到老年代。

这里的大对象比如是较大的字符串或者数组,因此在写程序时避免分配“朝生夕死”的大对象。

长期存活的对象直接进入老年代

在经历了多次 Minor GC 后仍然存活的对象,如果对象的年龄达到老年代阈值,会直接进入老年代。下文会阐述。

动态对象年龄判定

为了适应不同程序的内存情况,虚拟机不是永远只在对象的年龄达到老年代阈值时才将对象晋升到老年代。

如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进入老年代。

永久代

早期 JVM 的方法区实现,储存 Java 常量池、类元数据等,在 JDK 8 之后取消了永久代。

堆内存参数

最大堆体积

1
-Xmx:value

最小堆体积

1
-Xms:value

老年代和新生代的比例。

1
-XX:NewRatio=value

默认情况,老年代是新生代的 2 倍。即 新生代是堆大小的 1/3。也可以直接调整新生代的大小。

1
-XX:NewSize=value

Eden 和 Survivor 的大小比例。YoungGen = Eden + 2 * Survivor。

1
-XX:SurvivorRatio=value

堆内存结构中每一代中都存在Reserved 区域,当 Xms 小于 Xmx 时,堆的大小不会直接扩展到上限。当内存需求不断增长, JVM 会逐渐扩展区域大小,所以 Reserved 区域表示保留区域,暂时不可用的空间。

Minor GC

新生代 GC。

Java 应用不断创建对象,优先分配在 Eden 区域,当空间占用达到一定阈值时,触发 Minor GC。没有被引用的对象被回收,仍然存活的对象被复制到 JVM 选择的 Survivor 区域。如下图,数字 1 表示对象的存活年龄计数。

jvm_minor_gc_1

在下一次 Minor GC 时,另外一个 Survivor 区域会成为 to 区域, Eden 区域存活的对象和 from 区域对象都会被复制到 to 区域,存活的年龄计会被加 1。

jvm_minor_gc_2

上述过程会发生很多次,直到有对象年龄计数达到阈值,这些对象会被晋升到老年代。

jvm_minor_gc_3

Full GC

新生代、老年代和永久代都进行 GC 操作。

调用System.gc()

代码中 System.gc() 方法的调用是建议 JVM 进行 Full GC,多数情况下会触发 Full GC。

老年代空间不足

老年代的对象主要是大对象、长期存活的对象。如果老年代空间不足时,会触发 Full GC。

空间分配担保失败

当准备要触发一次 Minor GC 时,如果发现统计数据说之前 Minor GC 的平均晋升大小比目前老年代剩余的空间大,则不会触发 Minor GC 而是转为触发 Full GC。

JDK 1.7 及以前永久代空间不足

在JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。

在JDK 1.8中用元空间替换了永久代作为方法区的实现,元空间是本地内存,因此减少了一种 Full GC 触发的可能性。

Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

参考资料

深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)

Java 核心技术 36 讲(极客时间)

JVM 安全点介绍

什么是安全点?

JVM 中如何判断对象可以被回收 一文中,我们知道 HotSpot 虚拟机采取的是可达性分析算法。即通过 GC Roots 枚举判定待回收的对象。

那么,首先要找到哪些是 GC Roots。

有两种查找 GC Roots 的方法:

一种是遍历方法区和栈区查找(保守式 GC)。

一种是通过 OopMap 数据结构来记录 GC Roots 的位置(准确式 GC)。

很明显,保守式 GC 的成本太高。准确式 GC 的优点就是能够让虚拟机快速定位到 GC Roots。

对应 OopMap 的位置即可作为一个安全点(Safe Point)。

在执行 GC 操作时,所有的工作线程必须停顿,这就是所谓的”Stop-The-World”。

为什么呢?

因为可达性分析算法必须是在一个确保一致性的内存快照中进行。如果在分析的过程中对象引用关系还在不断变化,分析结果的准确性就不能保证。

安全点意味着在这个点时,所有工作线程的状态是确定的,JVM 就可以安全地执行 GC 。

如何选定安全点?

安全点太多,GC 过于频繁,增大运行时负荷;安全点太少,GC 等待时间太长。

一般会在如下几个位置选择安全点:

1、循环的末尾

2、方法临返回前

3、调用方法之后

4、抛异常的位置

为什么选定这些位置作为安全点:

主要的目的就是避免程序长时间无法进入 Safe Point。比如 JVM 在做 GC 之前要等所有的应用线程进入安全点,如果有一个线程一直没有进入安全点,就会导致 GC 时 JVM 停顿时间延长。比如这里,超大的循环导致执行 GC 等待时间过长。

如何在 GC 发生时,所有线程都跑到最近的 Safe Point 上再停下来?

主要有两种方式:

抢断式中断:在 GC 发生时,首先中断所有线程,如果发现线程未执行到 Safe Point,就恢复线程让其运行到 Safe Point 上。

主动式中断:在 GC 发生时,不直接操作线程中断,而是简单地设置一个标志,让各个线程执行时主动轮询这个标志,发现中断标志为真时就自己中断挂起。

JVM 采取的就是主动式中断。轮询标志的地方和安全点是重合的。

安全区域又是什么?

Safe Point 是对正在执行的线程设定的。

如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。

因此 JVM 引入了 Safe Region。

Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。

线程在进入 Safe Region 的时候先标记自己已进入了 Safe Region,等到被唤醒时准备离开 Safe Region 时,先检查能否离开,如果 GC 完成了,那么线程可以离开,否则它必须等待直到收到安全离开的信号为止。

参考

深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)

JVM 七种垃圾收集器

Java 垃圾收集器是 垃圾收集算法 的具体实现。

下图展示的是 7 种作用于不同分代的收集器,如果两种收集器之前有连接,表示它们可以配合使用。收集器所在的位置表示它是属于新生代收集器还是老年代收集器。

seven_garbage_collector

Serial 收集器

单线程串行收集器。即在垃圾清理时,必须暂停其他所有工作线程。

它是采用复制算法新生代收集器

下图是 Serial 收集器的运行过程。

serial_collector

ParNew 收集器

ParNew 收集器是 Serial 收集器的多线程版本。除了使用多线程收集,其他与 Serial 收集相比并无太多创新之处。

默认开启的线程数量与 CPU 数量相同。

在单 CPU 的环境,ParNew 收集器不会比 Serial 收集器更优秀。

parnew_collector

Parallel Scavenge 收集器

Parallel Scavenge 收集器也是一个 并行的多线程新生代收集器,使用的是复制算法

特点在于它的目标是达到一个可控制的吞吐量(Throughput)。

吞吐量就是 CPU 用于运行用户代码得时间与 CPU 消耗时间的比值。

吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)

高吞吐量可以高效率地利用 CPU 时间,尽快完成程序地运行任务,适合在后台运行不需要太多交互的任务。

-XX:GCTimeRatio : 设置吞吐量大小。

-XX:MaxGCPauseMillis : 设置最大垃圾收集停顿时间。

Serial Old 收集器

Serial 收集器的老年代产品。同样是单线程,使用标记整理算法

serial_collector

Parallel Old 收集器

Parallel Old 是 Parallel Scanvenge 的老年代版本,使用多线程标记整理算法

parallel_old_collector

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。

从名称是上看出 CMS 采用的是标记清除算法。整个过程有四个步骤:

  • 初始标记(CMS initial mark):仅仅标记一下 GC Roots 能关联到的对象,速度很快。
  • 并发标记(CMS concurrent mark):GC Roots Tracing 过程。
  • 重新标记(CMS remark):修正并发标记期间引用变化那一部分对象
  • 并发清除(CMS concurrent sweep)

其中,初始标记、重新标记需要“Stop The World”。并发标记和并发清除时收集器线程可以与用户线程一起工作。

cms_collector

优势

并发收集、低停顿。

缺陷

  • 对 CPU 资源敏感。多线程导致占用一部分 CPU 资源而导致应用程序变慢。
  • 无法处理浮动垃圾。并发清理过程中用户线程还在运行,会产生新的垃圾,CMS 无法在当次收集中处理它们,只好等待下一次 GC 时再清理掉。这一部分垃圾称为浮动垃圾。
  • CMS 采取的标记清除算法会产生大量空间碎片。往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。

G1收集器

Region

上述的 GC 收集器将连续的内存空间划分为新生代、老生代和永久代(JDK 8 去除了永久代,引入了元空间 Metaspace),这种划分的特点是各代的存储地址(逻辑地址)是连续的。

G1 (Garbage First) 的各代存储地址是不连续的,每一代都使用了 n 个不连续的大小相同的 region, 每个 region 占有一块连续的虚拟内存地址。

g1_region

G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

避免全堆扫描

多个 Region 之前的对象可能会有引用关系,在做可达性分析时需要扫描整个堆才能保证准确性,这显然降低了 GC 效率。

为避免全堆扫描,虚拟机为 G1 中每个 Region 维护了一个与之对应的 Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的Region的 Remembered Set 之中。当进行内存回收时,在GC根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏。

G1 的运作步骤

  • 初始标记(Initial Marking)
  • 并发标记(Concurrent Marking)
  • 最终标记(Final Marking)
  • 筛选回收(Live Data Counting and Evacuation)

g1_collector

特点:

  • 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
  • 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。

总结

收集器 串行、并行or并发 新生代/老年代 算法 目标 适用场景
Serial 串行 新生代 复制算法 响应速度优先 单CPU环境下的Client模式
Serial Old 串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式、CMS的后备预案
ParNew 并行 新生代 复制算法 响应速度优先 多CPU环境时在Server模式下与CMS配合
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后台运算而不需要太多交互的任务
Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务
CMS 并发 老年代 标记-清除 响应速度优先 集中在互联网站或B/S系统服务端上的Java应用
G1 并发 both 标记-整理+复制算法 响应速度优先 面向服务端应用

参考

深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)

深入理解JVM(3)——7种垃圾收集器