在我的代理中,我正在拦截monitorEnter
个事件。到目前为止,拦截器功能什么都不做,只能立即返回。由于我遇到了一些重大的性能影响,因此我试图找出问题所在。
我目前的理解是,修改后的字节码可以工作,但是JIT编译时遇到问题。我不知道为什么,什么是解决该问题的最佳方法。
拦截所有monitorEnter
的幼稚方法是DUP
监视程序,先执行monitorenter
,然后对通过监视器对象的拦截器执行INVOKESTATIC
。这样做有时会导致IllegalMonitorStateException
。 (不确定为什么)。然后,我将拦截器的代码序列更改为monitorEnter
,ALOAD
和INVOKESTATIC
。虽然我没有再次遇到异常,但是生成的代码无法进行JIT编译(实际上DUP版本也无法)。
这里是引起问题的方法的示例字节码(类com.mysql.jdbc.ResultSetImpl
)。我添加的唯一代码是12和13处的说明:
protected final void checkColumnBounds(int) throws java.sql.SQLException;
descriptor: (I)V
flags: ACC_PROTECTED, ACC_FINAL
Code:
stack=5, locals=4, args_size=2
0: aload_0
1: invokevirtual #319 // Method checkClosed:()Lcom/mysql/jdbc/MySQLConnection;
4: invokeinterface #323, 1 // InterfaceMethod com/mysql/jdbc/MySQLConnection.getConnectionMutex:()Ljava/lang/Object;
9: dup
10: astore_2
11: monitorenter
12: aload_2
13: invokestatic #329 // Method com/test/bootstrap/Interceptor.monitorEntered:(Ljava/lang/Object;)V
16: iload_1
17: iconst_1
18: if_icmpge 60
21: ldc_w #516 // String ResultSet.Column_Index_out_of_range_low
24: iconst_2
25: anewarray #121 // class java/lang/Object
28: dup
29: iconst_0
30: iload_1
31: invokestatic #470 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
34: aastore
35: dup
36: iconst_1
37: aload_0
38: getfield #236 // Field fields:[Lcom/mysql/jdbc/Field;
41: arraylength
42: invokestatic #470 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
45: aastore
46: invokestatic #519 // Method com/mysql/jdbc/Messages.getString:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
49: ldc_w #521 // String S1009
52: aload_0
53: invokevirtual #506 // Method getExceptionInterceptor:()Lcom/mysql/jdbc/ExceptionInterceptor;
56: invokestatic #512 // Method com/mysql/jdbc/SQLError.createSQLException:(Ljava/lang/String;Ljava/lang/String;Lcom/mysql/jdbc/ExceptionInterceptor;)Ljava/sql/SQLException;
59: athrow
60: iload_1
61: aload_0
62: getfield #236 // Field fields:[Lcom/mysql/jdbc/Field;
65: arraylength
66: if_icmple 108
69: ldc_w #523 // String ResultSet.Column_Index_out_of_range_high
72: iconst_2
73: anewarray #121 // class java/lang/Object
76: dup
77: iconst_0
78: iload_1
79: invokestatic #470 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
82: aastore
83: dup
84: iconst_1
85: aload_0
86: getfield #236 // Field fields:[Lcom/mysql/jdbc/Field;
89: arraylength
90: invokestatic #470 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
93: aastore
94: invokestatic #519 // Method com/mysql/jdbc/Messages.getString:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
97: ldc_w #521 // String S1009
100: aload_0
101: invokevirtual #506 // Method getExceptionInterceptor:()Lcom/mysql/jdbc/ExceptionInterceptor;
104: invokestatic #512 // Method com/mysql/jdbc/SQLError.createSQLException:(Ljava/lang/String;Ljava/lang/String;Lcom/mysql/jdbc/ExceptionInterceptor;)Ljava/sql/SQLException;
107: athrow
108: aload_0
109: getfield #196 // Field profileSql:Z
112: ifne 122
115: aload_0
116: getfield #214 // Field useUsageAdvisor:Z
119: ifeq 131
122: aload_0
123: getfield #164 // Field columnUsed:[Z
126: iload_1
127: iconst_1
128: isub
129: iconst_1
130: bastore
131: aload_2
132: monitorexit
133: goto 141
136: astore_3
137: aload_2
138: monitorexit
139: aload_3
140: athrow
141: return
Exception table:
from to target type
16 133 136 any
136 139 136 any
LocalVariableTable:
Start Length Slot Name Signature
0 142 0 this Lcom/mysql/jdbc/ResultSetImpl;
0 142 1 columnIndex I
LineNumberTable:
line 760: 0
line 761: 16
line 762: 21
line 766: 60
line 767: 69
line 773: 108
line 774: 122
line 776: 131
line 777: 141
StackMapTable: number_of_entries = 6
frame_type = 252 /* append */
offset_delta = 60
locals = [ class java/lang/Object ]
frame_type = 47 /* same */
frame_type = 13 /* same */
frame_type = 8 /* same */
frame_type = 68 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 4 /* same */
Exceptions:
throws java.sql.SQLException
方法访问者中使用的asm代码是:
if (opcode == MONITORENTER)
{
// super.visitInsn(DUP); // in the beginning I used DUP followed, now ALOAD
super.visitInsn(opcode);
super.visitVarInsn(ALOAD, lastAStoreIndex);
super.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Interceptor.class), "monitorEntered", "(Ljava/lang/Object;)V", false);
}
因此,该方法似乎与JIT不兼容。 -XX:+PrintCompilation
显示:
21938 619 ! 3 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes)
21938 619 ! 3 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes) COMPILE SKIPPED: invalid parsing (retry at different tier)
!m @ 6 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes) not compilable (disabled)
!m @ 13 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes) not compilable (disabled)
22105 716 ! 4 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes)
22105 716 ! 4 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes) COMPILE SKIPPED: cannot parse method (not retryable)
!m @ 6 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes) not compilable (disabled)
!m @ 13 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes) not compilable (disabled)
!m @ 62 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes) not compilable (disabled)
!m @ 13 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes) not compilable (disabled)
!m @ 6 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes) not compilable (disabled)
!m @ 13 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes) not compilable (disabled)
!m @ 13 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes) not compilable (disabled)
!m @ 6 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes) not compilable (disabled)
!m @ 62 com.mysql.jdbc.ResultSetImpl::checkColumnBounds (142 bytes) not compilable (disabled)
我知道我要添加的指令javac
不会生成任何东西,但是由于它是有效的字节码(至少我认为这样,并且该示例有效),所以我假设JIT可以处理它。但是,JIT似乎正在寻找一些众所周知的模式。我想知道其他基于JVM的语言如何处理它。他们是否总是需要产生与javac
相同或相似的字节码?
我目前唯一想到的理论解决方案是尝试像字节码一样提出javac
,这当然比我在这里尝试做的要复杂得多,因为我需要存储监视器对象放入新的本地变量中,然后在monitorEnter
之前从那里加载它,并在调用我的拦截器之前再次执行相同的操作。因此,我要么需要更改为asm树API(以便再次返回),要么看看我是否可以缓冲指令,以便在遇到monitorEnter
时仍然能够做出相应的反应。还有其他建议可能更容易实现吗?
答案 0 :(得分:0)
如果要监视锁争用,可以使用Flight Recorder,当争用时间超过10到20毫秒时,它实际上没有开销。
JDK 11:
java -XX:StartFlightRecording:filename=recording.jfr ...
早期版本还需要-XX:+ UnlockCommercialFeatures标志,并且只能免费用于开发。
如果要分析较短的延迟,则可以使用Java Mission Control(窗口->模板管理器)或以下配置来创建自定义配置文件,即locks.jfc。如果不使用自定义配置,则默认阈值为20 ms。
事件的名称(jdk.JavaMonitorEnter)在两个发行版之间已更改,但这是fpr JDK 11或更高版本。
<?xml version="1.0" encoding="UTF-8"?>
<configuration version="2.0">
<event name="jdk.JavaMonitorEnter">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
<setting name="threshold">20 ms</setting>
</event>
</configuration>
可以降低阈值,但是如果低于1 ms,开销将急剧增加。大多数开销是由于获取堆栈跟踪而引起的,仅当等待时间长于阈值时才会发生。
java -XX:StartFlightRecording:filename=recording.jfr,settings=locks.jfc
锁定检测发生在JVM内部,并且使用不变的TSC(仅花费约10-15 ns)来测量延迟的持续时间。
可以在JMC中打开记录 https://openjdk.java.net/projects/jmc/7/
或者可以通过编程方式访问结果:
try(Recording file : new RecordingFile(Path.of("recording.jfr")) {
while (file.hasMoreEvents()) {
System.out.println(file.readEvent());
}
}