Это будет довольно злой пост — то, что вы будете гуглить, когда действительно захотите сделать чью-то жизнь бедой. В мире 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 в среде, в которой вы экспериментируете, и первые седые волосы скоро начнут процветать. Теперь, если вы хотите, чтобы скрыть свои следы, у нас есть несколько более сложных концепций, чтобы добавить в портфель, таких как
- Загрузите испорченный код в другой загрузчик классов и сохраняйте ссылку на класс, загруженный после удаления исходного загрузчика классов, имитируя утечку загрузчика классов.
- Спрятать нарушающий код в методы finalize (), делая симптомы действительно непредсказуемыми
- Бросить в хитрой комбинации долго работающих потоков, хранящих что-то в ThreadLocals , доступ к которому осуществляется ThreadPool — управляемыми потоками приложения
Я надеюсь, что мы дали вам пищу для размышлений и некоторые хитрости, которые нужно использовать в следующий раз, когда вы злитесь на кого-то. Бесконечные часы хардкорной отладки гарантированы. Если ваш друг не использует Plumbr, конечно, который находит утечки для него. Но, несмотря на вопиющий маркетинг, я надеюсь, что мы смогли продемонстрировать в двух простых случаях, как легко создать утечку памяти в Java. И большинство из вас испытали, как трудно было бы отследить такую ошибку. Так что, если вам понравилась эта публикация, подпишитесь на нашу ленту в Twitter и будьте в курсе нашего будущего контента о настройке производительности JVM.