Java на пути в миниатюрный мир

Встраиваемые варианты платформы Java в некотором смысле интереснее, чем ее стандартная версия

В основу технологии Java положен принцип: “код, написанный однажды, должен работать везде” (WORA). Эта красивая формулировка заставляет многих верить в то, что на любой аппаратной или программной платформе Java даст в распоряжение пользователя идентичный набор функций, а написанное на Java приложение будет исполняться везде одинаково. Но такое представление верно лишь отчасти.

В реальности только набор байт-кодов, исполняемых виртуальной машиной Java (JVM), не зависит от платформы (и даже не весь набор, а лишь его часть - см. ниже). Для каждого типа аппаратуры и ОС существует своя JVM, обладающая специфическим набором возможностей, а на двух конкретных системах могут оказаться развернутыми две версии библиотек Java, совместимые не полностью. В качестве примера первого случая можно привести “частичную” поддержку Java на платформе Microsoft, а в качестве примера второго - хорошо известный факт, что некоторые методы классов Java 1.02 были помечены как “устаревшие” в версии Java 1.1 и не поддерживаются в Java 2.

И все же можно быть уверенным в почти полной переносимости кода между разными JVM, имеющими одинаковую версию и работающими на аппаратуре одного типа, а также в полной его переносимости, если при этом обе машины сертифицированы на 100%-ное соответствие требованиям Sun. Опыт последних лет показывает, что уже этого достаточно, чтобы серьезно сократить труд разработчика по переносу ПО с платформы на платформу и обеспечить Java большую популярность.

Все усложняется, когда речь идет о работе Java в маломощных устройствах. Скажем, невозможно развернуть полный набор классов Java Development Kit (JDK) объемом порядка 10 Мб на встраиваемой системе с ПЗУ 512 Кб.

Чтобы совместить такие несовместимые, казалось бы, вещи, Sun прилагала в последние годы поистине колоссальные усилия. Был избран единственно возможный путь - выделить несколько типов аппаратно-программных устройств и разработать заточенные под них среды исполнения Java и базовые библиотеки, из которых безжалостно удалены все бесполезные на слабых системах функции и вместо них добавлены некоторые новые, нужные. При этом важнейшим требованием остается обеспечение совместимости Java-кода снизу вверх.

На сегодняшний день создано четыре разные Java-платформы: JavaCard для смарт-карточек, Embedded Java для встраиваемых устройств, PersonalJava для потребительских устройств с графическим интерфейсом, а также стандартный JDK, предназначенный для настольных ПК, ноутбуков, рабочих станций, сетевых компьютеров, серверов и т. п. Самый современный вариант JDK имеет номер 1.2 (так называемая Java2). Sun развивает и старую платформу 1.1.x (текущая версия 1.1.8), перенося на нее некоторые из входящих в Java2 библиотек. Java2 имеет новую архитектуру JVM, позволяет использовать сторонние сборщики мусора, содержит дополнительные библиотеки для работы с CORBA, считавшиеся ранее необязательными.

К концу года Sun намечает создать еще один тип Java-платформы, специально для нужд корпораций - Java2 Enterprise Edition (J2EE). Она будет включать в себя Java 2, а также спецификации на серверные Java-технологии, такие, как Enterprise Java Beans, Java Server Pages, Java Transaction API и Java Transaction Service (последние две пока только разрабатываются).

Задача Sun заключается в выработке спецификаций и так называемой Reference platform, т. е. эталонного кода среды исполнения и библиотек. В начале года компания стала использовать схему “открытого сообщества разработчиков”, согласно которой любой желающий может получить доступ к исходному эталонному коду, модифицировать его, применять в своих разработках и представлять Sun свои предложения по его улучшению. Это способствовало ускорению развития Java и усилению влияния на нее других фирм (IBM, Novell и пр.).

В данной статье я не буду затрагивать обычный и “корпоративный” JDK, а остановлюсь на вариантах Java для встраиваемых устройств. Следует иметь в виду, что кроме указанных сред исполнения есть еще одна - JavaOS, полноценная операционная система, совместно разработанная Sun и IBM. Она позволяет запускать только Java-приложения, хотя, в отличие от всех упомянутых выше платформ Java, работает с оборудованием напрямую, не требуя наличия какой-либо иной базовой ОС. Но о ней я постараюсь рассказать в другой раз.

JavaCard

Самая младшая из платформ Java, пожалуй, является и самой оригинальной. Ведь типичная смарт-карточка имеет всего 1 Кб ОЗУ, 16 Кб энергонезависимой перезаписываемой памяти (EEPROM или флэш) и 24 - 32 Кб ПЗУ (см. табл. 1). Sun пришлось изрядно потрудиться, чтобы заставить Java работать в этом прокрустовом ложе. Игра стоила свеч: JavaCard решает серьезную проблему переносимости ПО на разнородные смарт-карточные платформы - многоплатформность дается разработчику на Java как нечто само собой разумеющееся. Благодаря ей открываются и новые возможности, например, на установленную в интеллектуальный сотовый телефон карту можно пересылать новое ПО прямо “по воздуху”, не заботясь о том, карта какого именно производителя используется.

Технология определяется тремя спецификациями (см. http://java.sun.com), описывающими виртуальную машину (интерпретатор байт-кода), среду исполнения JavaCard Run Time Environment (JCRE, текущая версия 2.1) и интерфейс прикладного программирования JavaCard API, обеспечивающий доступ из работающего в карточке аплета к JCRE и ее сервисам, таким, как служба передачи сообщений. На базе этого свода требований вендоры карточек могут создавать собственные реализации JavaCard.

В виду малого объема памяти на самой карте размещается всего несколько компонентов: базовая ОС, службы шифрования и управления памятью, виртуальная машина, классы JavaCard API и JCRE. Здесь отсутствуют механизмы сборки мусора, “полного завершения” (finalization) работы классов, клонирования объектов, динамической загрузки классов из исполняемых программ, почти все проверки прав доступа (вместо них предложено другое решение) и поддержка многопоточности. Последнее подразумевает, что поток исполнения в Java-карте только один: в нем работают все программы и JVM, и если где-то произошла ошибка, приводящая к завершению работы потока, то функционирование карточки временно прекратится (до ее “холодного” перезапуска).

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

Внутри карты аплеты отгорожены друг от друга непроницаемой стеной, именуемой в фирменной документации Sun брандмауэром (firewall). Он пресекает обращения аплетов к объектам, им не принадлежащим, кроме как в случаях, когда это оговорено особо (т. е. когда объект реализует специальный интерфейс Shareable Interface Object). Так достигается защита одной программы от неправильного функционирования другой. Заметим, однако, что программы могут свободно посылать сообщения друг другу и во внешний мир.

Наиболее примечательным в технологии JavaCard является то, что даже синтаксис языка Java в ней поддерживается не полностью. За ненадобностью убраны такие первичные типы данных, как char, double, float, long и все средства работы с ними, поддержка массивов размерностью больше единицы, ключевые слова native, synchronized, transient, volatile. Из первичных типов однозначно поддерживаются только boolean, byte, short. Тип int может поддерживаться или нет по желанию вендора карточки. К тому же вследствие исчезновения ряда типов данных был сокращен и набор команд байт-кода.

Также ограничено число используемых в классе интерфейсов, статических методов, полей и пр. Усложнилась и работа с памятью, так как сборка мусора не гарантирована спецификацией и созданный оператором new объект может существовать бесконечно долго. Для борьбы с этим в новый класс JCSystem были введены функции создания транзиентных (т. е. пропадающих с выключением питания) и постоянных (persistent) массивов данных.

Кардинально сокращены вспомогательные классы: пакет java.lang опустел почти целиком. В частности, исчезли классы String, SecurityManager, Thread, Boolean, Integer, Class, System и пр., а остались лишь сокращенные варианты классов Object (без интерфейса Cloneable) и Throwable (без большинства методов). Поддержка обработки исключительных ситуаций (exceptions) осталась, хотя число распознаваемых ситуаций уменьшено. Остальные библиотеки JDK устранены вовсе.

Однако JavaCard API характеризуется не только тем, что в нем удалено практически всё. Появились и новые специфические пакеты:

- javacard.framework содержит классы и интерфейсы, обеспечивающие базовую функциональность JavaCard-программ. В нем имеются классы аплета (Applet) и его идентификатора (AID); классы управления исполнением аплета, обмена данными с другими аплетами, управления ресурсами и транзакциями (JCSystem), взаимодействия между аплетом и внекарточными компонентами (APDU, Application Protocol Data Unit), хранения и управления PIN-кодами и т. п.;

- javacard.security включает средства для поддержки криптографических алгоритмов, хранения, генерации ключей и управления ими, а также обеспечения безопасности, формирования электронной подписи и дайджеста сообщения, генерации случайных чисел;

- javacardx.crypto состоит из классов и интерфейсов для шифрования данных. Все используемые в нем алгоритмы подлежат экспортному контролю при вывозе из США. В базовом варианте поддерживаются алгоритмы шифрования и электронной подписи с открытым ключом DSA и RSA.

Необычен класс Applet - он мало чем похож на Applet в стандартной Java. Здесь нет методов start и stop, зато есть методы install, process, select и deselect. Первый из них вызывается JCRE при установке аплета и должен либо выдать ошибку, либо вызвать метод register для подтверждения регистрации аплета на карте. Аплеты, зарегистрированные этим методом, живут до конца жизни карты, их состояние сохраняется в EEPROM.

Остальные методы ответственны за логику работы аплета, которая в JavaCard привязана к обработке сообщений протокола обмена данными (APDU-команд), поступающих из внешнего, в том числе “внекарточного”, мира. Метод process - главный, на него в основном падает обработка приходящих команд; select вызывается средой непосредственно перед обработкой пришедшей команды, и после этого аплет “пробуждается”; среда также обращается к deselect, когда аплет необходимо “усыпить” для вызова команды select у другого аплета. Согласно документации Sun, последний метод полезен, если нужно произвести расчистку ресурсов.

Еще одной жертвой, принесенной на алтарь смарт-карточных технологий, стал стандартный формат исполняемых классов Java (.class). Помимо исполняемого кода, он содержит и необходимую компилятору информацию о типах и именах элементов программы, т. е. служит в качестве документации. Такая информация бесценна, если применяются динамически загружаемые классы или производится верификация кода, но совершенно избыточна в JavaCard, где ответственность за эти операции возложена на систему, с которой производится запись кода в карту.

Поэтому в Java-карточку библиотека классов или аплет загружаются в специальном формате CAP (Converted APplet - преобразованный аплет). По сути своей CAP-файл - это просто JAR-архив, хранящий определяемый спецификацией набор компонентов, описывающих тот или иной аспект содержимого: заголовок, входящие в пакет классы, статические поля, байт-коды методов и т. п. Поэтому он вполне может содержать и другие CAP-файлы, как, например, в случае библиотеки классов.

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

EmbeddedJava

Эта технология предназначена для слабых в аппаратном плане устройств, в особенности тех, которые не имеют общецелевого дисплея, а стало быть, не нуждаются в графических библиотеках Java. Sun долго билась над созданием эталонной реализации EmbeddedJava: для этого пришлось сжать код библиотек и JVM так, чтобы они умещались в 512 Кб ПЗУ. Компании потребовалось на эту работу на год больше времени, чем планировалось первоначально, и результат был достигнут лишь к весне 1998-го. Исходный код среды исполнения был полностью переработан: например, разработчики довольно часто отказывались от констант типа int, заменяя их константами типа short или byte.

Вместе с тем данная разновидность Java гораздо меньше отличается от базового JDK, чем JavaCard. Она создана на основе принципа полной поддержки спецификаций языка Java и JVM. Однако в ней, в отличие от PersonalJava, не поддерживаются аплеты, значит, всегда допускается прямое обращение к “родным” методам управляющей устройством ОС реального времени (RTOS, Real-Time OS).

EmbeddedJava отличает высокая конфигурируемость - API текущей версии 1.1 получается из JDK 1.1.7 удалением любых классов, которые пользователь сочтет ненужными на своей платформе (удаление java.applet обязательно, см. табл. 2). Более того, допускается сокращение некоторых возможностей JVM, например отказ от поддержки динамической загрузки классов и проверки семантики. Правда, если вендор решит, что ему нужна какая-то одна из этих функций, он обязан обеспечить поддержку и другой. Для конфигурирования этой платформы Sun выпускает утилиту с незамысловатым названием JavaConfig.

Таблица 2. Пакеты JDK 1.1.x, включенные в Personal и Embedded Java

Использование Java в первую очередь обеспечивает комфорт разработчику ПО - ему меньше приходится принимать в расчет особенности конкретной RTOS, хотя абстрагироваться от нее полностью удается далеко не всегда. Спецификация предписывает вендорам при необходимости создавать специальные Java-библиотеки, служащие мостом между Java-программами и “родным” API RTOS.

В некоторых случаях, однако, ставка на Java оказывается неприемлемой. Так, объем кода JVM и библиотек EmbeddedJava все еще значителен, а сама технология тяготеет к 32-разрядной архитектуре и, стало быть, не подходит для применения на 4-, 8- или 16-разрядных устройствах. Помимо этого, JVM опирается на операционную систему при управлении потоками, памятью, периферийными устройствами и взаимодействия с сетями, что накладывает на RTOS серьезные ограничения. Следует также учесть, что программы для EmbeddedJava работают медленнее написанных на “родном” для конкретной платформы языке Си и для нормального функционирования могут потребовать слишком большого объема ОЗУ.

В придачу ко всему JVM весьма “оригинально” осуществляет очистку памяти: она откладывается до тех пор, пока память не окажется практически полностью исчерпанной, а на период проведения данной операции приостанавливается исполнение всех других потоков. Это снижает привлекательность использования Java для устройств реального времени, требующих гарантированного времени отклика. О ситуации с ними будет рассказано чуть ниже.

Для решения части описанных проблем придуманы достаточно эффективные способы, которые, кстати говоря, применяются не только в EmbeddedJava, но и в PersonalJava. Скажем, имеются инструменты, позволяющие определить, какие классы библиотек, продекларированных в import-выражениях Java, реально используются. В ПЗУ устройства затем записывается только то что нужно. Такие приложения либо сканируют исходный текст программ и строят список использованных классов, либо следят за исполнением кода в специальной тестовой среде. Первый (статический) метод менее точен, чем второй (динамический), так как может пропустить классы, загружаемые в ходе исполнения Java-программы с помощью метода Class.forName(). Обычно, для достижения наилучших результатов, одновременно используют оба подхода.

К числу таких программ относится Java Filter фирмы Sun, генерирующая список всех реально используемых в приложении полей, методов EmbeddedJava API, библиотек взаимодействия с “железом” и RTOS, предоставляемых производителем аппаратуры, других пакетов. Этот список можно использовать для устранения ненужных для конкретной платформы Java-классов.

Но даже при применении подобных средств разработчик на EmbeddedJava может столкнуться с проблемой перерасхода памяти. Дело в том, что JVM откладывает загрузку классов программы на самый последний момент. Это оправдывает себя в обычных компьютерах, считывающих .class-файлы из сети или с жесткого диска, но плохо подходит для систем с малым объемом ОЗУ, так как при этом JVM обязана разложить класс на компоненты, проверить его целостность, связать компоненты с другими классами и преобразовать байт-код в специальный “быстрый” внутренний формат, пригодный для интерпретации. Эта технология гарантирует, что JVM не станет работать с “враждебным кодом”, и обеспечивает возможность загрузки новых классов по команде из самой программы, но приводит к недопустимому для встраиваемых устройств расходованию ЦП и ОЗУ.

Некоторые производители предлагают специальные инструменты, условно называемые Romizers, для преодоления данного недостатка. Они генерируют из обычных Java-библиотек пригодные для размещения в ПЗУ образы классов во внутреннем формате JVM. Одним из таких инструментов является Java CodeCompact фирмы Sun, принимающий на вход пакеты .class-файлов и список, сгенерированный Java Filter. На выходе он выдает текст на языке Си, описывающий внутреннюю для JVM структуру программы. Вместе с CodeCompact естественно применять и другую утилиту - Java DataCompact, упаковывающую все используемые Java-программой файлы данных еще в один Си-файл.

Полученные в результате тексты транслируются “родным” для RTOS компилятором и связываются (линкуются) с ее системными библиотеками, образуя единый образ (image) ПЗУ. Заметим, что при этом устраняется множество дублирующих констант, ненужных полей данных, избыточных таблиц имен классов, снижается использование стека, и т. п. Преобразованные программы, код которых JVM может исполнять напрямую из постоянной памяти, называются ROMlet. В документации Sun (см. PersonalJava 1.1 Application Environment Memory Usage Technical Note) указано, что объем ОЗУ, занятый под внутренние структуры данных JVM, при таком преобразовании может уменьшаться в 25 - 30 раз, а объем кода самой программы - на 30%.

Зачастую разработчикам приходится учитывать возможность модификации ROMlet уже после его записи в ПЗУ. Для этой цели предусмотрен особый, допускающий обновления тип исполняемых модулей, которые дополнительно содержат таблицу, хранящую ссылки на объекты программы. Помещаемые во флэш-память заплатки к таким программам содержат аналогичную таблицу и коды модифицированных или добавленных классов. Опираясь на данные этих двух таблиц, JVM определяет, когда нужно взять старый код из ПЗУ, а когда обновленный из флэш-памяти.

Важным методом повышения производительности Java-программ является компиляция их в родной код RTOS. Этот подход также делает исполнение программы более гладким и позволяет совместно применять в одном проекте Java, Си и Си++. Но он приводит к “разбуханию” кода и не избавляет от JVM - она все равно нужна для сборки мусора и поддержки многопоточности. Кроме того, теряются все преимущества Java, в том числе возможность простой установки “заплат” для кода.

Можно и другим способом ускорить работу программ - перепроектировать устройство с применением микросхемы picoJava, предназначенной для быстрой обработки байт-кода Java. Но этот путь далеко не всегда желателен, хотя и сохраняет все преимущества Java в максимальной степени.

Теперь вернемся к системам реального времени. EmbeddedJava ориентирована прежде всего на них, но как раз здесь и возникли серьезные трудности. В целом их можно охарактеризовать как проблемы непредсказуемого времени реакции Java-приложений.

Непредвиденное начало сборки мусора и блокирование сборщиком работы всей системы, сложное управление исполнением программ, отсутствие средств анализа исполнения, непрогнозируемое потребление ресурсов Just in Time-компиляторами и другими оптимизаторами, непредсказуемые взаимодействия потоков, связанные с плохо документированной структурой приоритетов исполнения, неконтролируемой сменой виртуальной машиной приоритетов потоков, недостаточные возможности языка Java по описанию сообщений, семафоров, прерываний, сигналов ввода-вывода, граничных времен и т. п. - вот список самых главных проблем, стоящих перед разработчиками EmbeddedJava-систем.

И хотя первые инкрементные сборщики мусора, обеспечивающие плавную очистку памяти, уже появились, борьба за выработку спецификаций расширений Java, устраняющих проблемы непредсказуемого времени исполнения пока только начинается. Ясно, что кто первым найдет устраивающее все заинтересованные стороны решение, тот и будет контролировать рынок. Вполне естественно, что созданы целых две отраслевые группы - одну возглавляют компании Sun и IBM (Requirements Group), а другую - Microsoft и Hewlett-Packard (Real-Time Extensions Group и связанный с ней J Consortium). Напомним, что в прошлом году HP создала свою собственную реализацию технологии EmbeddedJava, именуемую Chai и не опирающуюся на базовый код Sun.

PersonalJava

Данная версия платформы Java, называемая также Java Applet Environment (среда исполнения аплетов), адаптирована для подключаемых к сети передачи данных и оснащенных графическим экраном потребительских устройств. Ими могут быть “интеллектуальные” сотовые телефоны, персональные цифровые секретари, телевизоры нового поколения, Web-телефоны, игровые приставки и т. п.

PersonalJava Application Environment (PJEA) - более зрелая платформа, чем EmbeddedJava. Первая версия спецификации вышла в сентябре, а “эталонная реализация” - в декабре 1997 г. В отличие от EmbeddedJava, вызвавшей неоднозначную реакцию, PersonalJava была почти сразу принята отраслью. Однако некоторые идеи EmbeddedJava, связанные с уменьшением объема кода и используемой памяти, нашли применение при переходе к новой, пока текущей, версии PersonalJava 1.1 (версия “эталонной реализации” - 3.0).

В основу PJEA были заложены две идеи - во-первых предоставить инструмент, с помощью которого можно было бы разрабатывать на Java программы, максимально переносимые между устройствами описанного типа, и, во-вторых, сократить потребление ресурсов памяти виртуальной машиной и системными библиотеками. Реализация первой идеи гарантирует, что встраиваемые приложения не придется создавать с нуля, а можно собирать их из многократно используемых компонентов, на производителей которых придется основная тяжесть всей разработки. Такой подход экономит средства и время сборщиков приложений (в основном поставщиков оборудования и конкретных решений). Осуществление второй идеи дает надежду, что потребительское устройство не окажется слишком дорогим, ведь цена памяти до сих пор существенно влияет на конечную стоимость продукта и в итоге на его конкурентоспособность.

Для “облегчения” системы Sun оптимизировала исходные тексты эталонной реализации и проанализировала в новом контексте полезность некоторых библиотек JDK. Каждому пакету, классу и методу была дана одна из следующих оценок: Required (обязательный), Unsupported (не поддерживается), Optional (включается по желанию) и Modified (изменен). Последняя характеристика указывает на то, что изменены структура библиотеки, синтаксис методов или добавлены новые классы, необходимые для поддержки какой-либо специфической для малых устройств функции.

К категории “не поддерживается” отнесены все классы, необходимость которых на потребительских платформах сомнительна. Однако в PersonalJava API были добавлены некоторые новые компоненты, нужные для подобных систем.

В результате библиотека Abstract Window Toolkit (AWT) была оптимизирована для работы с маленькими дисплеями, в API введена поддержка сенсорных экранов, джойстика, пультов дистанционного управления; отдельно выпущены коммерческие модули для отображения HTML (например, встраиваемый браузер Sun HotJava). В PJAE появились специфические классы для работы с таймером и интерфейсы KeyboardInputPreferred, ActionInputPreferred, PositionalInputPreferred, NoInputPreferred, определяющие тип предпочтительного устройства ввода - клавиатура, кнопочная панель, позиционно-чувствительная панель или его (предпочтения) отсутствие. Появилось понятие Minimum PJAE - среда, откуда удалены все необязательные компоненты, но где должны поддерживаться все PAJE-специфические компоненты.

В итоге структура API получилась довольно сложной. Кроме того, версия спецификации 1.1 заметно отличается от версии 1.0. Скажем, пакеты java.net и java.util в новой версии поддерживаются полностью, а многие другие библиотеки, которые не поддерживались в версии 1.0, теперь отмечены как включаемые по желанию. Для облегчения жизни разработчика Sun была вынуждена выпустить специальный инструмент JavaCheck, проверяющий соответствие создаваемого пользователем приложения требованиям спецификации PersonalJava (и, кстати, EmbeddedJava - вся разница в файле, хранящем описание спецификации), а также специальную среду эмуляции, позволяющую исполнять PersonalJava-приложения в тестовом режиме на ПК или рабочей станции. Для оптимизации же использования ОЗУ и ПЗУ в PersonalJava рекомендуются ровно те же методы, что и для EmbeddedJava.

Реальность и перспективы

Sun Microsystems перекрыла своими платформами существенную часть спектра малых устройств. Создавать ПО для них все еще сложно, но ситуация стремительно изменяется по мере появления написанных сторонними производителями компонентов, из которых можно собирать готовые программы, - ведь для их создания достаточно скомпилировать и отладить код всего один раз.

На рынке потребительских устройств PersonalJava уже нашла свое место, ассоциация Visa объявила о поддержке JavaCard в новой среде Visa Open Platform, а сами Java-карты выпускаются миллионами штук. EmbeddedJava была встречена более сдержанно, частично из-за насыщенности рынка программных средств для встраиваемых устройств, а частично из-за проблем с работой Java в реальном времени. Однако нет сомнений, что, как только эти проблемы будут решены, спрос на EmbeddedJava вырастет. Появление Chai - альтернативной встраиваемой Java-машины компании HP, и формирование двух групп, соперничающих в выработке нового стандарта на расширения реального времени, говорит в основном о хороших перспективах этой технологии в большом секторе рынка, за который, собственно, и идет борьба.