jdk 8u91一个lambda类型推断的bug

公司内部发布maven包是在公司构建服务器上编译的。最近一次上线,发现同样的代码,在本地的jdk8上能编译通过,但是在构建服务器上,就编译报错。

简化后的代码如下:

JavaBugTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.Optional;

public class JavaBugTest {

public static <T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E {
return retryCallback.doWithRetry(new Object());
}

public static void main(String[] args) {
Optional.ofNullable(execute((param) -> new Object()))
.ifPresent(s -> System.out.println(s));
}
}
RetryCallback.java
1
2
3
public interface RetryCallback<T, E extends Throwable> {
T doWithRetry(Object var1) throws E;
}

用maven编译报错如下:

1
JavaBugTest.java:[10,36] unreported exception E; must be caught or declared to be thrown

最后发现,是构建服务器jdk版本太老导致的。

二分法尝试了几个版本,发现8u91不行,但是8u92就能编译通过了。

于是开始调试javac代码,发现两个版本的AST不一样:

阅读更多

HashMap的初始容量设置

先说结论

知道大小的情况下,new HashMap的时候这么写:

1
HashMap<Integer, String> map = Maps.newHashMapWithExpectedSize(expectedSize);

正文

Java中的HashMap大家都很熟悉,其底层使用了Node数组来存储Map中的数据。但是如果存储的数据太多,空间不够,就需要扩容这个数组来存储新的数据了。

扩容的实现可以看下java.util.HashMap#resize函数,基本上就是将数组中的内容逐个复制到新数组中。

扩容操作的时间复杂度是O(n),空间复杂度是O(n),还需要计算对象的hash。在平常编码中,如果我们提前知道map的大小,就应该指定初始容量,避免发生扩容。

《阿里巴巴开发手册》中也有这么一条建议:

于是,很多开发者(包括刚开始的我),就这么写:

阅读更多

如何以string方式查看heapdump中的byte数组

昨天,线上OOM,dump下来hprof文件,里面有两个大数组:

从表象上来看,和thrift导致的oom是一样的:

但是问题是,这种情况是怎么出现的呢?

找了好几种办法,没有头绪。

最后发现,把这个byte数组转成string就看到了thrift服务端的错误信息。

当时为了快速解决问题,是直接将前60个byte手抄到java代码中,然后转成string输出。

但是,不能一直都这么干,所以就看了下如何方便的将heapdump中的byte[]输出为string。

查了半天,发现OQL没有这样的功能,但是VisualVM倒是可以间接的做这事:

阅读更多

不正确使用Thrift Client导致的OOM问题排查

最近线上有一个多线程的任务,会调用几个Thrift服务。 上线后观察到这个脚本在执行一段时间后,会有好几次Full GC,然后就会报OOM错误。

那就先下载heap dump(推荐压缩后,使用rz下载到本地),使用VisualVM分析。首先切换到Objects页面,看下是否有大对象:

heapdump-Objects

可以看到,有两个byte数组占用了大量内存,也可以看到这个对象是在Java栈上的,接下来就是要找谁在使用这个变量。

右击该对象,点击Select in Threads:

可以看到是名为rebuilder-9的线程,再查看这个线程的调用栈:

再结合readStringBody的代码:

阅读更多

lambda表达式导致arthas无法redefine的问题

作为一个从PHP转Java的人,发现alibaba的arthas很好用。通过arthas的redefine命令,可以像PHP一样,不用重新发布,就可以改变程序行为(前提是不改变类结构,不改变方法签名)。

但是用多了,发现很多时候,我们就改了几行代码,甚至有的时候就添加了一行日志,就无法redefine了。提示

redefine error! java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method

它提示我们新增加方法,那我们就看看是不是新增加了方法。通过javap来查看定义的方法:

老的类:

新的类:

对比之后发现,新的类,即本地编译的类,其中的lambda对应的方法名都是lambda$getAllCity$0这样的。

阅读更多

从fastjson漏洞谈防御式编程

最近,fastjson又爆出一个漏洞,在解析特殊字符的时候,直接OOM:

首先分析一下整体流程:

在scanString时,会直接读取两个字符:

而在next方法中,每次读取都会将bp的值加一(即使没有从输入中读取字符):

1
2
3
4
5
6
public final char next() {
int index = ++bp;
return ch = (index >= this.len ? //
EOI //
: text.charAt(index));
}

在处理完x之后,继续解析剩下的字符。由于没有更多字符了,所以读到的总是EOI,然后进入如下分支:

1
2
3
4
5
6
7
if (ch == EOI) {
if (!isEOF()) {
putChar((char) EOI);
continue;
}
throw new JSONException("unclosed string : " + ch);
}

本来到这一步,isEOF应该是true了,但是isEOF是这样的:

阅读更多

AsyncHttpClient对Cookie的控制太不灵活了

业务上遇到一个坑,java服务代理了一个接口到upstream,原样转发请求数据和头部。但是代理之后的结果总是莫名其妙的多了一个Cookie,比如是Set-Cookie: ticket=t1

业务上用一个静态的AsyncHttpClient来做代理,也没有做特殊处理,基本上就是如下的代码逻辑:

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
import org.asynchttpclient.*;

import java.io.IOException;
import java.util.concurrent.ExecutionException;

class Main {
private static AsyncHttpClient httpClient;

static {
DefaultAsyncHttpClientConfig.Builder builder = new DefaultAsyncHttpClientConfig.Builder();

httpClient = new DefaultAsyncHttpClient(builder.build());
}

public static void main(String[] args) throws ExecutionException, InterruptedException, IOException {
BoundRequestBuilder builder = httpClient.prepareGet(
"https://httpbin.org/cookies/set/ticket/val1"
);
builder.resetCookies();
builder.execute().get();

BoundRequestBuilder builder2 = httpClient.prepareGet(
"https://httpbin.org/cookies"
);
builder2.resetCookies();
Response res2 = builder2.execute().get();
System.out.println(res2.getResponseBody());
}
}

当时为了防止Cookie问题,特意加上了resetCookies。

首先是查看ticket Cookie的来源,发现upstream在客户端请求带上ticket Cookie的时候,会返回Set-Cookie: ticket=<val> 这个应该就是多余Cookie的来源了。

但是,即使客户端不带Cookie,java服务这边也会返回Set-Cookie字段。这个问题,排查之后发现问题在于resetCookies只能reset本次请求的Cookie,而客户端的Cookie,则不能清除。

即,某次请求,upstream返回了Set-Cookie: ticket=val,那么,以后的代理请求中,都会带上这个Cookie,那么最终用户也会拿到Set-Cookie字段……

从上述代码的运行结果也可以看出:

1
2
3
4
5
{
"cookies": {
"ticket": "val1"
}
}

即,async-http-client没有一个request级别的Cookie控制,只能全局控制Cookie存储。这个问题也有人反馈给了async-http-client

阅读更多

Java中的SPI机制

SPI 全称为 (Service Provider Interface) ,是Java 1.6之后内置的一种服务提供发现机制。SPI可以通过配置来替换服务(或者说interface)的实现;比如java.sql.Driver接口,可以很轻松的从MySQL切换到MongoDB实现。

问题的核心在于,如何根据interface查找对应的实现。

SPI的实现

Java 1.6中,开发者只需要在META-INF/services下添加文件,即可切换、修改对应interface的实现。文件名为实现的接口,文件内容为N个实现的类名,按行分隔。比如:

1
2
3
$ cat src/main/resources/META-INF/services/com.robberphex.Plugin 
com.robberphex.impl.PluginImpl1
com.robberphex.impl.PluginImpl2

使用起来,主要就是ServiceLoader类

1
2
3
4
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
for (Plugin plugin : loader) {
System.out.println(plugin.getClass().getCanonicalName());
}

要注意的是,ServiceLoader#load方法是CallerSensitive的,即load的时候用的是调用者的类加载器。

Java 9

Java 9之后,由于模块化的引入,所以SPI机制也做了扩展:除了原有的通过META-INF/services来注册服务外,还可以通过module中的provides…with语句来注册服务:

阅读更多

Java的类/实例初始化过程

昨天看到群里面有人分享了一道题目,我答错了,于是趁机了解了下Java的类/对象初始化过程:

程序的输出见文章最后。

程序A主要考察的是类实例初始化。简单验证了下,类实例初始化过程如下:

  • 父类实例初始化
  • 构造块/变量初始化(按照文本顺序执行)
  • 构造函数

程序B考察的则是类初始化。类初始化的过程如下:

  • 父类初始化
  • static变量初始化/static块(按照文本顺序执行)

但是我们必须做到面向接口编程,而不是面向实现编程(Program to an ‘interface’, not an ‘implementation’)。

于是就得看看Java Language Specification是如何规定的。其中类初始化过程如下:

  1. 每个类都有一个初始化锁LC,进程获取LC(如果没有获取到,就一直等待)
  2. 如果C正在被其他线程初始化,释放LC并等待C初始化完成
  3. 如果C正在被本线程初始化,即_递归初始化_,释放LC
  4. 如果C已经被初始化了,释放LC
  5. 如果C处于erroneous状态,释放LC并抛出异常NoClassDefFoundError
  6. 否则,将C标记为正在被本线程初始化,释放LC;然后,初始化那些final且为基础类型的类成员变量
  7. 初始化C的父类SC和各个接口SI_n(按照implements子句中的顺序来) ;如果SC或SIn初始化过程中抛出异常,则获取LC,将C标记为erroneous,并通知所有线程,然后释放LC,然后再抛出同样的异常。
  8. 从classloader处获取assertion是否被打开
  9. 接下来,按照文本顺序执行类变量初始化和静态代码块,或接口的字段初始化,把它们当作是一个个单独的代码块。
  10. 如果执行正常,获取LC,标记C为已初始化,并通知所有线程,然后释放LC
  11. 否则,如果抛出了异常E。若E不是Error,则以E为参数创建新的异常ExceptionInInitializerError作为E。如果因为OutOfMemoryError导致无法创建ExceptionInInitializerError,则将OutOfMemoryError作为E。
  12. 获取LC,将C标记为erroneous,通知所有等待的线程,释放LC,并抛出异常E。
阅读更多

JVM如何获取当前容器的资源限制

本文是《容器中的Java》系列文章之 1/n ,欢迎关注后续连载 :) 。

最近同事说到Java的 ParallelGCThreads 参数,我翻了下jdk8的代码,发现 ParallelGCThreads 的参数默认值如下:

  • 如果cpu核心数目少于等于8,则GC线程数量和CPU数一致
  • 如果cpu核心数大于8,则前8个核,每个核心对应一个GC线;其他核,每8个核对应5个GC线程

但是被提醒,发现即使在分配4核的容器上,GC线程数也为38。然后就想到应该和容器的资源限制有关——jvm可能无法觉察到当前容器的资源限制。

翻了下代码,发现最新版本的java是能感知容器的资源限制的,就按照jdk版本再翻了下代码:

线上的jdk(jdk8u144)

写一个sleep 1000s的程序,用于查看JVM的线程数量:

1
./jdk1.8.0_144/bin/java -XX:+UseG1GC -XX:+ParallelRefProcEnabled Main 

然后查看GC线程数目:

1
2
$ jstack $pid | grep 'Parallel GC Threads' | wc -l
38
阅读更多