Java 14中的 JEP 358: Helpful NullPointerExceptions是如何实现的

Java 14在2020年3月17号正式发布,不久AdoptOpenJDK也跟进发布了新版。

JEP 358: Helpful NullPointerExceptions

其中对使用者比较有用的功能是 JEP 358: Helpful NullPointerExceptions,即在NullPointerException的message中指明为什么产生了这个NPE。

示例代码如下(需要Java14,并配置参数-XX:+ShowCodeDetailsInExceptionMessages):

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
public class Main {
static class Class1 {
Object object;
}
static class Class2 {
Class1 class1;
public Class1 getClass1() {
return class1;
}
}
static Class2 class2;
private static Object get(int i) {
return null;
}
// 打印NPE的message
private static void printNPE(Runnable runnable) {
try {
runnable.run();
} catch (NullPointerException e) {
System.err.println(e.getMessage());
}
}
public static void main(String[] args) {
Class2 class21 = null;
printNPE(() -> {
// 1. getstatic为null
// Cannot invoke "Main$Class2.getClass1()" because "Main.class2" is null
System.out.println(class2.getClass1().object.toString());
});
Class2 class22 = new Class2();
printNPE(() -> {
// 2. 调用函数结果为null
// Cannot read field "object" because the return value of "Main$Class2.getClass1()" is null
System.out.println(class22.getClass1().object.toString());
});
class22.class1 = new Class1();
printNPE(() -> {
// 3. 函数调用后,getfield为null
// Cannot invoke "Object.toString()" because "Main$Class2.getClass1().object" is null
System.out.println(class22.getClass1().object.toString());
});
printNPE(() -> {
// 4. 链式getfield
// Cannot invoke "Object.toString()" because "class22.class1.object" is null
System.out.println(class22.class1.object.toString());
});
Integer[] ary1 = null;
printNPE(() -> {
// 5. 对null执行aaload
// Cannot load from object array because "ary1" is null
System.out.println(ary1[0].intValue());
});
Integer[] ary2 = new Integer[1];
printNPE(() -> {
// 6. iconst_0+aaload后为null
// Cannot invoke "java.lang.Integer.intValue()" because "ary2[0]" is null
System.out.println(ary2[0].intValue());
});
printNPE(() -> {
int i = 0;
// 7. iconst_0+iload_1后为null
// Cannot invoke "java.lang.Integer.intValue()" because "ary2[i]" is null
System.out.println(ary2[i].intValue());
});
printNPE(() -> {
int i = 0;
// 8. iconst_0+iadd后为null
// Cannot invoke "java.lang.Integer.intValue()" because "ary2[...]" is null
System.out.println(ary2[i + 0].intValue());
});
printNPE(() -> {
// 9. invokevirtual结果为null
// Cannot invoke "Object.toString()" because the return value of "Main.get(int)" is null
System.out.println(Main.get(1).toString());
});
}
}

令我比较意外的是情况4和情况7,Java能够追溯链式回调,而且连变量名都带上了!

JEP 358实现细节

那我们就看下这么神奇的特性是如何实现的吧。

首先,这个功能需要添加参数-XX:+ShowCodeDetailsInExceptionMessages才能开启,那我们只要在JDK的代码中搜索这个字符串就行。于是找到给NPE填充Message的代码:

阅读更多

不正确使用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这样的。

阅读更多

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
阅读更多

StringBuffer,StringBuilder以及String

今天在网上闲逛,看见 @姚冬 的一个回答

他提到的问题也很有深度,然后思考了下,想评论来着。然而评论区太小,写不下,所以单独写在这儿。

基本上可以当作快问快答来读…

为什么java中的string不以\0结尾?

  • \0结尾在很大程度上要求程序员写规范的代码,如果写出了不规范的代码,那么很容易就内存越界了。
  • 另外,string的内部存储是char[],而为了内存安全,java数组本来就有一个length属性,这时以\0结尾就是一个多余的设计了。
  • String的内部存储也只能是char[]了,如果是其他的方式,比如通过native内部放一个c风格的数组,那么java代码中的char[]和string的转换就要很多内存拷贝操作了。
  • 而C语言设计成\0结尾,是为了减少抽象层,让C语言更加贴近硬件

(在语言设计中,)字符串的长度放哪里,放到起始指针的位置,还是起始指针的前面 ?

  • Java中,String的length也就是数组的length,JLS也只是说明了arraylength字节码,没有规定如何实现
  • 不过Hot Spot的实现是,先元数据,再长度,再具体的内容(比如char[])

如果放前面,那么字符串起始指针和内存块起始不一致怎么解决

Java不存在这个问题,我觉得。元数据和length字段都在实际数组之前呢。Java中,访问任何对象之前都要再多一次跳转,跳过元数据(和length)。

字符串拼接的时候把源串复制到目标串结尾,那么目标串剩余内存不够怎么办,重新分配要多一次赋值,频繁拼接性能有问题怎么办

阅读更多

vscjava.vscode-java-debug 0.18.0的新特性!

微软为VSCode开发了一个Java调试器 Debugger for Java。之前用这个很不爽,还和微软的人吐槽过VSCode在debug java的时候,只能看到HashMap等java自带数据结构的物理视图,比如一个HashMap,在 0.17.0 版本下debug时,是这样的:

HashMap里面有很多实现的细节,但是一般在debug的时候,我们比较关注的是这个HashMap里面存储了哪些东西等,而不是这种具体实现的细节。

然后0.18.0就实现了HashMap的逻辑视图,就是只查看数据,而不查看实现的视图:

在调试的时候,可以很方便的查看容器数据类型内的数据。

然后再仔细看了下Debugger for Java 的Changelog,发现还有一个比较有用的更新:

Add the source hyperlinks for the stack traces in the Debug Console output.

比如异常打印的StackTrace,在0.17.0是这样的:

阅读更多

为何一次请求会有两次HttpServlet:service调用?

今天看了下阿里出的 Arthas使用文档中有问到:

为什么只访问了http://localhost:8080/a.txt,但Arthas的trace命令打印出了两个请求树?

然后我去自己试了下,发现还真的是这个情况,trace日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
`---ts=2019-02-24 18:05:20;thread_name=http-nio-8080-exec-1;id=15;is_daemon=true;priority=5;TCCL=org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedWebappClassLoader@6601a0f8
`---[22.125552ms] javax.servlet.http.HttpServlet:service()
`---[22.048005ms] javax.servlet.http.HttpServlet:service()
`---[21.977486ms] org.springframework.web.servlet.FrameworkServlet:service()
+---[0.015829ms] javax.servlet.http.HttpServletRequest:getMethod()
+---[0.023379ms] org.springframework.http.HttpMethod:resolve()
`---[21.847595ms] org.springframework.web.servlet.HttpServletBean:service()
`---……

`---ts=2019-02-24 18:05:20;thread_name=http-nio-8080-exec-1;id=15;is_daemon=true;priority=5;TCCL=org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedWebappClassLoader@6601a0f8
`---[108.691709ms] javax.servlet.http.HttpServlet:service()
`---[108.658563ms] javax.servlet.http.HttpServlet:service()
`---[108.612929ms] org.springframework.web.servlet.FrameworkServlet:service()
+---[0.024764ms] javax.servlet.http.HttpServletRequest:getMethod()
+---[0.004569ms] org.springframework.http.HttpMethod:resolve()
`---[108.540682ms] org.springframework.web.servlet.HttpServletBean:service()
`---……

然后,过去翻了一下代码才发现,是org.apache.catalina.core.StandardHostValve#invoke方法的逻辑:

1
2
3
4
5
6
7
8
9
10
//正常的处理请求(这是第一个HttpServlet:service调用)  
context.getPipeline().getFirst().invoke(request, response);
// 如果response是失败的,那么再处理ErrorPage的逻辑
if (response.isErrorReportRequired()) {
if (t != null) {
throwable(request, response, t);
} else {
status(request, response);
}
}

在status方法中,获取ErrorPage,然后给request设置javax.servlet.error.*的属性,然后再forward到HttpServlet:service。这儿就会出现第二个HttpServlet:service请求。

那么问题来了,这个行为是规范里面有约定吗?

翻了翻 Java Servlet Specification,第10.9.2小节有说:

To allow developers to customize the appearance of content returned to a Web client when a servlet generates an error, the deployment descriptor defines a list of error page descriptions. The syntax allows the configuration of resources to be returned by the container either when a servlet or filter calls sendError on the response for specific status codes, or if the servlet generates an exception or error that propagates to the container.

If the sendError method is called on the response, the container consults the list of error page declarations for the Web application that use the status-code syntax and attempts a match. If there is a match, the container returns the resource as indicated by the location entry.

The Web application may have declared error pages using the exception-typeelement. In this case the container matches the exception type by comparing the exception thrown with the list of error-page definitions that use the exception-typeelement. A match results in the container returning the resource indicated in the location entry. The closest match in the class hierarchy wins.

阅读更多

使用repox搭建sbt/maven镜像

最近sbt的速度实在是不能忍受了,所以使用repox搭建了sbt镜像。 搭建过程没有什么好说的,直接sbt assembly,然后

java -Xmx512m -jar target/scala-2.11/repox-assembly-0.1-SNAPSHOT.jar

就好了。


但是发现了两个repox的问题:

  1. 下载文件时,服务器全部下载完后,才能传输给sbt(不支持nginx那种“流式代理”),这在下载大文件时尤其明显。
  2. 有的时候,pom文件总是404 比如curl https://repo1.maven.org/maven2/org/w3c/css/sac/1.3/sac-1.3.pom -I的结果是200,curl http://106.75.27.110:8078/org/w3c/css/sac/1.3/sac-1.3.pom -I的结果就是404,重试好几次都不行;重启repox就好了。
阅读更多