Статьи

Следы стека говорят правду, но не всю правду

Это сообщение от Владимира Шора из блога Plumbr.

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

Примеры сводятся к возможностям JVM для создания значимых стековых трасс. Во многих случаях трассировка стека содержит всю информацию, необходимую для решения проблемы. В других ситуациях это только поверхностные симптомы, без понятия о том, что может быть причиной проблемы. Позвольте мне проиллюстрировать это тремя примерами, вызывающими следующие печально известные сообщения об ошибках:

  • java.lang.OutOfMemoryError: невозможно создать новый собственный поток
  • java.io.IOException: слишком много открытых файлов в системе
  • java.lang.OutOfMemoryError: пространство кучи Java

Все примеры проиллюстрированы простыми фрагментами кода, облегчающими понимание основной проблемы.

Слишком много тем

static void createTooManyThreads() {
	try {
		for (int i = 0; i < TOO_MANY; i++) {
			Thread t = new Thread(new Runnable() {

				public void run() {
					try {
						Thread.sleep(3000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			});
			t.start();
		}
	} catch (OutOfMemoryError e) {
		e.printStackTrace();
	}
}

В приведенном выше коде мы продолжаем запускать потоки, пока не достигнем системного предела и не встретим сообщение «java.lang.OutOfMemoryError: не в состоянии создать новый собственный поток» . Что плохого в понимании того, что проблема связана с исчерпанием ограничения потока? Давайте ближе рассмотрим трассировку стека:

java.lang.OutOfMemoryError: unable to create new native thread
	at java.lang.Thread.start0(Native Method)
	at java.lang.Thread.start(Thread.java:693)
	at eu.plumbr.demo.MisleadingStacktrace.createTooManyThreads(MisleadingStacktrace.java:34)
	at eu.plumbr.demo.MisleadingStacktrace.main(MisleadingStacktrace.java:16)

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

Но давайте посмотрим на ту же проблему — потребление ресурсов с другой точки зрения:

Слишком много открытых файлов

Опять же, давайте начнем с примера кода:

static void createTooManyFiles() {
	try {
		for (int i = 0; i < TOO_MANY; i++) {
			File f = new File(PATH_TO_FILE + i + ".txt");
			f.createNewFile();
			OutputStream s = new FileOutputStream(f);
			s.write(1);
		}
	} catch (IOException e) {
		e.printStackTrace();
	}
}

Образец пытается создать много файлов и записать только одно целое число в каждый из файлов, не закрывая предыдущие. И снова, выполнение приведенного выше кода приводит к не слишком полезной трассировке стека:

java.io.IOException: Too many open files in system
	at java.io.UnixFileSystem.createFileExclusively(Native Method)
	at java.io.File.createNewFile(File.java:947)
	at eu.plumbr.demo.MisleadingStacktrace.createTooManyFiles(MisleadingStacktrace.java:45)
	at eu.plumbr.demo.MisleadingStacktrace.main(MisleadingStacktrace.java:17)

Та же самая проблема теперь замаскирована по-другому — мы получаем сообщение о том, что я сейчас слишком много пытался открыть один файл, но — кто открыл другие файлы, чтобы подчеркнуть JVM до такой степени, что он не может завершить выполнение?

Если вы все еще не уверены — взгляните на третий пример нашего нынешнего хлеба с маслом:

Слишком много памяти занято

Пример кода снова прост — мы берем структуру данных и продолжаем увеличивать ее, пока доступная куча не будет исчерпана:

static void createTooLargeDataStructure() {
	try {
		List l = new ArrayList();
		for (int i = 0; i < TOO_MANY; i++) {
			l.add(i);
		}
	} catch (OutOfMemoryError e) {
		e.printStackTrace();
	}
}

Запуск кода дает вам печально известное сообщение java.lang.OutOfMemoryError: пространство кучи Java . Что, опять же, трудно интерпретировать, если рассматриваемая структура данных была заполнена в различных возможных местах исходного кода.

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

Если вас интересует полный пример кода, его можно скачать здесь . Код запускался на моем Macbook Pro с OS X 10.9 на JDK 7u25. После этого убедитесь, что вы не пропустите будущие обновления интересного контента, и подпишитесь на нашу ленту Twitter .