Статьи

Как создать утечку памяти

Это будет довольно злой пост — то, что вы будете гуглить, когда действительно захотите сделать чью-то жизнь бедой. В мире Java-разработки утечки памяти — это как раз те ошибки, которые вы бы привели в этом случае. Дни или даже недели бессонных ночей в офисе гарантированы для вашей жертвы.

Мы опишем две утечки в этом посте. Оба они легко понять и воспроизвести. Утечки происходят из реальных кейсов, но для ясности мы извлекли демонстрационные примеры, чтобы они были короче и проще для понимания. Но будьте уверены — после того, как мы увидели и устранили сотни утечек — случаи, подобные тем, которые были продемонстрированы, являются более распространенными, чем вы могли ожидать.

Первый участник, который вступит в кольцо — печально известные решения HashSet / HashMap, в которых используемый ключ либо не имеет, либо имеет неверные решения equals () / hashCode ( ).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class KeylessEntry {
  
   static class Key {
      Integer id;
  
      Key(Integer id) {
         this.id = id;
      }
  
      @Override
      public int hashCode() {
         return id.hashCode();
      }
   }
  
   public static void main(String[] args) {
      Map<key, string=""> m = new HashMap<key, string="">();
      while (true)
         for (int i = 0; i < 10000; i++)
            if (!m.containsKey(i))
               m.put(new Key(i), "Number:" + i);
   }
}

Когда вы выполняете приведенный выше код, вы ожидаете, что он будет работать вечно без каких-либо проблем — в конце концов, построенное наивное решение для кэширования должно быть расширено только до 10 000 элементов, и тогда рост остановится, поскольку все ключи уже присутствуют в HashMap . Однако это не так — элементы продолжают добавляться, поскольку класс Key не содержит надлежащей реализации equals () рядом с его hashCode () . Решение было бы простым — добавьте реализацию метода equals (), подобную следующей, и все готово. Но прежде чем вам удастся найти причину, вы определенно потратили потерянные драгоценные клетки мозга.

1
2
3
4
5
6
7
8
@Override
public boolean equals(Object o) {
   boolean response = false;
   if (o instanceof Key) {
      response = (((Key)o).id).equals(this.id);
   }
   return response;
}

Вторая проблема, чтобы держать вашего друга бодрствующим — обработка строк в некоторых операциях. Работает как шарм, особенно в сочетании с различиями версий JVM. В JDK 7u6 были изменены методы работы с String , поэтому, если вам удастся найти среды, в которых производство и подготовка отличаются только второстепенными версиями, то все готово. Добавьте вашего друга для отладки кода, подобного следующему, и удивитесь, почему проблема не возникает нигде, кроме как в производстве.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
class Stringer {
   static final int MB = 1024*512;
  
   static String createLongString(int length){
      StringBuilder sb = new StringBuilder(length);
      for(int i=0; i < length; i++)
         sb.append('a');
      sb.append(System.nanoTime());
      return sb.toString();
   }
  
   public static void main(String[] args){
      List<string> substrings = new ArrayList<string>();
      for(int i=0; i< 100; i++){
         String longStr = createLongString(MB);
         String subStr = longStr.substring(1,10);
         substrings.add(subStr);
      }
   }
}

Что происходит в приведенном выше коде — когда он запускается на pre JDK 7u6, возвращаемая подстрока содержит ссылку на большую строку ~ 1 МБ. Поэтому, когда образец запускается с -Xmx100m, вы можете столкнуться с неожиданным исключением OutOfMemoryException. Объедините это с различиями платформ и получите другую версию JDK в среде, в которой вы экспериментируете, и первые седые волосы скоро начнут процветать. Теперь, если вы хотите, чтобы скрыть свои следы, у нас есть несколько более сложных концепций, чтобы добавить в портфель, таких как 3d Tap с гигантской капельницей

  • Загрузите испорченный код в другой загрузчик классов и сохраняйте ссылку на класс, загруженный после удаления исходного загрузчика классов, имитируя утечку загрузчика классов.
  • Спрятать нарушающий код в методы finalize (), делая симптомы действительно непредсказуемыми
  • Бросить в хитрой комбинации долго работающих потоков, хранящих что-то в ThreadLocals , доступ к которому осуществляется ThreadPool — управляемыми потоками приложения

Я надеюсь, что мы дали вам пищу для размышлений и некоторые хитрости, которые нужно использовать в следующий раз, когда вы злитесь на кого-то. Бесконечные часы хардкорной отладки гарантированы. Если ваш друг не использует Plumbr, конечно, который находит утечки для него. Но, несмотря на вопиющий маркетинг, я надеюсь, что мы смогли продемонстрировать в двух простых случаях, как легко создать утечку памяти в Java. И большинство из вас испытали, как трудно было бы отследить такую ​​ошибку. Так что, если вам понравилась эта публикация, подпишитесь на нашу ленту в Twitter и будьте в курсе нашего будущего контента о настройке производительности JVM.

Ссылка: Как создать утечку памяти у нашего партнера JCG Никиты Сальникова-Тарновского в блоге Plumbr Blog .