拦截monitorEnter-无法对JIT编译生成的字节码

时间:2018-12-14 17:01:14

标签: java performance jvm jit jvm-hotspot

在我的代理中,我正在拦截monitorEnter个事件。到目前为止,拦截器功能什么都不做,只能立即返回。由于我遇到了一些重大的性能影响,因此我试图找出问题所在。

我目前的理解是,修改后的字节码可以工作,但是JIT编译时遇到问题。我不知道为什么,什么是解决该问题的最佳方法。

拦截所有monitorEnter的幼稚方法是DUP监视程序,先执行monitorenter,然后对通过监视器对象的拦截器执行INVOKESTATIC。这样做有时会导致IllegalMonitorStateException。 (不确定为什么)。然后,我将拦截器的代码序列更改为monitorEnterALOADINVOKESTATIC。虽然我没有再次遇到异常,但是生成的代码无法进行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时仍然能够做出相应的反应。还有其他建议可能更容易实现吗?

1 个答案:

答案 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());
  }
}