Статьи

Причина медленного чтения больших строк в JDK 7 и JDK 8

Ранее я публиковал пост в блоге « Чтение больших строк медленнее» в JDK 7 и JDK 8, и в посте было несколько полезных комментариев, описывающих проблему. В этом посте дается более подробное объяснение, почему чтение файлов, продемонстрированное в этом посте (и используемое Ant ‘s LineContainsRegExp ), намного медленнее в Java 7 и Java 8, чем в Java 6.

Пост X Ванга Метод substring () в JDK 6 и JDK 7 описывает, как String.substring () был изменен между JDK 6 и JDK 7. В этом посте Ван пишет, что JDK 6 substring() «создает новую строку, но значение строки по-прежнему указывает на тот же массив [backing char] в куче ». Он противопоставляет это подходу JDK 7: «В JDK 7 метод substring () фактически создает новый массив в куче».

Пост Вана очень полезен для понимания различий в String.substring() между Java 6 и Java 7. Комментарии к этому посту также проницательны. Комментарии включают в себя чувство, которое я могу оценить : «Я бы сказал« другой », а не« улучшенный »». Есть также объяснения того, как JDK 7 предотвращает потенциальную утечку памяти, которая может произойти в JDK 6.

Поток StackOverflow Java 7 String — сложность подстроки объясняет мотивацию изменения и ссылается на ошибку JDK-4513622: (str) сохранение подстроки поля запрещает GC для объекта . Эта ошибка гласит: «Ошибка OutOfMemory [происходит], потому что объекты не получают мусор, если вызывающая сторона хранит подстроку поля в объекте». Ошибка содержит пример кода, который демонстрирует эту ошибку. Я адаптировал этот код здесь:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * Minimally adapted from Bug JDK-4513622.
 *
 */
public class TestGC
{
   private String largeString = new String(new byte[100000]);
     
   private String getString()
   {
      return this.largeString.substring(0,2);
   }
     
   public static void main(String[] args)
   {
      java.util.ArrayList<String> list = new java.util.ArrayList<String>();
      for (int i = 0; i < 1000000; i++)
      {
         final TestGC gc = new TestGC();
         list.add(gc.getString());
      }
   }
}

Следующий снимок экрана демонстрирует, что последний фрагмент кода (адаптированный из Bug JDK-4513622) выполняется как с Java 6 (jdk1.6 является частью пути исполняемого средства запуска Java), так и с Java 8 (версия по умолчанию на моем хосте). Как показано на снимке экрана, OutOfMemoryError генерируется, когда код запускается в Java 6, но не генерируется при запуске в Java 8.

demoTestGCForJdk4513622

Другими словами, изменение в Java 7 устраняет потенциальную утечку памяти за счет снижения производительности при выполнении String.substring длинных строк Java. Это означает, что любые реализации, которые используют String.substring (включая Ant’s LineContainsRegExp) для обработки действительно длинных строк, вероятно, необходимо изменить, чтобы реализовать это иначе, или их следует избегать при обработке очень длинных строк при переходе с Java 6 на Java 7 и выше.

Как только проблема известна (в данном случае изменение реализации String.substring ), легче найти документацию в Интернете о том, что происходит (спасибо за комментарии, которые облегчили поиск этих ресурсов). У повторяющихся ошибок JDK-4513622 есть записи, которые предоставляют дополнительные детали. Это ошибки JDK-4637640: утечка памяти из-за реализации String.substring () и JDK-6294060: использование substring () вызывает утечку памяти . К другим связанным онлайн-ресурсам относятся Изменения в String.substring в Java 7 [которые включают ссылку на String.intern () — есть лучшие способы ], Java 6 по сравнению с Java 7: когда важна реализация , и комментарии с большим количеством комментариев (более 350 комментариев) Reddit thread TIL Oracle изменил внутреннее представление String в Java 7 Update 6, увеличив время выполнения метода substring с константы до N.

Сообщение « Изменения в строковом внутреннем представлении», сделанное в Java 1.7.0_06, предоставляет хороший обзор этого изменения и суммирует исходную проблему, исправление и новую проблему, связанную с исправлением:

Теперь вы можете забыть об утечке памяти, описанной выше, и никогда больше не использовать новый конструктор String (String). Как недостаток, вы должны помнить, что String.substring теперь имеет линейную сложность, а не постоянную.