Санкт-Петербургская группа тестирования JVM


« Sun Java Real-Time... | Main | jdksearch.com »
20070803 пятница Август 03, 2007

Sun Java Real-Time System 2.0

В связи с недавним выходом Sun Java™ Real-Time System 2.0, реализации Real-Time Specification for Java (JSR–001), хочется немного рассказать о некоторых особенностях новой виртуальной машины и об использовании API, предоставляемого спецификацией The Real-Time Specification for Java (RTSJ).

Что мешает Java быть подходящей платформой для создания приложений реального времени? Основные причины следующие: для Java не специфицирован алгоритм планировки потоков и, как правило, используется алгоритм, основанный на разделении времени. Хотя API и позволяет задавать приоритеты потоков, но даже для потока с наивысшим приоритетом не гарантируется, что он не может быть вытеснен в любой момент потоком с приоритетом ниже. Кроме того, в Java память приложения управляется сборщиком мусора, и поэтому любой поток может быть приостановлен в произвольный момент на время, необходимое для очистки памяти, что неприемлемо для систем реального времени (помимо названных причин, работа потока может быть прервана на неопределённое время из-за загрузки необходимого для работы класса или для выполнения just-in-time компиляции, но в этой статье мы не будем рассматривать эти источники непредсказуемого поведения).

Каким образом спецификация RTSJ решает эти проблемы?

Первое важное нововведение RTSJ это предоставление новых классов потоков реального времени: RealtimeThread и NoHeapRealtimeThread, для которых должен использоваться алгоритм планировки, при котором гарантируется, что потоку с наибольшим приоритетом всегда будет предоставлено процессорное время (fixed priority pre-emptive scheduling), при этом минимальный приоритет для потоков реального времени выше максимального приоритета обычных потоков Java. Это довольно серьёзное отличие от обычной реализации Java, о котором обязательно следует помнить при использовании этих классов.

Рассмотрим, например, следующую программу:

public static void main(String[] args) {
Thread thread = new RealtimeThread() {
public void run() {
// выполнить длительные вычисления
}
};
thread.start();

thread.setName("RealtimeThread");
}

Если запустить её на однопроцессорной машине, то поток, выполняющий метод main, будет вытеснен сразу же после вызова метода start потоком на основе класса RealtimeThread, так как последний имеет приоритет реального времени. Стандартный поток java.lang.Thread не сможет выполнить метод setName до тех пор, пока RealtimeThread сам не освободит процессор (а это может случиться и в том случае, когда поток завершит выполнение, и в этом случае вызов метода setName, скорее всего не будет иметь смысла). Очевидно, что такое поведение кардинально отличается от поведения обычных потоков Java.

Другая важная часть RTSJ—это введение новых областей памяти, отличных от обычной кучи (heap), используемой в Java. Работа с этими областями памяти осуществляется с помощью класса ScopedMemory. Объекты ScopedMemory должны создаваться непосредственно программистом, например:

// создать область памяти размером 1MB (LTMemory - стандартный подкласс ScopedMemory)
ScopedMemory scopedMemory = new LTMemory(1024 * 1024);
Создаваемая таким образом память не управляется сборщиком мусора. Очищаются эти области памяти в тот момент, когда не остаётся работающих в них потоков. Рамки использования памяти задаются синтаксически: поток использует ScopedMemory, пока он выполняет метод ScopedMemory.enter. Также надо отметить, что области ScopedMemory доступны только для потоков реального времени.

Память, созданная в предыдущем примере, может быть использована следующим образом:

Runnable runnable = new Runnable() {
public void run() {
// выполнить необходимые вычисления
}
};

while (!threadStoped) {
scopedMemory.enter(runnable);
}

Действия, задаваемые объектом runnable, выполняются в scopedMemory: то есть все объекты, создаваемые в рамках метода run, будут размещаться не в общей куче, а в созданной scopedMemory, причём в конце каждой итерации цикла while scopedMemory будет очищаться, так как после выхода из метода ScopedMemory.enter не остаётся потоков, использующих объект scopedMemory. Главное в данном случае—создать область памяти, размера которой будет достаточно для работы Runnable.

Применяя ScopedMemory вместе с классом NoHeapRealtimeThread, можно избежать влияния сборщика мусора на критическую часть приложения. Спецификация гарантирует, что поток, основанный на классе NoHeapRealtimeThread, не должен приостанавливаться в результате работы сборщика мусора. Это не гарантируется спецификацией для потоков RealtimeThread, однако, как можно понять из названия, использование класса NoHeapRealtimeThread связано с одним серьёзным ограничением: этот тип потоков не может использовать объекты, созданные обычной куче. За счёт этого и достигается возможность избавиться от взаимодействия со сборщиком мусора. Если во время работы программы поток NoHeapRealtimeThread попытается использовать объект, созданный в куче, то будет сгенерировано специальное исключение javax.realtime.MemoryAccessError. Таким образом NoHeapRealtimeThread должен использовать новые области памяти: или ScopedMemory, или ещё один тип памяти—ImmortalMemory.

ImmortalMemory имеет ряд отличий от ScopedMemory: эта память доступна всем типам потоков, включая потоки java.lang.Thread. Существует только один экземпляр ImmortalMemory, который создаётся во время инициализации виртуальной машины и уничтожается при завершении работы. И главное отличие: объекты, созданные в этой области памяти, никогда не удаляются. Основное назначение ImmortalMemory—упростить взаимодействие потоков на основе NoHeapRealtimeThread с другими типами потоков и с подсистемой исполнения (runtime system) JVM. Например, все объекты java.lang.Class создаются в ImmortalMemory и за счёт этого доступны для потоков NoHeapRealtimeThread. Но так как эта память не очищается на протяжении работы виртуальной машины, она является довольно ограниченным ресурсом и в основном предназначена только для хранения неизменяемых данных.

Применение потоков NoHeapRealtimeThread и новых областей памяти помогает избавиться от проблем, связанных со сборщиком мусора. Но у этого решения есть ряд недостатков, связанных с особенностями использования ScopedMemory. Это область памяти с ограниченным временем жизни, при этом время жизни памяти определяется синтаксическими границами метода ScopedMemory.enter. При завершении данного метода все объекты, созданные в этой области памяти, могут быть уничтожены. Поэтому из соображений безопасности спецификация RTSJ запрещает использовать объекты, размещённые в ScopedMemory, за пределами метода ScopedMemory.enter. В противном случае поток, получивший ссылку на объект из ScopedMemory и не выполняющий при этом метод ScopedMemory.enter, не может гарантировать, что этот объект не будет уничтожен в произвольный момент, а использование объекта, уничтоженного в неподходящее время, может привести к краху приложения.

Для гарантии выполнения этого ограничения RTSJ вводит ряд правил, которые должны выполняться при работе со ссылками. Так, объекты, созданные в куче и ImmortalMemory, не могут ссылаться на объекты, созданные в ScopedMemory. То есть объекты с большим временем жизни не могут ссылаться на объекты с потенциально меньшим временем. Нарушение этого правила во время выполнения приведёт к генерации исключения javax.realtime.IllegalAssignmentError.

Эти правила серьёзно усложняют использование ScopedMemory. Hапример, если результат работы потока, использующего память ScopedMemory, создается в этой же памяти, то результат не может быть непосредственно передан потокам, работающим в других областях. Кроме этого, при работе в ScopedMemory могут возникнуть проблемы при использовании существующих библиотек. Рассмотрим довольно широко используемый метод static double java.lang.Math.random(). В JDK1.5 этот метод реализован следующим образом:

class Math {
...
public static double random() {
if (randomNumberGenerator == null) initRNG();
return randomNumberGenerator.nextDouble();
}

private static Random randomNumberGenerator;

private static synchronized void initRNG() {
if (randomNumberGenerator == null)
randomNumberGenerator = new Random();
}
...
}
Если при вызове метода Math.random генератор случайных чисел randomNumberGenerator не инициализирован, то он создаётся и сохраняется в статическом поле для возможного использования в будущем. Что произойдёт, если Math.random будет первый раз вызван из ScopedMemory? Согласно спецификации RTSJ все статические поля создаются в ImmortalMemory, поэтому при выполнении строки 'randomNumberGenerator = new Random();' произойдёт попытка присваивания объекта, только что созданного в ScopedMemory, ссылке, созданной в ImmortalMemory. В результате получим исключение IllegalAssignmentError:
javax.realtime.IllegalAssignmentError
at java.lang.Math.initRNG(Math.java:668)
at java.lang.Math.random(Math.java:693)
Конечно, этой ошибки можно легко избежать, если перед вызовом Math.random в ScopedMemory вызвать этот метод в какой-либо другой области памяти. Но представьте ситуацию, когда генератор случайных чисел был инициализирован потоком, работающим в общей куче, и должен после этого использоваться в потоке NoHeapRealtimeThread, в этом случае NoHeapRealtimeThread просто не сможет использовать метод Math.random, так как всякий раз будет получать MemoryAccessError:
javax.realtime.MemoryAccessError
at java.lang.Math.random(Math.java:693)
Cоздание статических объектов для общего использования и кэширование результатов широко используется, например, в стандартных библиотеках Java для повышения производительности, и было бы нецелесообразно избавляться от всех этих оптимизаций, так как это бы привело к существенному снижению скорости работы приложений, использующих Sun Java Real-Time System. Таким образом, работая в ScopedMemory надо быть готовым к тому, что некоторые классы не могут использоваться.

Надо отметить, что использование новых областей памяти и потоков NoHeapRealtimeThread—это единственный способ обойти задержки, вызванные сборщиком мусора, предлагаемый спецификацией RTSJ. Но в тоже время, описанные проблемы делают использование этого способа не очень привлекательным для разработчиков и серьёзно усложняют процесс добавления функциональности реального времени в существующие приложения. К счастью, реализация RTSJ компании Sun предлагает гораздо более лёгкий в использовании способ для организации взаимодействия сборщика мусора с потоками реального времени. Это сборщик мусора реального времени (Realtime Garbage Collector или RTGC).

Виртуальная машина Sun Java™ Real-Time System 2.0 предоставляет два сборщика мусора:

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

Поведение RTGC определяется рядом параметров (значения всех параметров могут быть при необходимости изменены):

Приоритет сборщика мусора в нормальном режиме по умолчанию—это минимальный приоритет реального времени. Как только размер свободной памяти приложения становится меньше NormalMinFreeBytes, сборщик мусора начинает работу в нормальном режиме, при этом он может приостановить только обычные потоки java.lang.Thread, приоритет которых не может быть установлен больше или равным минимальному приоритету реального времени. Все же потоки на основе javax.realtime.RealtimeThread, имеющие приоритет выше минимального, не будут затронуты.

Если в нормальном режиме сборщику мусора не удаётся получить достаточно процессорного времени для очистки памяти (из-за активности пользовательских потоков реального времени), и размер свободной памяти становится ниже CriticalMinFreeBytes, то сборщик мусора переходит в критический режим работы. Его приоритет повышается до значения RTGCCriticalPriority. В этом случае могут быть приостановлены как обычные потоки Java, так и потоки реального времени с приор������тетом ниже RTGCCriticalPriority.

Исходя из такого алгоритма работы сборщика мусора реального времени, при проектировании приложения необходимо выделить три группы потоков:

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

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

Как видите, при использовании сборщика мусора реального времени, нет необходимости применять потоки NoHeapRealtimeThread. Все действия, чувствительные к работе сборщика мусора, могут безопасно выполняться в потоках RealtimeThread, использующих общую кучу. Однако ScopedMemory всё же может быть полезна для более точного управления памятью, доступной потоку. Например, можно предоставить каждому критическому потоку собственную область памяти с фиксированным размером, которая не будет использоваться никакими другими потоками и сам критический поток гарантированно не сможет выйти за рамки потребляемой им памяти.

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

Пусть наша программа должна решать следующую задачу, довольно типичную для приложений реального времени: набор некоторых действий, которые должны выполняться с заданной периодичностью, например, каждые 100 миллисекунд. В реальном приложении это может быть, скажем, проверка показаний каких-нибудь датчиков. При этом, введем ограничение на крайний срок выполнения этой работы (deadline): например, выполняемые действия обязательно должны быть завершены за 50 миллисекунд. А чтобы еще усложнить задачу, добавим в нашу программу обычный поток Java, постоянно потребляющий память и тем самым провоцируя работу сборщика мусора.

RTSJ предоставляет API для создания подобных приложений: пакет javax.realtime. Для обеспечения периодичности выполнения, поток реального времени, во-первых, должен быть создан с использованием класса PeriodicParameters, который позволяет задать период и крайний срок выполнения работы:

// значение периода в миллисекундах
final long periodValue = 100;

// период выполнения действий
RelativeTime period = new RelativeTime(periodValue, 0);

// за какой срок поток должен успеть завершить работу
RelativeTime deadline = new RelativeTime(periodValue / 2, 0);

PeriodicParameters periodicParameters = new PeriodicParameters(null, period, null, deadline, null, null);
Периодический поток будет использовать метод RealtimeThread.waitForNextPeriod, который блокирует вызывающий поток до наступления следующего периода. Значение, возвращаемое RealtimeThread.waitForNextPeriod, позволяет определить, успел ли поток завершить работу в заданный срок:
while (thereIsWork) {
doWork();

boolean deadlineWasMissed = !RealtimeThread.waitForNextPeriod();

if (deadlineWasMissed) {
// обработать ситуацию, когда поток не успел выполнить работу
}
}

Сначала напишем наше приложение с использованием классов NoHeapRealtimeThread и ScopedMemory. При этом нет необходимости задавать потоку реального времени высокий приоритет, просто необходимо организовать приложение так, чтобы вся необходимая работа выполнялась в ScopedMemory (полный текст программы: NoHeapRealtimeThreadExample.java):

class NoHeapRealtimeThreadExample {
...
static class PeriodicThreadInScopedMemory implements Runnable {
// память, в которой работает поток
ScopedMemory scopedMemory = new LTMemory(1024 * 1024);

// класс, задающий действия, выполняемые потоком (реализует интерфейс Runnable)
CalculationLogic calculationLogic = new CalculationLogic();

public void run() {
for (int i = 0; i < 100; i++) {
scopedMemory.enter(calculationLogic);

// ждём начала следующего периода и проверям, была ли работа завершена в срок
boolean deadlineWasMissed = !RealtimeThread.waitForNextPeriod();

if (deadlineWasMissed) {
System.out.println("Поток не успел завершить работу (итерация: " + i + ")");
break;
}
}
}
}

public static void main(String[] args) {
// запускаем поток Java, постоянно провоцирующий работу сборщика мусора
new GarbageProducer().start();

// NoHeapRealtimeThread не может быть создан в HeapMemory, поэтому входим в ImmortalMemory
ImmortalMemory.instance().executeInArea(new Runnable() {
public void run() {
final long periodValue = 100;
RelativeTime period = new RelativeTime(periodValue, 0);
RelativeTime deadline = new RelativeTime(periodValue / 2, 0);
PeriodicParameters periodicParameters = new PeriodicParameters(null, period, null, deadline, null, null);

/*
* Создаём периодический поток, который начинает работу в ImmortalMemory,
* логика работы потока задаётся классом PeriodicThreadInScopedMemory
*/
Thread thread = new NoHeapRealtimeThread(
null,
periodicParameters,
null,
ImmortalMemory.instance(),
null,
new PeriodicThreadInScopedMemory());

thread.start();
...
}
}

Запустим программу и убедимся, что она и в самом деле работает так, как ожидалось: поток реального времени всегда укладывается в заданные временные рамки. Можно также немного поэкспериментировать и посмотреть, что будет, если заменить NoHeapRealtimeThread на RealtimeThread. Для этого достаточно поменять всего одну строчку. В этом случае сборщик мусора довольно быстро нарушает работу потока:

java -XX:-UseRTGC NoHeapRealtimeThreadExample
Поток не успел завершить работу (итерация: 12)
Обратите внимание, что для запуска должна использоваться опция -XX:-UseRTGC, при которой используется обычный сборщик мусора.

Теперь решим эту же задачу, используя возможности сборщика мусора реального времени. В этом случае не надо использовать NoHeapRealtimeThread и ScopedMemory, а вместо этого просто надо задать высокий приоритет потоку, выполняющему действия, которые не должны прерываться сборщиком мусора, для этого можно, например, использовать класс PriorityParameters (полный текст программы: RTGCExample.java).

class RTGCExample {
...
static class PeriodicThread implements Runnable {

CalculationLogic calculationLogic = new CalculationLogic();

public void run() {
for (int i = 0; i < 100; i++) {
calculationLogic.run();

// ждём начала следующего периода и проверям, была ли работа завершена в срок
boolean deadlineWasMissed = !RealtimeThread.waitForNextPeriod();

if (deadlineWasMissed) {
System.out.println("Поток не успел завершить работу (итерация: " + i + ")");
break;
}
}
}
}

public static void main(String[] args) {
// запускаем поток Java, постоянно провоцирующий работу сборщика мусора
new GarbageProducer().start();

final long periodValue = 100;
RelativeTime period = new RelativeTime(periodValue, 0);
RelativeTime deadline = new RelativeTime(periodValue / 2, 0);
PeriodicParameters periodicParameters = new PeriodicParameters(null, period, null, deadline, null, null);

// задаём потоку приоритет, больше критического приоритета сборщика мусора
final int criticalPriority = 41;
/*
* Создаём периодический поток с приоритетом 41, работающий в HeapMemory,
* логика работы потока задаётся классом PeriodicThread
*/
Thread thread = new RealtimeThread(
new PriorityParameters(criticalPriority),
periodicParameters,
null,
HeapMemory.instance(),
null,
new PeriodicThread());

thread.start();
...
}
}
При запуске указываем критический приоритет сборщика мусора и размер памяти, зарезервированный для критического потока:
java -XX:RTGCCriticalPriority=40 -XX:RTGCCriticalReservedBytes=1M RTGCExample
С этой программой можно также провести эксперимент и посмотреть, что будет, если задать приоритет сборщика мусора равным 42 (больше, чем у потока, выполняющего критические к задержкам действия):
java -XX:RTGCCriticalPriority=42 -XX:RTGCCriticalReservedBytes=1M -cp bin/classes/ RTGCExample
Поток не успел завершить работу (итерация: 4)

Думаю, из приведённых примеров видно, что RTSJ не трудна в использовании и сереъезных изменений кода для добавления функциональности реального времени в существующие приложения, скорее всего, не потребуется. Sun Java™ Real-Time System 2.0 призвана существенно облегчить сложный процесс создания приложений реального времени, делая доступными для их разработчиков все преимущества платформы Java.

Ссылки

Семён Бойков

опубликовал vmrobot ( авг 03 2007, 08:08:34 PM MSD ) Permalink Комментарии [2]

Trackback URL: http://blogs.sun.com/vmrobot/entry/%D0%BA%D0%BE%D1%80%D0%BE%D1%82%D0%BA%D0%BE_%D0%BE_sun_java_real
Комментарии:

А как осуществляется передача данных от realtime потока к некритичному? Допустим, realtime поток генерирует events, которые складывает в очередь и которые должны будут обработаться позже некритичным потоком? Тут вижу еще одну проблему - при герерации события realtime поток должен будет вызвать синхронизироваться с обычным потоком, который в свою очередь уже может быть заблокирован GC.
Далее, немного странно, что NoHeapRealtimeThread может существовать только в ImmortalMemory. Если я динамически делаю короткоживущие realtime потоки, все инстанции будут занимать память даже после прекращения работы?
Вообще, я думал, архитекторы пойдут более привычным путем - добавят функцию удаления объекта и освобождения памяти как в сях.

опубликовал null Август 08, 2007 at 03:00 PM MSD #

"Тут вижу еще одну проблему - при герерации события realtime поток должен будет вызвать синхронизироваться с обычным потоком, который в свою очередь уже может быть заблокирован GC."

Для решения проблем с синхронизацией предлагаются следующие способы:
- в API включён класс WaitFreeWriteQueue (и аналогичный ему WaitFreeWriteQueue), метод записи в очередь WaitFreeWriteQueue не синхронизированный и не приводит к блокировки потока, выполняющего запись
- инверсия приоритета: в том случае, если realtime поток пытается получить блокировку, захваченную не-realtime потоком (или realtime потоком с меньшим приоритетом), то приоритет потока, удерживающего блокировку будет автоматически повышен до приритета потока, ожидающего блокировку

"Далее, немного странно, что NoHeapRealtimeThread может существовать только в ImmortalMemory."

NoHeapRealtimeThread также может быть создан в ScopedMemory

опубликовал Семён Август 08, 2007 at 04:07 PM MSD #

Опубликовать комментарий:

Имя
E-Mail:
URL:

Ваш комментарий:

HTML Syntax: Отключен

Хиты страниц за сегодня: 45