2018年4月

java 集合类占用内存空间的自动扩展

昨天突然遇到一个很奇怪的案例:
一个 JVM, 它使用 CMS GC 算法, 有2G左右内存, 老年代大概1.4G, 年轻代 700M 左右, 在事故之前, 一直都是很正常的年轻代快满的时候, 做年轻代 ParNew GC, 同时老年代过一段时间就做一次 CMS 并发 GC, 每次 CMS GC 回收之后, 老年代大概能有600M 的空间被 free. 这样美妙的状态一直持续到某个时间点, 突然 JVM 狂做 full GC. CPU 飙升到100%, GC overhead 基本到100%.

观察:
首先查看了 gc verbose GC, 通过 log, 我们看到在某个时间点上, 年轻代做了一次回收, 年轻代基本回收完, 之后老年代 CMS 做了一次回收, 回收完之后老年代大概有 (1433600K - 951227K) 471M的空余, 永久代也剩余非常多. 然而, 紧接着 JVM 又做了一次 Full GC, 之后就不停的做 full GC. 到底发生了什么, 在系统年轻代, 老年代, 永久代都有很多剩余的情况下, 还不停的触发 full GC?

猜测及求解:
首先猜测, 是不是代码里面的的 System.gc() 方法被触发? 不可能, 没有重启, 没有人改参数, 关键是如果是这种代码触发的 GC, log 里会表明是 System GC. 被否定.
之后终于有同事指明 log 的疑点:
msg=&st=java.lang.OutOfMemoryError: Java heap space
java.util.Arrays.copyOf(Arrays.java:2219)
java.util.Vector.grow(Vector.java:262)
java.util.Vector.ensureCapacityHelper(Vector.java:242)
java.util.Vector.addElement(Vector.java:616)

这个 Vector 在扩展空间的时候, 导致系统一直做 full GC. 为什么呢? 在 heap dump 里面, 我们可以找到这个 Vector, 它包含非常的元素, 其间接引用的空间已经达到380M 之多. 当它需要更多空间的时候, 他要扩展, 根据 Vector 代码, 如果没有设置每次扩展的大小, 默认扩展到原来的2倍, 也就是说, 他这次扩展要760M 的内存空间. 但是年轻代, 老年代都没有这么多空间供它用了, 他们只好不停的做 full GC. 另外一个相关的问题是, 这个 Vector 是一个所有 request 共享的数据结构, 也就是说每个 request 进来之后, 只向里面加元素, 而不做减元素, 即使第一个尝试尝试扩展空间的 request 失败, 之后的每个 request 同样要做一样的扩展, 所以只好 不停的 full GC, 不停的失败....

那么让我们找几个 Java 的集合类, 来看一下他们是如何扩展空间的:
1) java.util.Vector
Vector 内部有个 capacityIncrement 字段, 它只有可能在 Vector 初始化的时候设置, 如果不设置, 默认是0. 当 Vector 现有申请的空间不够用的时候, 它就自动扩展空间, 这时候, 它会首先检查 capacityIncrement 是不是大于0, 如果大于0, 那么就先添加 capacityIncrement 的数量, 如果添加后能达到要求, 就使用这么多, 如果 capacityIncrement 小于或者等于0, 那么久直接 double 现在的已有空间, 如果 double 后还不够, 那么就直接申请 minCapacity 的空间. 当申请到之后, 就直接 copy 内存到新的空间. 它内部基于连续 Array. 所以如果申请到, 基本做一次全 copy.
下面是 API 文档的摘抄:
Vector can grow or shrink as needed to accommodate adding and removing items. Each vector tries to optimize storage management by maintaining a capacity and a capacityIncrement. The capacity is always at least as large as the vector size; it is usually larger because as components are added to the vector, the vector's storage increases in chunks the size of capacityIncrement.

public void ensureCapacity(int minCapacity)
Increases the capacity of this vector, if necessary, to ensure that it can hold at least the number of components specified by the minimum capacity argument.
If the current capacity of this vector is less than minCapacity, then its capacity is increased by replacing its internal data array, kept in the field elementData, with a larger one. The size of the new data array will be the old size plus capacityIncrement, unless the value of capacityIncrement is less than or equal to zero, in which case the new capacity will be twice the old capacity; but if this new size is still smaller than minCapacity, then the new capacity will be minCapacity.

2) java.util.ArrayList
Array 在初始化的时候, 如果你给他一个 initialCapacity 参数, 那么他就根据你的参数初始化, 否则就初始化为空数组. 当增长需要更多空间的时候, 他也没有那么简单粗暴, 他先尝试增长为原来 capacity 的一半, 如果还不够, 才尝试按照你真实需要的大小去申请空间. 当然最多不能超过 Integer 的最大值.
下面是 API 文档的摘抄:
This class is roughly equivalent to Vector, except that it is unsynchronized. Each ArrayList instance has a capacity. The capacity is the size of the array used to store the elements in the list. It is always at least as large as the list size. As elements are added to an ArrayList, its capacity grows automatically

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

3) java.util.LinkedList
LinkedList 没有这个问题, 它是链表结构, 每次增加的时候, 随便申请一块内存, 添加到列表里面.
4) java.util.HashMap
HashMap 在扩展的时候, 在不超过 Integer 最大值的情况下,也是 double 原来的大小, 所以这也是需要注意的;
5) java.util.HashSet
HashSet 是基于 HashMap 来实现的, 所以和 HashMap 情况一样;

================ 当时的 verbose GC log ================
2018-04-18T23:07:26.246-0700: 2545864.810:
[GC2018-04-18T23:07:26.247-0700: 2545864.810:

[ParNew Desired survivor size 41943040 bytes, new threshold 6 (max 6)
  • age 1: 1612856 bytes, 1612856 total
  • age 2: 557552 bytes, 2170408 total
  • age 3: 1147384 bytes, 3317792 total
  • age 4: 446720 bytes, 3764512 total
  • age 5: 1296960 bytes, 5061472 total
  • age 6: 301240 bytes, 5362712 total
    : 662669K->7154K(737280K), 0.0193370 secs]
    1673715K->1018388K(2170880K), 0.0199400 secs]
    [Times: user=0.00 sys=0.00, real=0.02 secs]

2018-04-18T23:07:28.693-0700: 2545867.257:
[GC2018-04-18T23:07:28.694-0700: 2545867.258:

[ParNew Desired survivor size 41943040 bytes, new threshold 6 (max 6)
  • age 1: 867240 bytes, 867240 total
  • age 2: 636096 bytes, 1503336 total
  • age 3: 449408 bytes, 1952744 total
  • age 4: 1144464 bytes, 3097208 total
  • age 5: 415728 bytes, 3512936 total
  • age 6: 1283896 bytes, 4796832 total
    : 169999K->7158K(737280K), 0.0251900 secs]

2018-04-18T23:07:28.719-0700: 2545867.283:
[CMS: 1011528K->951227K(1433600K), 5.1650580 secs] 1181233K->951227K(2170880K),
[CMS Perm : 133656K->128154K(524288K)], 5.1909560 secs]
[Times: user=4.63 sys=0.34, real=5.20 secs]

2018-04-18T23:07:33.885-0700: 2545872.449:
[Full GC2018-04-18T23:07:33.886-0700: 2545872.449:
[CMS: 951227K->945587K(1433600K), 4.6014860 secs] 951227K->945587K(2170880K),
[CMS Perm : 128154K->127985K(524288K)], 4.6019310 secs]
[Times: user=3.80 sys=0.80, real=4.60 secs]

2018-04-18T23:07:38.625-0700: 2545877.189:
[GC2018-04-18T23:07:38.626-0700: 2545877.189:
[ParNew Desired survivor size 41943040 bytes, new threshold 6 (max 6)

  • age 1: 1281456 bytes, 1281456 total
    : 10358K->4684K(737280K), 0.0182240 secs]

2018-04-18T23:07:38.644-0700: 2545877.208:
[CMS: 945587K->944299K(1433600K), 4.1096320 secs] 955945K->944299K(2170880K),
[CMS Perm : 127985K->127985K(524288K)], 4.1284360 secs]
[Times: user=6.46 sys=0.01, real=4.12 secs]

2018-04-18T23:07:42.754-0700: 2545881.318:
[Full GC2018-04-18T23:07:42.754-0700: 2545881.318:
[CMS: 944299K->944012K(1433600K), 4.0007380 secs] 944299K->944012K(2170880K),
[CMS Perm : 127985K->127985K(524288K)], 4.0011340 secs]
[Times: user=3.32 sys=0.00, real=4.01 secs]

2018-04-18T23:07:46.788-0700: 2545885.352:
[GC2018-04-18T23:07:46.788-0700: 2545885.352:
[ParNew Desired survivor size 41943040 bytes, new threshold 6 (max 6)

  • age 1: 4923328 bytes, 4923328 total
    : 20718K->5722K(737280K), 0.0165210 secs]

2018-04-18T23:07:46.805-0700: 2545885.368:
[CMS: 944012K->948245K(1433600K), 4.0567560 secs] 964730K->948245K(2170880K),
[CMS Perm : 127985K->127985K(524288K)], 4.0738820 secs]
[Times: user=3.46 sys=0.00, real=4.07 secs]

2018-04-18T23:07:50.862-0700: 2545889.426:
[Full GC2018-04-18T23:07:50.862-0700: 2545889.426:
[CMS: 948245K->947961K(1433600K), 3.9952950 secs] 948245K->947961K(2170880K),
[CMS Perm : 127985K->127985K(524288K)], 3.9956870 secs]
[Times: user=3.31 sys=0.00, real=4.00 secs]

关于 代理服务器

如果一个 web 请求走代理服务器, 那么 client 端是直接和 proxy 建立连接还是和 server 端建立连接?

有代理服务器的话, 代理服务器是如何影响 read timeout 和 connection timeout 的?

关于 http cert

JDK 有2个环境变量控制 SSL/TLS:
-Dhttps.protocols=TLSv1,TLSv1.1,TLSv1.2
-Djdk.tls.client.protocols=TLSv1.1,TLSv1.2

如果一个 vip 背后有2个 IP, 两个 IP 的 cert 不一样, 会产生什么后果?

如果一个 vip 背后有2个 IP, 两个 IP 的 cert 支持的 SSL/TLS 不一样, 会产生什么后果?

在一个正在被使用的 VIP 上 突然更新一个 cert 会产生什么后果?

在一个正在被使用的 VIP 上 突然更新一个 cert 让它支持最新的 TLS 1.2 会产生什么后果?

关于 Tomcat HTTP connector 的具体实现及使用

以 tomcat 8 为例, 有4个 http protocol 实现:

  1. org.apache.coyote.http11.Http11Protocol - blocking Java connector
  2. org.apache.coyote.http11.Http11NioProtocol - non blocking Java NIO connector
  3. org.apache.coyote.http11.Http11Nio2Protocol - non blocking Java NIO2 connector
  4. org.apache.coyote.http11.Http11AprProtocol - the APR/native connector.

如果配置里面没有设置这个 protocol 的值, 是有个自动选择机制:

Sets the protocol to handle incoming traffic. The default value is
HTTP/1.1 which uses an auto-switching mechanism to select either a non
blocking Java NIO based connector or an APR/native based connector. If
the PATH (Windows) or LD_LIBRARY_PATH (on most unix systems)
environment variables contain the Tomcat native library, the
APR/native connector will be used. If the native library cannot be
found, the non blocking Java based connector will be used. Note that
the APR/native connector has different settings for HTTPS than the
Java connectors.

nio 和 nio2 有什么区别?

Apr/native 是什么?

refer:
https://tomcat.apache.org/tomcat-8.0-doc/config/http.html#Connector_Comparison

关于 http 1.1 的 Keep-Alive 的持久连接 以及客户端和服务器端的处理

关于 http 1.1 的 Keep-Alive 有以下几点要注意:

  1. default 行为
  2. 可设置timeout 和 max request 数: Keep-Alive: timeout=5, max=1000

对于Java 无论是BIO 还是NIO,客户端和服务端都可以采用一定的策略来关闭这个http 连接。另外TCP层有半连接概念,Http层没有,要么关掉,要么没关。

那么若是 Keep-Alive 的长连接, 如何区分一个 request payload 数据是不是已经结束呢?

server端:

以 tomcat8 为例: 在 tomcat8 的代码里面,
org.apache.coyote.http11.Http11Processor 的 prepareRequest() 方法里,
可以看到处理 HTTP header: transfer-encoding 的逻辑: 根据是 identity 或者 chunked
来使用不同的 filter 类来判断 payload 有没有结束, 所以在 servelet 的 service() 或 doPost
方法里, 拿到 inputStream 的时候, 已经不用担心 payload 结束的问题. 具体的 Filter 类在 package
org.apache.coyote.http11.filters里面, 比如 处理 input 的就有:
ChunkedInputFilter, IdentityInputFilter.

client 端

以 apache httpClient 5 为例: 因为 Client 端不像服务器端有过滤处理, 所以 client 端要自己处理,
所以在 httpClient 的代码里面, 就有专门处理 payload 是否结束的代码. 有个类:
DefaultContentLengthStrategy, 它有个方法 determineLength(final HttpMessage
message), 用来判断是使用 chunked 还是 content-length. 然后根据这个返回结果,
使用不同的解码器或者使用不同的 Stream 来封装 payload. stream 如:
org.apache.hc.core5.http.impl.io.ChunkedInputStream 和
org.apache.hc.core5.http.impl.io.IdentityInputStream; 解码器如:
org.apache.hc.core5.http.impl.nio.LengthDelimitedDecoder 和
org.apache.hc.core5.http.impl.nio.ChunkDecoder