Intercepting monitorEnter - Результирующий байт-код не может быть скомпилирован JIT - PullRequest
0 голосов
/ 14 декабря 2018

В моем агенте я перехватываю 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 и сделать то же самое еще раз до вызова моего перехватчика.Поэтому мне нужно будет либо перейти на API asm tree (чтобы вернуться снова), либо посмотреть, смогу ли я буферизовать инструкции, чтобы все еще иметь возможность реагировать соответствующим образом в случае, если я достигну monitorEnter.Любые другие предложения, которые могут быть немного легче реализовать?

1 Ответ

0 голосов
/ 15 декабря 2018

Если вы хотите отслеживать конфликты блокировок, вы можете использовать Flight Recorder, у которого практически нет накладных расходов, когда речь идет о конфликтах, которые занимают более 10-20 мс.

JDK 11:

java -XX:StartFlightRecording:filename=recording.jfr ...

В более ранних выпусках также требуется флаг -XX: + UnlockCommercialFeatures, и его можно использовать только бесплатно для разработки.

Если вы хотите профилировать более короткий профильзадержки, вы можете создать пользовательский файл конфигурации, т.е. locks.jfc, используя Java Mission Control (Window -> Template Manager), или использовать конфигурацию ниже.Пороговое значение по умолчанию составляет 20 мс, если не используется пользовательское подтверждение.

Имя события (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 мс.Большинство накладных расходов связано с получением трассировки стека, что происходит только в том случае, если задержка превышает пороговое значение.

java -XX:StartFlightRecording:filename=recording.jfr,settings=locks.jfc

Инструментарий блокировки происходит внутри JVM, и длительность задержек измеряется с использованием инвариантного TSC, который стоит всего около 10-15 нс.

Запись может быть открыта в 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());
  }
}
...