JVM crashes at libjvm.so?

本人设计两个Java程序,程序A是设计一个自定义类加载器,负责编译以及加载类似hello world的一个类,程序B使用javaagent与attac…
关注者
60
被浏览
10,923

1 个回答

哈哈哈题主撞坑上了。这是题主用的Oracle JDK8u60上没有修的一个非常坑爹的问题。

这个问题是跟VM anonymous class与retransform class的不相容而造成的。

注意:这是Oracle JDK / OpenJDK的HotSpot VM的一个实现细节,并不是Java SE规范所要求的行为。


VM anonymous class是Oracle JDK 7 / OpenJDK 7开始,HotSpot VM为了支持JSR 292而添加的一项内部功能。请参考John Rose老大的博文介绍:

anonymous classes in the VM (John Rose @ Oracle)

(跑个题:.NET的程序员可能听说过.NET上有一种叫做Lightweight Code Generation,简称LCG的功能。它允许.NET上的程序动态创建出不依附于任何类型的自由方法,主要用于支持动态生成并执行MSIL片段的需求。例如说.NET 3.5的LINQ Expression Tree的 Expression.Compile() 就会通过LCG把表达式树编译为一段自由的、不依附于任何宿主类型的MSIL,然后就可以交由CLR来执行。

HotSpot VM的这个VM anonymous class最本质的目的其实就是为了做LCG,但做成了一个很奇怪的东西。

结果大家在讨论Java 10的VM功能的时候,真正轻量级的LCG又被摆上议程了——跟CLR所支持的LCG类似的设计。汗)

这VM anonymous class大家如果不自己利用JSR 292来实现动态语言的话,很少会直接用到,所以这里就不展开多说。

Java 8开始,Oracle JDK 8 / OpenJDK 8结合使用invokedynamic与VM anonymous class来实现Java语言层面的lambda表达式。题主所看到的那个“java.lang.invoke.LambdaForm$BMH/791452441”类就是一个VM anonymous class,是HotSpot VM上invokedynamic / MethodHandle的一点实现细节。

(BMH是Bound Method Handle的缩写)

这里就提几个要点:

  • 它可以看作一种模版机制,如果程序要动态生成很多结构相同、只是若干常量不同的类的话,可以先创建出一个包含占位符常量的正常类作为模版,然后利用 sun.misc.Unsafe.defineAnonymousClass() 方法,传入该类以及一个作为“constant pool patch”的数组来替换指定的常量为任意新值,结果得到的就是一个替换了新常量的VM anonymous class;
  • VM anonymous class从VM的角度看是真正“没有名字”的,在构造出来之后只能通过 Unsafe.defineAnonymousClass() 所返回出来的 java.lang.Class 对象来反射操作;
  • VM anonymous class可以指定一个host class,以便拥有跟host class一样的权限(可见性等);
  • VM anonymous class不显式挂在任何ClassLoader下面。它可以很方便地被GC回收:只要该类没有任何活着的实例,并且没有任何活着的强引用指向代表该类的 java.lang.Class 对象,则该类可以被回收;
    • 而一般的类则需要牵扯到更多东西,例如说要等类的ClassLoader(defining class loader)以及该ClassLoader所加载的所有类都死了才可以回收。
  • VM anonymous class 与 Java语言层面的匿名内部类(anonymous inner class)是完全不同、不相关的概念。后者只是在Java语言层面“没有名字”而已,对VM而言它还是完全正常、普通、有名字的类(名字是由Java源码编译器(例如javac)构造出来的)。
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);


上面介绍中提到的“constant pool patch”是一个Object[]。模版类中任何标记为CONSTANT_String 类型的常量都可以被替换为任意Java对象

对,您没看错。一个VM anonymous class的运行时常量池里,标记为CONSTANT_String 类型的常量,既可能真的引用着一个 java.lang.String 实例,也可能引用着经由那个Object[]数组传入的任意Java对象。这种指向非 String 类型的 CONSTANT_String 常量项,在HotSpot VM里面叫做“pseudo string”类型的常量项。

关系到题主所遇到的问题的大背景,请阅读OpenJDK邮件列表上的这串讨论:

ClassFileTransformer does not apply to anonymous classes

特别是John Rose老大的这个回复:

ClassFileTransformer does not apply to anonymous classes

问题就在于:在JVMTI或者Java agent要求retransform一个以及被加载好的类的时候,JVM要把运行时已经加载好的类重新写成符合Class文件格式的一个byte[]数组。但如果要被retransform的类是一个VM anonymous class,里面的“pseudo string”类型的常量项却不能按照普通Class文件的格式写出——它根本不符合普通 CONSTANT_String 常量项的意义和结构。

所以本质上说 VM anonymous class是不应该允许做retransform操作的——Instrumentation.isModifiableClass()遇到这种类应该返回false才合理。

============================================

OpenJDK里有这么一个相关的bug:

JDK-8008678: JSR 292: constant pool reconstitution must support pseudo strings

这个bug想解决的就是上面说的VM anonymous class与retransform的不相容问题。它的思路大概说就是:既然已经patch过的constant pool没办法按照正常的Class文件格式写出,那能不能把原本占位用的 CONSTANT_String 的内容给写出去呢?

有了这个实现之后,至少对VM anonymous class做retransform不会crash了。但这样retransform得到的Class文件内容却也不能反映原本的VM anonymous class所能引用的丰富的常量,而必须有别的方式把这些常量传出来。

这个bug只有在Oracle JDK 9 / OpenJDK 9上有fix,没有backport到JDK8上。所以题主如果想靠升级到JDK8的更加新的版本来避开这个crash,看来是木有指望…

相关的还有这个bug:

JDK-8158475: JVMTI RedefineClasses doesn't handle anonymous classes properly

同样只在JDK9版有fix,没有backport到JDK8u。

============================================

所以要解决问题,目前还是得靠自己写点代码来绕开它。

关键点就是:如果要retransform一个Class,一定要先检查清楚它是不是一个VM anonymous class,如果是的话就不要对它做retransform。

可是怎么判断一个Class是不是VM anonymous class呢?

Class (Java Platform SE 8 )

<- 不要试图用 java.lang.Class.isAnonymousClass() 方法来判断VM anonymous class。这个 Class.isAnonymousClass() 方法是用于判断Java语言层面意义上的匿名内部类用的。前面提到了,VM anonymous class与Java层面的匿名内部类是完全无关的东西。

由于VM anonymous class是HotSpot VM的实现细节,而不是Java SE的标准功能,所以在Java SE的API里其实是没有任何方法可以判断一个Class是不是VM anonymous class的。

所以这里只能曲线救国了:HotSpot VM在回答 Class.getName() 查询时,对于VM anonymous class,会使用下述格式的名字:

<class name>/<identity hash code>

用题主给的例子:

java.lang.invoke.LambdaForm$BMH/791452441

其中的斜杠“/”以及后面的数字就是这个格式的一部分。

注意:“/”不是一个合法类名可以使用的字符,所以在正常的类的名字中是不会有这个字符的。

所以在HotSpot VM上运行Java程序,如果发现类名以“/”和一串数字结尾,就可以判断它是一个VM anonymous class。遇到它请绕道走。

以上~