среда, 29 февраля 2012 г.

Использование памяти в JAVA (Часть 1)

Разрабатывая очередную программу для Android, столкнулся с моментом, когда нужно было создать сложную структуру. Проблем с созданием структуры вообще никаких не возникло: средств для этого в JAVA больше чем предостаточно. Но вот какой тип данных или объект применить для определенного элемента? Vector или ArrayList? А может просто массив? Интеровских "холиваров" по этому поводу "пруд пруди", но опираться на них - бессмыслено. "Каждый кулик своё болото хвалит" либо потому что ему так нравится использовать тот или иной тип данных или объект, либо потому что его так научили. Лично меня научили, что нельзя бездумно тратить память компьютера, она увы не бесконечна... И даже сейчас во времена гигабайтных размеров оперативки. А в то время, когда "дереья ещё были большими", слово "мегабайт" применялась только в специализированных журналах и то исключительно для суперкомпьютеров. Обыденному пользователю такая роскошь была просто напросто недоступна. Поэтому и приходилось "экономить на спичках", что выросло в привычку взвешивать каждое объявление переменной. Однако, признаюсь честно, если бы я писал приложение для персоналки, я бы даже не заморачивался по этому вопросу, сделал бы как удобно. Но, приложение пишется для девайсов под управлением Android, и малое наличие оперативной памяти на многих моделях, сказывается пагубно. Для некоторых "прожорливых" приложений если с натягом и хватает загрузить какое то приложение в память телефона, то для его установки памяти уже не остается. Поэтому я стараюсь избегать таких моментов, тщательно анализируя то, что я создаю. Однако, одно дело взвешивать подобные действия "в слепую", другое дело опираться на реальные факты. Именно поэтому я решил разобраться более детально с этим вопросом.

Первым делом конечно же я побежал в маны JAVA по типам данным (http://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html). Но увидев первые же строчки понял, что этой информации мне будет не то что не достаточно, она вообще не охватывает даже часть того вопроса, который стоял передо мной. Без сомнений, она достаточна и можно было используя эти данные, вручную вычислить сколько занимает тот или иной объект. Однако, одно дело вычислить, другое дело - получить результат программным способом. Поэтому требовалось нечто большее.

Тут на помощь как всегда пришёл добрый Google и вывел меня на страницу (Memory Usage in Java), где некий Dr. Heinz M. Kabutz был озадачен тем же вопросом, что и я. Применив свой опыт, он реализовал этот вопрос практически и показал "сырцы" с коментариями, каким образом он всё это делал. Используя его код, я решил провести собственное расследование.

При первых же тестах я увидел, что данные, которые были указаны в его результатах разнятся по сравнению со моими. Сначала я пытался найти ошибку, которую возможно я допустил. Но понял, что дело не в ошибке, а в том, что его тестирование проводились на 32-битной системе, я же тестировал на 64-битной, что придало большего контраста тестам: мало того, что можно узнать какой из объектов сколько занимает места в памяти, можно так же посмотреть, на сколько эти данные разнятся между разными системами. БИНГО!

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

Ещё в его примерах я заметил некую не согласованность сравнения данных. Если BooleanArray и PrimitiveByteArray он сранивает по 1000 байт, для самых "прожорливых" объектов FullArrayList и FullLinkedList он почему то применяет 10000 итераций. Сравнивая ведра воды с количеством лаптей особо верную картину вряд ли получишь. Поэтому я решил для своих тестов применить определенные правила:
- там где требуется явное указание байта - это будет (byte)12;
- для целых чисел будет использоваться число 123, если это int или Integer и 1234567891234 если это long или Long;
- для типов с "плавающей запятой" будет использоваться число 123.45;
- строкам будет применяться текст "Field";
- размерность массивов или итераций там где это требуется будет 10000;
- тестирование будет производиться как на 32-х так и на 64-битной системе.

Описывать те примеры, которые приведены на сайте указанном выше, я не буду. Все данные сравнения вы увидите в таблице, приведенной в конце второй части этой статьи. Опишу только те примеры, которые были добавлены мной.

Array of String

Самым первым тестом я решил проверить именно этот тип данных, так как он не так уж и редко встречается в примерах кода Android SDK. Я знал заблаговременно, что использование массива строк очень не желательно. Однако, убедиться на сколько это не желательно, просто необходимо. И даже был удивлен, почему Heinz в своих тестах его не использовал в качестве наглядного примера. Первый свой пример я набросал таким образом, как я бы его написал в коде какого нить приложения. В итоге получился следующий объект:

public class StringArrayFactory implements ObjectFactory {
    public Object makeObject() {
        return new String[]{
            "12",
            "12",
            "a",
            "2012-02-29 11:47:55",
            "123.45",
            "123.45",
            "2012-02-29 11:47:55",
            "123.45",
            "123.45",
            "123",
            "123",
            "1234567891234",
            "1234567891234",
            "Field"
        };
    }
}

Результат показал:

Factories.StringArrayFactory produced [Ljava.lang.String; which took 72 bytes

Здесь и далее в описании я буду приводить только результаты на 64-битной системе.

Не обратив внимания на малое количество байт в результате, я набросал следующий объект на 10000 итерации,

public class StringArrayTCFactory implements ObjectFactory {
    public Object makeObject() {
        int rows = 10000;
        String[] col = new String[]{
            "12",
            "12",
            "a",
            "2012-02-29 11:47:55",
            "123.45",
            "123.45",
            "2012-02-29 11:47:55",
            "123.45",
            "123.45",
            "123",
            "123",
            "1234567891234",
            "1234567891234",
            "Field"
        };
        String[][] str = new String[rows][col.length];
        for (int i = 0; i < rows; i++) {
            str[i] = col;
        }
        return str;
    }
}

который мне выдал уже очень сомнительный результат:

Factories.StringArrayTCFactory produced [[Ljava.lang.String; which took 39680 bytes

Что за ерунда такая? Массив строк оказался оптимальней чем Vector или ArrayList? Да в жизни не поверю! Ни для кого не секрет, что класс String в любом языке программирования - зло, если его применять где не попадя. А с приходом UTF-8 и UTF-16 - это стало мегазло, нещадно пожирающее память. А если ещё этот объект "обернут" в массив... А тут такой результат!!! Но ларчик как всегда просто открывался. Создавая объекты таким образом, текст (или точнее сказать текстовые метки) создаются в памяти в виде констант, а массив уже заполняется ссылками на эти созданные объекты. Поэтому оба результата были "фейком". Следовательно, нужно привести оба объекта к динамическому созданию строк. Приводим эти объекты к следующим видам:

public class StringArrayFactory implements ObjectFactory {
    public Object makeObject() {
        return new String[]{
            new String("12"),
            new String("12"),
            new String("a"),
            new String("2012-02-29 11:47:55"),
            new String("123.45"),
            new String("123.45"),
            new String("2012-02-29 11:47:55"),
            new String("123.45"),
            new String("123.45"),
            new String("123"),
            new String("123"),
            new String("1234567891234"),
            new String("1234567891234"),
            new String("Field")
        };
    }
}

Factories.StringArrayFactory produced [Ljava.lang.String; which took 520 bytes

public class StringArrayTCFactory implements ObjectFactory {
    public Object makeObject() {
        int rows = 10000;
        int cols = 14;
        String[][] str = new String[rows][cols];
        String[] r;
        for (int i = 0; i < rows; i++) {
            r = new String[cols];
            for (int j = 0; j < cols; j++)
                r[j] = new String("Field");
            str[i] = r;
        }
        return str;
    }
}

Factories.StringArrayTCFactory produced [[Ljava.lang.String; which took 5239608 bytes

Увидев результат последнего объекта, я был полностью удовлетворён ответом. Но состояние было в пору хвататься за валерьянку. Тут уже без дополнительных объяснений любому станет понятно, почему не желательно использовать подобного рода определения переменных.
Почему я заполнил StringArrayFactory именно такими данными, будет понятно дальше. Сейчас я не буду на этом останавливаться.

java.util.HashMap

Даный объект является почти самым популярным при использовании Android SDK. При чем чаще всего, первый объект мапирования как правило идёт типа String. После предыдущего примера на класс String я уже стал смотреть с подозрением, так как ожидать от него можно достаточно мощный разнос. Однако, каждый фрукт одинаково полезен, употреблённый в нужное время в нужном количестве. Поэтому посмотрим "что это за фрукт" HashMap и рассмотрим так же 2 варианта: сколько занимает "простой" объект и сколько он будет занимать, содержа в себе 10000 "строк" объектов. Так же учитывается тот момент, что первый объект будет типа String.

public class HashMapSimpleFactory implements ObjectFactory {
    public Object makeObject() {
        HashMap hm = new HashMap(1);
        hm.put(new String("Field"), new Object());
        return hm;
    }
}

Результат:

Factories.HashMapSimpleFactory produced java.util.HashMap which took 152 bytes

Объект HashMapTC для 2-го варианта выглядит следующим образом:

public class HashMapTCFactory implements ObjectFactory {
    public Object makeObject() {
        HashMap hm = new HashMap(10000);
        for (int i = 0; i < 10000; i++)
            hm.put(new String("Field"), new Object());
        return hm;
    }
}

Результат выполнения:

Factories.HashMapTCFactory produced java.util.HashMap which took 65272 bytes

По правде говоря, получив результат в 152 байт для объекта HashMapSimpleFactory, я подумал, что опять накосячил с созданием строк. Тем не менее, пример оказался верным. Результата HashMapTCFactory я ожидал в 2 раза большего и был поражен, что он занимает всег 64к. Что само по себе ответила на мой вопрос "Почему так часто в Android приложениях используется этот тип данных".

Продолжение тестирования читайте в следующей статье "Использование памяти в JAVA (Часть 2)".