JVM crashes at libjvm.so?

软件工程师、主攻高级编程语言虚拟机的设计与实现

73 👍 / 8 💬

问题描述

本人设计两个Java程序,程序A是设计一个自定义类加载器,负责编译以及加载类似hello world的一个类,程序B使用javaagent与attach机制,连接到程序A,获取程序A中已经加载的应用类,并对其进行hash。首先启动程序A,当程序B附接到程序A时,A进程就发生crash,在程序A所在目录中出现了hs_err_pid6101.log文件,然而使用AppClassLoader加载hello world类,却不会出现该问题,请问这是怎么一回事?谢谢
环境:linuxmint-17.3-cinnamon-64bit,jdk1.8.0_60
下图为进程A crash后输出的信息:
下图为进程B输出的信息:

// 以下为agentmain的主体部分
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class Agent {
	public static void agentmain(String agentArgs, Instrumentation inst) {
		System.out.println("Start Instrumentation");

		Transformer tf = new Transformer();
		inst.addTransformer(tf, true);

		for (Class<?> c : inst.getAllLoadedClasses()) {
			try {
				if (inst.isModifiableClass(c)) {
				   inst.retransformClasses(c);
				}
			} catch (UnmodifiableClassException e) {
				e.printStackTrace();
			} catch (Exception e) {
				System.out.println("<Exception ");
			} catch (Error e) {
				System.out.println("<Error ");
			}
		}

		inst.removeTransformer(tf);
		
		System.out.println("End Instrumentation");
	}
}
以下为程序B的主体部分
		if (args.length != 1) {
			return;
		}
		System.out.println("It begins...");
		VirtualMachine vm = null;
		try {
			vm = VirtualMachine.attach(args[0]);
			vm.loadAgent(new File("." + File.separator + "AgentMain.jar").getCanonicalPath());
			vm.detach();
		} catch (Exception e) {
			e.printStackTrace();
		}
		System.out.println("It is over and exits.");

详细信息可以参照链接里的hs_err_pid6101.log文件与core文件:pan.baidu.com/s/1boz1gK
经过调试,发现是在对“java.lang.invoke.LambdaForm$BMH/791452441”这个类进行retransform的时候出现的错误,可是它的Class对象是Modifiable,这到底是这么一回事?另外,该类是如何产生的,它不在rt.jar包里面,望R大等大牛释惑,谢谢。

哈哈哈题主撞坑上了。这是题主用的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的缩写)

这里就提几个要点:

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。遇到它请绕道走。

以上~