何为StreamAllocation泄露?
上一篇文章ttps://www.jianshu.com/p/3688967830fe说了,每次请求都会将StreamAllocation和其对应的Connection都会建立起关系:StreamAllocation有成员变量connection,Connection#allocations中持有StreamAllocation的弱引用。每次请求完毕,用户正常操作Response,就会解除StreamAllocation和Connection的这种关系,这样Connection就会被其它的请求复用。所谓的"用户正常操作Response"包含3个方面:
- 如果响应流中没有数据,用户不需要做任何操作,OkHttp内部会自动解除关系;
- 用户读取完响应流中的所有数据;
- 用户读没有取完响应流中的所有数据,但是调用了Response#close。
如果没有满足上面三个条件,在Call#execute执行完毕用户的调用返回,返回之后正好发生了一次Full GC,分配给该Call的StreamAllocation被回收,此时由于StreamAllocation和Connection的关系没有被解除。Connection中还包含StreamAllocation的弱引用对应的对象,此时Connection不能被其它请求复用,这样就发生了StreamAllocation泄露。
一个栗子
对两个服务发起http请求,第一个请求由响应body,但是请求完之后用户不会读取相应body的数据也不会调用response#close,马上调用System.gc(),这样第一个请求就人为的制造了StreamAllocation泄露。下面代码执行的结果为:

public class TestOkGet {
private static OkHttpClient client = new OkHttpClient.Builder()
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build();
static{
Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);
}
public static void main(String[] args) throws InterruptedException, IOException {
String getPath = "http://localhost:7777/TestServlet";
// 在Springboot-test 这个工程中。。。
String getPath1 = "http://localhost:8888/boot/properties/testGet";
okGet(getPath);
// 这个调用有可能触发FullGC也有可能不会触发FullGC,和具体虚拟机的实现有关
// 参考https://www.zhihu.com/question/38551124
// Hotspot虚拟机,在没有配置"-XX:-+DisableExplicitGC"调用System.gc()会马上触发FullGC
System.gc();
Thread.sleep(1000);
okGet(getPath1);
}
public static void okGet(String path) {
Request request = new Request.Builder().url(path).build();
Call call = client.newCall(request);
try {
Response response = call.execute();
System.out.println(response.code());
// System.out.println(response.body().string());
// response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
StreamAllocation泄露检测原理
正常情况下请求正常结束读取完响应body的数据或者调用response#close,会调用StreamAllocation#deallocate来解除StreamAllocation和Connection之间的关系,即将当前的Reference<StreamAllocation>从Connection#allocations中移除。
如果请求完之后没有读取完响应body的数据并且没有调用response#close就不会解除相关关系,这样在ConnectionPool做下次空闲连接检测时,发现该Reference<StreamAllocation>已经没有强引用关联了,就会报出StreamAllocation泄漏的警告。具体逻辑是在:ConnectioPool#pruneAndGetAllocationCount这个方法中。这也是为什么上面的代码要发送两次Http请求,第二次发起Http请求的目的只有一个,那就是促使ConnectionPool做一次空闲检测。