Когда идет разработка приложения критичного к памяти (и)или размеру байт кода, к скорости выполнения, то начинаешь задумываться о вещах, о которых не думаешь, когда разрабатываешь J2EE приложения, работающие на серверах с большим количеством доступной оперативной памяти, пространства на жестком диске и процессорного времени. Здесь я сделаю попытку собрать в одном месте, советы и рекомендации по оптимизации, оптимизации Java и J2ME приложений.
Виды оптимизации
Оптимизировать приложение можно с разных сторон, выделим основные виды оптимизации:
- по скорости выполнения байт кода;
- по использованию ресурсов системы;
- по размеру байт кода и файлов приложения;
- алгоритмическая оптимизация.
В некоторых местах виды оптимизации могут пресекаться, и для достижения максимально эффекта необходимо соблюсти принцип "золотой середины", особенно это касается скорости выполнения и размера байт кода.
Инструменты оптимизации
Есть много утилит и оптимизаторов, отмечу тот, который знаком мне, это Proguard - обфускатор, оптимизатор и еще много чего умеет. Обычно после компиляции, делают оптимизацию и обфускацию, эта утилита может это делать в 12 проходов, при этом на каждом шаге что либо оптимизируя и обрезая мертвый или лишний код. Нельзя сказать, что приложение или библиотека на выходе получается полностью оптимизированным, есть некоторые конструкции кода, которые можно оптимизировать, но она не может обработать их из-за того, как они написаны. Иногда, что-то приходиться переписать в таком случае, чтобы это поддалось оптимизации - расставить модификаторы final например.
Общие советы по оптимизации
Не открою некоторым Америку, если перечислю следующие советы:
Не оптимизируйте преждевременно
Пишите код без мыслей о том, что в дальнейшем вы будете его оптимизировать, сконцентрируйтесь на том, чтобы написать код чистым, правильным и стабильным. Если он получился большой и (или) медленный, только тогда вы можете принять решение об оптимизации.
Используйте профайлер
В Sun JDK идет два профайлера, VisualVM ($JDK_HOME/bin/jvisualvm) и jsonsole ($JDK_HOME/bin/jconsole или $JDK_HOME/lib/jconsole.jar). Оба подходят для профилирвоания приложения на локальной машине. Для профилирования на удаленной машине можно использовать эти утилиты по JMX, о настройке удаленного профилирования можно почитать в Oracle wiki How to use JConsole, JVisualVM or VisualVM with Oracle Application Server.
Запускайте приложение до и после оптимизации
Сравнивайте результаты до и после оптимизации, размеры, скорости выполнения benchmark тестов, и т.д. Чтобы быть в курсе, к чему приводят действия оптимизации. Можно выиграть значительно в размере, но потерять катастрофически в производительности.
Используйте правильные алгоритмы и правильные структуры данных
Не используйте сортировку пузырьком O(n2) на массиве в тысячу элементов, если можно использовать алгоритм быстрой сортировки (quick sort) O(n log n). Соответственно не храните тысячу элементов массиве, в котором необходимо будет искать элементы O(n), лучше использовать бинарное дерево O(log n) или java.util.HashMap для однопоточной работы со структурой данных, или java.util.HashTable для многопоточной работы. Также нужно иметь представление о внутренней реализации коллекций, чтобы понимать когда применить ArrayList, а когда LinkedList.
Не пишите магические символы
Пишет читабельный человеком код, а не читабельный, но заставляющий компилятор или оптимизатор делать свою работу лучше.
Дизассемблируйте
Используйте утилиту javap в Sun JDK. О ее использовании написано ниже.
Используйте обфускатор, оптимизатор, шринкер
Например такой как proguard, есть аналоги.
Оптимизация по размеру
Этот вид оптимизации очень важен для Java апплетов и J2ME приложений, когда файлы приложения должны иметь как можно меньше размер. В случае с апплетами, это значение очень критично для времени затраченное пользователем на скачивание апплета. Во втором случае, мобильные устройства, терминалы и другие устройства с поддержкой J2ME спецификаций не всегда обладают достаточными объемами Flash памяти, позволяя хранить файлы больших размеров. За частую прибегая к этому виду оптимизации приходится жертвовать производительностью приложения, оценивать вам на сколько важно в том или ином случае размер или скорость выполнения.
Как сравнить результаты до и после? А их обязательно нужно сравнивать. Есть два способа.
1. Самый простой сравнить размер, если он стремится к нулю, после очередной проделанной операции по уменьшению размера, то все идет возможно хорошо.
2. Дизассемблировать классы, можно используя утилиту javap, которая находиться в $JDK_HOME/bin/, например так:
javap -c MyClass.class
Результатом ее работы станет набор кодов операций - оп. код (opcode).
Например такой код:
void createBuffer() {
int buffer[];
int bufsz = 100;
int value = 12;
buffer = new int[bufsz];
buffer[10] = value;
value = buffer[11];
}
Будет выглядеть так:
0 bipush 100
2 istore_2
3 bipush 12
5 istore_3
6 iload_2
7 newarray int
9 astore_1
10 aload_1
11 bipush 10
13 iload_3
14 iastore
15 aload_1
16 bipush 11
18 iaload
19 istore_3
20 return
Подробнее о компиляции под JVM можно почитать в спецификации виртуальной Java машины, 7 раздел
Альтернатива дизассемблированию - декомпиляция, когда мы получаем исходный код программы. С этой задачей успешно справляется утилита Jad NT.
Компонуйте приложение в JAR файл
Для того чтобы избежать накладных расходов на загрузку отдельных классов виртуальной машиной.
Можно использовать утилиту jar в $JDK_HOME/bin, например так:
jar cvf myapp.jar -C src/ .
Также это умеют делать среды разработки и такие системы управления проектами как Ant, Ivy, Maven или Gradle.
Не изобретайте уже существующее API
Используйте или наследуйтесь от классов Java API, расширяя их функциональные возможности для своих потребностей. Не изобретайте велосипед.
Простой пример. Программист не знал о существовании java.lang.Math и создал класс под названием MathUtil и написал функцию возведения в степень N:
public static int pow(int value, int n) {
for(int i = 0; i < n; i++) {
value *= value;
}
return value;
}
Лишний код, который можно заменить функцией java.lang.Math.pow(double value, double n), причем она работает не только с целыми числами, но и double, и работает гораздо быстрее.
Используйте наследование
Используйте наследование в своем коде, чем больше методов вы сможете наследовать, тем меньше вам придется писать кода.
Приведу пример, есть интерфейс:
interface ObjectTranslator<Domain , Dto implemenets java.io.Serializable> {
List<Dto> translate(List<Domain> ds);
Dto translate (Domain d);
}
Есть два класса, первый транслирует объекты User в UserDto:
class UserDataObjectTransaltor implements OjectTranslator<User, UserDto> {
public List <UserDto> translate(List<User> ds) {
List<UserDto> dtos = new ArrayList<UserDto>(ds.size());
for(User o : ds) dtos.add(transalte(o));
return dtos;
}
public UserDto translate(User d) {
UserDto dto = new UserDto();
dto.setId(d.getId());
dto.setName(d.getFirstName() + " " + d.getLastName());
return dto;
}
}
второй транслирует Car в CarDto:
class CarDataObjectTransaltor implements OjectTranslator<Car, CarDto> {
public List <CarDto> translate(List<Car> ds) {
List<CarDto> dtos = new ArrayList<CarDto>(ds.size());
for(Car o : ds) dtos.add(transalte(o));
return dtos;
}
public CarDto translate(Car d) {
CarDto dto = new CarDto();
dto.setId(d.getId());
dto.setCopmanyName(d.getCompanyName());
dto.setYear(d.getYear());
return dto;
}
}
Видно, что в реализациях метод translate(List<Domain> ds) одинаковый, и порождает дублирование кода с каждым новым классом - транслятором. Применим шаблон проектирования Interface and Abstract Class (Интерфейс и абстрактный класс). Создадим абстрактный класс, в который вынесем реализацию метода translate(List<Domain> ds) - общую для всех классов - трансляторов. Если в каком нибудь классе необходимо будет изменить логику данного метода, то мы просто переопределим его.
abstract class AbstractObjectTranslator<Domain,
Dto implements java.io.Serializable>
implements ObjectTranslator<Domain, Dto> {
List<Dto> translate(List<Domain> ds)
}
Изменяем наши классы, убирая метод, и заменяя определение того, что мы реализуем интерфейс на наследование от абстрактного класса:
class UserDataObjectTransaltor
extends AbstractObjectTranslator<User, UserDto> {
public UserDto translate(User d) {
UserDto dto = new UserDto();
dto.setId(d.getId());
dto.setName(d.getFirstName() + " " + d.getLastName());
return dto;
}
}
по аналогии, тоже делаем с другими классами - трансляторами.
Кода меньше, применили шаблон. Осталось написать javadoc комментарии на интерфейс и абстрактный класс, мол " наследуйся от меня, если хочешь транслировать объекты".
При компиляции используйте оптимизацию компилятором
Используйте javac -O, для того чтобы компилятор оптимизировал например работу со встраиваемыми функциями, и выполнил другую работу по оптимизации - зависит от компилятора.
Выделяйте общий код в метод
Одинаковые блоки кода в разных классах, можно выделять в собственные методы. Но в случае с оптимизацией по скорости, предпочтительнее этого не делать - тут нужно оценить ситуацию исходя из того при каких условиях и как часто выполняется данных код в приложении, и на сколько уменьшится размер в результате выделения в отдельный метод.
Не инициализируйте большие массивы статически
Да такие массивы действительно занимают большое количество байт кода, например класс подсчета контрольной суммы. Если есть возможность, заменить статическую инициализацию на формирование массива во время выполнения приложения. Уменьшая размер байт кода, приходится жертвовать временем выполнения. Если нет возможности заменить статическую инициализацию на динамическую, то можно воспользоваться следующей безумной идеей - хранить данные элементов массива в одной строке, а в при выполнении разобрать строку и инициализировать массив.
Date занимает много байт кода
Date объект создает удивительно много байт кода, учитывая тот минимальный функционал, который он предоставляет. Если вы храните объекты Date, то будет более оптимально хранить данные в long, или даже int если миллисекунды не важны, а там где необходимо выполнить операцию с датами заново создать объект Date.
Используйте короткие названия
Имена public объектов класса, таких как методы и свойства, желательно делать короткими, это сохранит дополнительное место. Также можно использовать обфускаторы, шринкеры (shrinker) если это допустимо, они изменяю имена на более короткие - 1,2 - 3 буквы. Если вы используете такой обфускатор, то он обработает все файлы, кроме того, который содержит точку входа в приложение - main метод, или класс указанный в MANIFEST.MF. Желательно, чтобы имя пакета (package name), которое является частью имени класса было как можно короче. Например org.mysite.projects.fooapp.MainClass стоит заменить на fooapp.MainClass.
Размещайте static final константы в интерфейсе
Компилятор для файла с ключевым словом class генерирует больше byte кода, чем для файла с ключевым словом interface. Константы с модификаторами statiс final определенные в файле интерфейса будут занимать меньше байт кода. Класс может реализовывать неограниченное количество интерфейсов - будет более читабельно и понятно, какой интерфейс констант использует данный класс.
Например, есть класс констант:
class Const {
public static final int A = 0;
public static final int B = 1;
public static final int C = 2;
}
Есть классы, которые используют эти константы, например такой класс:
class IAmUsingConstatnts {
public boolean isA(int value) {
return value == Const.A;
}
}
Изменяем наш класс констант на интерфейс констант:
interface Const {
public static final int A = 0;
public static final int B = 1;
public static final int C = 2;
}
Можно изменить наши классы, которые используют константы, например так:
class IAmUsingConstatnts implements Const {
public boolean isA(int value) {
return value == A;
}
}
Если вы скомпилируете первый вариант без изменений, соберете все в jar файл, затем скомпилируете изменения, и соберете второй jar файл, а затем сравните их размер в байтах, у второй версии будет размер меньше.
Используйте static final boolean для того, что в стабильной версии приложения не нужно
Например вы используете логирование везде по коду:
public static final boolean LOG = true;
... some code
if (LOG) log.debug("some debug info");
... some code
if (LOG) log.info("some info");
... some code
try {
// ... some operations
} catch(Exception e) {
log.error(e);
}
При компиляции с LOG = true компилятор скомпилирует весь код как представлено выше. Если установить LOG = false и скомпилировать приложение, то байт кода будет меньше, и он будет соответствовать следующему коду:
public static final boolean LOG = false;
try {
... some operations
} catch(Exception e) {
log.error(e);
}
Обычно отключат логирование в стабильной версии приложения, оставляя только вывод в лог ошибок.
Избегайте анонимных или вложенных классов и реализаций
Компилятор на каждый анонимный класс или реализацию интерфейса создает файл, если открыть менеджером архивов jar файл или посмотреть результаты компиляции в директории сборки, то можно увидеть файлы с именами, содержащие знак $, например: package.name.ParentClass$AnonymClass1. Каждый class файл, даже без инструкций, в jar занимает примерно 200 байт. Если бой идет за килобайты, то стоит пересмотреть код.
Например есть следующий код, который создает анонимный класс, наследующий java.lang.Thread:
class A {
public void someJob() {
new Thread("someJobThread") {
public void run() {
... doing some job in other thread
}
}.start();
}
}
В зависимости от решаемой задачи классом A, более оптимальнее будет сделать так:
class A implements Runable {
public void run() {
... doing some job in other thread
}
public void someJob() {
new Thread(this, "someJobThread").start();
}
}
Генерируйте исключительные ситуации, там где действительно нужны
Операция throw порождает большое количество байт кода. Лучшее ее использовать разумно.
Оптимизация по скорости
Использование java.lang.StringBuffer вместо оператора +
Особенно это эффективно когда конкатенация строк осуществляется в циклах. Обычно использование StringBuffer или StringBuilder увеличивает объем байт кода на несколько десятков байт.
О конкатенации я писал ранее в статье под названием Оптимизация кода: Конкатенация строк в Java . Там я сравнивал возможные варианты конкатенации строк, и на сколько быстро они работают в циклах с большим количеством итераций.
Использование inline (встраиваемых) методов
Метод будет встраиваемым, если он имеет один из следующих модификаторов final, static или private. Если ваш код тратит много времени на вызовы других методов, сделайте их final. Оптимально делать inline методами, методы в которых 1, максиму 2 операции, иначе значительный возможен рост байт кода. За счет встраиваемых методов стек вызовов становится короче, и виртуальной машине становиться немножко легче жить.
Например возьмем класс, из совета о использовании констант в интерфейсах и модифицируем его метод isA:
class IAmUsingConstatnts implements Const {
public final boolean isA(int value) {
return value == A;
}
}
Также к этому методу вместо модификатора final можно применить модификатор static, так как код, который содержится в нем не зависит от экземпляра объекта.
Не используйте глубокое наследование
Чем больше дерево наследования, тем труднее работать виртуальной машине, это важно для J2ME.
synchronized не всегда быстрая операция
Это конечно не относиться к потоку, который ждет освобождения другим потоком synchronized метода или блока. Но в любом случае виртуальная машина, делает дополнительные операции перед выполнением код в таких местах. Если обращение к synchronized методу будет в цикле, то цикл будет быстрее выполнятся если, метод будет не synchronized. Создавая многопоточные алгоритмы, и работая с потоками нужно не забывать об этом.
Выполнение на synchronized методе происходит быстрее чем на synchronized блоке
Так же, можно сказать и про байт код, для synchronized метода его меньше. Результаты декомпиляции утилитой javap двух вариантов кода - метод и блок:
| synchronized void method() { } | void method() { synchronized (this) { } } |
| 0: return | 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: aload_1 5: monitorexit 6: goto 14 9: astore_2 10: aload_1 11: monitorexit 12: aload_2 13: athrow 14: return |
При компиляции используйте оптимизацию компилятором javac -O
Используйте буферизацию в I/O
java.io.BufferedInputStream и java.io.BufferedOutputStream работает быстрее, чем обычные потоки I/O. Также можно отметить, что Java NIO это классы из java.nio.* работают быстрее, чем их предшественники из java.io, за счет асинхронности и меньшего количества синхронизации потоков.
Переопределяйте методы Java API
Если при использовании методов и классов из JDK ваше приложение испытывает проблемы в производительности, попробуйте создать класс наследник и переопределить метод, написав более эффективную версию кода.
Избегайте затратных конструкций кода
К таким конструкциям можно отнести оператор instanceof, если код критичен к скорости выполнения, желательного его не использовать. Как альтернатива, заменить его на оператор switch, а в классах для которых необходимо делать проверку типа добавить метод getType, который будет возвращать int константу.
Также к таким конструкциям относятся N мерные массивы, если есть необходимость, их заменяют на одномерные, а доступ к элементу массива производят по формуле, вычисляющей индекс.
Используйте двойную буферизацию (double buffering)
В J2ME приложения с графическим интерфейсом и Java апплетах, это позволит избавится от мигания графики при перерисовке графики. Рисуйте в закадровый буфер, а затем копируйте содержимое буфера в дисплей. При непосредственном рисовании в дисплей будет происходить мерцание изображения.
Оптимизация по использованию ресурсов
Не используйте временные переменные
Зачастую использование временной переменной обусловлено алгоритмом, но также бывают случай когда, использование временной переменной является избыточным. Не желательно использовать временные переменные в циклах.
Определяйте оптимальные размеры буферов
При использовании таких классов как java.util.ArrayList, java.langStringBuffer, java.io.ByteArrayOutputStream и других, в основе алгоритма работы которых стоит буферизация или динамическая работа с памятью, желательно указывать размер данных с которым будет происходить работа. Так как подобные классы обычно производят копирование памяти, если необходимо работать с данными большей длины, чем определенно по умолчанию, или было указанно программистом при создании объекта.
Приведу пример на основе java.util.ArrayList. У данной реализации списка на массиве, есть свойство capacity, его можно установить при инициализации объекта, предав в конструктор int значение. А вот по умолчанию, размер внутреннего массива задается значением 10 - это можно посмотреть набрав в поисковике запрос java.util.ArrayList source или другое имя класса, в первых строчках результатов будут страницы с исходными кодами.
Так вот, если будет выполнятся следующий код:
List<String> els = new ArrayList<String>();
for(int i = 0; i < 20; i++) {
els.add(String.valueOf(i));
}
то после первых 10 итерации, еще 10 раз будет произведена инициализация массива размером i + 1 и копирование старого массива в новый - 10 последних итерации будут выполнятся дольше, и займут больше памяти, чем первые 10.
В данном случае, список лучше инициализировать не конструктором по умолчанию, а конструктор с параметром int и указать размер внутреннего массива списка:
List<String> els = new ArrayList<String>(20);
for(int i = 0; i < 20; i++) {
els.add(String.valueOf(i));
}
Работайте с объектами, а не с их копиями
Желательно использовать методы работающие по такому принципу, и писать такие же.
Повторно используйте объекты
Это уменьшит затраты на память, а также затраты виртуальной машины на создание новых объектов.
Обновления информации о оптимизации
По мере узнавания мной новых способов оптимизации java приложений, буду обновлять содержимое страницы.
Теги:
Java
optimization
J2ME
java perfomance
obfuscator
proguard
оптимизация java