Программирование JNI в AIX

Рекомендации и советы

Данная статья является общим руководством по разработке приложений, основанных на интерфейсе Java Native Interface (JNI), с помощью IBM JDK для AIX, прежде всего IBM JDK 1.4.1 для AIX. Там, где это необходимо, указывается на особенности 32- и 64-битовых платформ для JDK 1.4.1 . Несколько слов сказано о программировании JNI на платформе Java II SDK от IBM. Эта статья задумана не как руководство по API-интерфейсам JNI, а как краткое описание специфики JNI-программирования на AIX. Предполагается, что читатель знает спецификацию JNI и умеет использовать JNI.

Николай Эвик, технический консультант Linux on POWER, IBM  

Николай Эвик (Nikolay Yevik ) - консультант по Linux на платформе POWER из группы Solutions Enablement компании IBM. Он более 5 лет работает с платформами UNIX и программирует на C, C++ и Java. Николай получил степени магистра в области нефтепереработки и компьютерных наук. С ним можно связаться по адресу yevik@us.ibm.com.



08.10.2009

Введение

В этой статье процедуры, написанные на C/C++, именуются "родными" (платформенно-зависимыми) функциями. При программировании на Java с использованием JNI API не требуется, чтобы существующие "родные" библиотеки, возможно, устаревшие, были полностью обновлены, включая "родные" системные библиотеки. JNI API можно использовать главным образом двумя способами:

  • создание библиотеки, которую можно вызывать из Java-приложения;
  • интеграция виртуальной машины Java (JVM) в приложение на C/C++ и выполнение Java-кода при помощи интерфейса JNI.

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

  • C и/или C++,
  • Java,
  • JNI API,
  • ОС AIX,
  • компиляторов AIX C/C++.

Мощность и гибкость JNI обеспечиваются за счет его возможностей портирования. Принцип "Написать единожды и запускать везде" (WORA - "Write Once, Run Anywhere", лозунг Sun Microsystems Inc), конечно, не действует, когда на сцене возникает JNI. Спецификации JNI разработаны Sun Microsystems Inc. Только в версии 1.0 спецификаций JNI "родные" методы были разработаны специально для JVMот Sun. Начиная от версии 1.1 спецификации JNI определяют нейтральный для JVM интерфейс "родных" методов, доступ к переменным и вызов методов. Тем не менее версии компилятора и возможности создания библиотек с совместным доступом с помощью JNI API отличаются на разных платформах. Более важным является то, что спецификации JNI не навязывают способ реализации JNI, и JNI реализуется различными способами в зависимости от поставщика JVM. Конструктор совместимости Java (JCK) от Sun и спецификация JNI предполагают соответствие спецификации, но не способу реализации. Могут возникнуть проблемы для кода, создатели которого при написании ориентировались на метод выполнения, а не точно следовали спецификации.


Основные положения программирования JNI

Этот раздел описывает основные пункты в программировании в JNI - производительность, обработку исключений и управление памятью.

Производительность JNI

Обычно для увеличения производительности части приложений, в особенности интенсивно используемых, пишутся в "родном" коде и связываются с Java. Тем не менее, взаимодействие между JVM и "родным" кодом обычно происходит медленно, и слишком большое число вызовов JNI может уменьшить производительность. К такому же результату может привести и обработка исключений в самом "родном" коде JNI.

Исключительные ситуации JNI

Обработка исключений в платформенно-зависимом коде может потребоваться при внедрении "родного" кода в Java. JNI предлагает "родные" функции для обработки исключительных ситуаций, а также для передачи исключений обратно в JVM. Обработка исключений в "родном" коде может снизить производительность, но является необходимым этапом в случае, если JNI-функция не выполняется успешно, а возвращает ошибку. Для обработки исключений в самом JNI-коде обычно используются следующие JNI-функции: ExceptionOccurred(), IsInstanceOf(), ExceptionCheck() и ExceptionClear() - для "зачистки" исключительной ситуации. Эти функции весьма затратны с точки зрения вычислений, ExceptionCheck() является менее "дорогостоящей", чем ExceptionOccurred(), так как последняя должна создавать объект, на который надо ссылаться, наряду с локальной ссылкой.

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

Платформенно-зависимая память и Java-память

Использование "родного" кода увеличивает вероятность утечки памяти. Сборщик мусора JVM (garbage collector, GC) больше не защищает программистов от необходимости освобождения "родных" ресурсов после использования. Помогают в этом некоторые функции JNI API: ReleaseStringUTFChars(), ReleaseStringChars(), DeleteGlobalRef(), DeleteLocalRef() и т.д.

Локальные ссылки
Ссылки из "родного" кода на Java-объекты в динамической памяти Java (Java-"куче") могут быть в формате, не доступном сборщику мусора для анализа. JNI автоматически создает локальную ссылку на любой объект, на который производится какая-либо ссылка из "родного" кода в динамической памяти Java, фактически это указатель на адрес в Java-"куче", созданный в соответствующем стеке потока. Локальные ссылки действительны, пока продолжается вызов "родной" функции. Они автоматически освобождаются, когда исчезают из "кругозора" "родной" функции, которая возвращает результат, но передаются всем остальным функциям в стеке. Локальная ссылка передается всем функциям, которые были вызваны внутри функции, изначально создавшей эту ссылку.

При работе с локальными ссылками есть две известные проблемы

Потеря локальной ссылки
Когда происходит завершение выполнения метода, в котором была создана локальная ссылка, локальная ссылка уходит из диапазона действия. Объект все еще может существовать в динамической памяти Java, но указатель на него - из области локальной ссылки стека "родной" функции, которую может видеть сборщик мусора - уже утерян. Объект, который можно "собрать и выбросить в мусор", недоступен, по мнению сборщика мусора. Сборщик мусора не может отслеживать "родные" ссылки из "родных" стеков потока, которые могут существовать. Таким образом, использование "родных" ссылок, которые могут существовать в "родном" стеке, может привести к нежелательным последствиям, так как ничто не гарантирует существования объекта в динамической памяти Java в следующем периоде активности сборщика мусора.
Допустимое число локальных ссылок
Каждая локальная ссылка занимает некоторое количество ресурсов JVM. Хотя локальные ссылки после возвращения "родного" метода в Java автоматически освобождаются, излишнее выделение ресурсов локальным ссылкам может привести к тому, что память JVM закончится при выполнении "родного" метода. Функция EnsureLocalCapacity (JNIEnv *env, jint capacity) гарантирует возможность создания в текущем потоке хотя бы заданного числа локальных ссылок. При успешном выполнении возвращается 0, в противном случае возвращается отрицательное число и выбрасывается ошибка OutOfMemoryError. До начала исполнения "родного" метода JVM автоматически проверяет, что можно создать как минимум 16 локальных ссылок, согласно спецификации JNI. Для совместимости с предыдущими версиями JVM выделяет ресурсы для большего числа локальных ссылок, чем заявленное допустимое число. Поддерживая режим отладки, JVM может выдать предупреждение о том, что число созданных локальных ссылок слишком велико:
***ALERT: JNI local ref creation exceeded capacity

В Java 2 SDK для подключения этой возможности программисты могут добавить параметр командной строки verbose:jni. Спецификация JNI не прописывает жестко допустимое число локальных ссылок для JVM, равно как и не обязывает использовать это предупреждение. Сообщение может возникать или не возникать в разных ситуациях в JVM от различных вендоров.

Данные: скопированные и связанные
Конвертирование массивов из Java в "родной" код является непростой задачей, потому что в противоположность "родным" языкам элементы Java-массивов не всегда расположены в памяти рядом. Когда "родной" функции необходим доступ к созданному в Java массиву, JNI должен корректно упорядочить этот массив, а также гарантировать, что этот массив будет возвращен JVM после выполнения "родной" функции.

Между созданием массивов объектов и массивов примитивных типов в JNI есть существенное различие. Функция NewObjectArray() в качестве аргументов использует размер массива (jint), тип массива (jclass) и начальное значение для каждого элемента массива (обычно NULL). Подобным образом происходит создание массивов примитивных типов, хотя функция New<primitive_type>Array() использует лишь один аргумент - размер массива. Каждая из этих функций создает Java-массив. Однако элементы этого массива могут не находиться в памяти рядом. Для получения доступа к отдельному элементу массива примитивного типа необходим вызов функции Get<primitive_type>ArrayElements с массивом примитивного типа <primitive_type> и jboolean в качестве аргументов. Параметр jboolean передается по ссылке, и функцией устанавливается его значение JNI_TRUE или JNI_FALSE. Если массив непрерывно распределен в памяти, результат jboolean равен JNI_FALSE, а это значит, что "родной" код имеет указатель прямого доступа к массиву, и данные связаны. В противном случае JNI_TRUE означает, что была создана копия массива, которую можно поместить в "цикл уплотнения данных" сборщика мусора или собрать им же.

Какое бы значение (JNI_TRUE или JNI_FALSE) не приняло jboolean, спецификация JNI утверждает, что ссылка, возвращенная Get<primitive_type>ArrayElements, является действительной, пока не будет выполнен соответствующий метод Release<type>ArrayElements(). Спецификация JNI гласит, что "невозможно предсказать, будет ли данная JVM копировать или связывать данные при каком-либо вызове JNI". Если jboolean принимает значение JNI_FALSE, то массив будет связан, что не даст JVM собрать его как "мусор" или переместить во время "цикла уплотнения данных" сборщика мусора. Спецификация JNI утверждает совершенно четко, что по окончании использования ссылки на массив нужно вызвать функцию Release<primitive_type>ArrayElements(). Ссылка необязательно должна быть копией; не имеет значения, чему равна переменная jboolean - JNI_TRUE или JNI_FALSE, функция Release<primitive_type>ArrayElements() должна быть вызвана для того, чтобы избежать утечки памяти и обновить массив в JVM.

JVM от IBM обычно использует связывание. Распространенной ошибкой в программировании является освобождение только скопированных данных, так что "куча" постепенно становится все более и более дефрагментированной, заполненной фрагментами связанных данных пока, наконец, не произойдет сбой. Это еще один пример проблемы, которая может возникнуть, если написанное приложение основывается на конкретной реализации JVM, а не строго следует спецификации JNI. Вероятно, освобождение лишь скопированных данных будет корректно работать в JVM, предпочитающей метод копирования. Если приложение переносится в JVM, использующую связывание, или меняется реализация JVM, то не соответствующий спецификации код работать не будет.

Согласно общему правилу, Release<type> () нужно вызывать всегда после функции, использующей флаг, показывающий, являются ли возращенные данные копией или связанными.

Release<primitive_type>ArrayElements(), который нужно использовать, нужны следующие аргументы: указатель интерфейса JNIEnv, сам массив, указатель на массив и режим, в котором он должен быть очищен. Если данные были связаны, любые сделанные в них изменения копируются прямо в Java-"кучу", и параметр "режим" игнорируется. Если флаг jboolean описывает возвращаемые данные как копию, надо использовать флаг режима для изменений, проведенных с данными. В общем случае, для того чтобы обойти специфические черты реализаций JVM от разных производителей, самым эффективным способом будет поставить NULL(0) во флаге jboolean и всегда ставить нуль во флаге режима.

В отличие от массивов примитивных типов, объектные массивы в JVM нельзя перевести в формат, который может использовать C/C++. Конвертировать объект JVM в объект C++ нельзя; "родной" код должен иметь доступ к нему в памяти JVM посредством JNI. Не нужно реализовывать объектные массивы в JVM, так как они никогда не копируются из памяти JVM, и сборщик мусора сможет их обработать.


Особенности программирования JNI в AIX

Этот раздел охватывает примеры JNI в AIX, версии API, совместно используемые библиотеки, многопоточность и компиляторы AIX.

Примеры JNI в AIX

Программистам, впервые пробующим свои силы в работе с JNI API на примере AIX JDK, рекомендуется ознакомиться с кодом и функциями JNI. В AIX JDK 1.4.1 файлы Java14.samples - примеры для 32-битовой версии, а Java14_64.samples - для 64-битовой версии. Также рекомендуется прочесть руководство пользователя JDK, в особенности главу, описывающую совместимость с JNI.

Версии JNI API

В JDK 1.4.1 больше невозможно использовать интерфейс JNI 1.1. Вызвать JNI_CreateJavaVM() и получить доступ к версии JNI_VERSION_1_1(0x00010001) нельзя. Доступные версии - JNI_VERSION_1_2(0x00010002) и JNI_VERSION_1_4(0x00010004).

Совместно используемые библиотеки JNI

Самые распространенные ошибки в программировании при создании и эксплуатации совместно используемых библиотек JNI:

  • в LIBPATH не включены libjava.a (../jre/bin) и libjvm.a (../jre/bin/classic) для программ, внедряющих и запускающих JVM с помощью JNI API;
  • установка бита SETUID на исполняемую программу, которая запускает JVM посредством API, вызывающего JNI;
  • не включены каталоги, содержащие совместно используемые объекты JNI, или права на них отсутствуют;
  • попытка загрузить 32-битовые совместно используемые объекты в 64-битовые процессы (или наоборот);
  • для многопоточных приложений не создан совместно используемый объект, подготовленный для использования в многопоточном окружении.

Переменная среда LIBPATH сообщает приложениям AIX, таким как JVM, местонахождение общих библиотек. LIBPATH используется аналогично LD_LIBRARY_PATH в других основанных на UNIX системах. Совместно используемые библиотеки JVM находятся в подкаталогах ./jre/bin и ./jre/bin/classic каталога, где установлена Java. Для совместимости со сценариями, написанными для иных UNIX-систем, программы запуска Java добавляют LD_LIBRARY_PATH при условии, если он установлен, к началу LIBPATH. Однако, если приложению нужно искать совместно используемые библиотеки в определенных каталогах, лучше определить переменную LIBPATH. Если приложение запускает из своего кода JVM с помощью Invocation API, то необходимо включить оба вышеупомянутых каталога для того, чтобы загрузить и libjava.a, и libjvm.a.

Если пользователь имеет доступ к запуску выполняемой программы (включен флаг SETUID), то значение переменной LIBPATH при старте программы из соображений безопасности автоматически сбрасывается. Для приложения, использующего JNI Invocation API, которое динамически загружает библиотеки (с помощью dlopen()), это может помешать вызовам методов, которым необходим LIBPATH для поиска совместно используемых объектов (например, метод JNI_CreateJVM()), поскольку LIBPATH не задан корректно. Необходимо либо отключить флаг SETUID, или запускать приложение от имени пользователя с правами администратора. Если флаг доступа SETUID надо сохранить, можно применить другой способ (в зависимости от внешней среды) - определить значение LIBPATH в явной форме в коде с помощью выражения setenv(). Например,

setenv("LIBPATH","/usr/java14/jre/bin:/usr/java14/jre/bin/classic", 1);

Обычно, когда приложение JNI выдает ошибку java.lang.UnsatifiedLinkError, это указывает на ошибки при написании, компиляции или конфигурации приложения. Наиболее распространенными ошибками программистов являются отсутствие корректного пути поиска библиотеки для System.loadLibrary(), прав на каталоги на этом пути, вызов "родных" методов до загрузки библиотеки. В общем случае библиотеку следует загрузить в блоке static, чтобы гарантировать, что она будет загружена до вызова какого-либо "родного" метода.

Попытка загрузить 64-битовую совместно используемую библиотеку в адресное пространство 32-битовой JVM (или наоборот), также вполне вероятно приведет к возникновению java.lang.UnsatisfiedLinkError. Не существует метода загрузки 64-битового объекта в 32-битовое адресное пространство (и наоборот). Редактор связей соответствующим образом выбирает объекты из библиотеки, основываясь на запрашиваемом типе связи (32 или 64 бит), и создает объект или приложение такого типа.

Совместно используемые библиотеки JNI, которые нужно загрузить в JVM, должны быть созданы в виде поточно-ориентированных объектов с повторным вхождением с соответствующими параметрами компиляторов. Это подробно описано в разделе Компиляторы AIX.

JNI и многопоточность (параллельные процессы) в AIX

JVM IBM для AIX использует для работы с потоками пакет AIX POSIX pthreads. Java-потоки, созданные JVM, используют модель POSIX pthreads (pthreads), которую поддерживает AIX. В настоящее время она обеспечивает взаимно-однозначное соответствие между Java-потоками и потоками ядра. Если создаются потоки pthreads, то при разработке JNI-программы нужно запускать ее вместе с моделью взаимно-однозначного соответствия потоков (1 к 1) и системной областью для потоков:

export AIXTHREAD_SCOPE=S

Другой возможностью является предварительное задание в коде значения атрибута области потока как PTHREAD_SCOPE_SYSTEM с помощью функции AIX pthread_attr_setscope() при создании потока.

По умолчанию переменные окружения AIXTHREAD_MUTEX_DEBUG, AIXTHREAD_RWLOCK_DEBUG и AIXTHREAD_COND_DEBUG отключены, т.е. имеют значения OFF. Эти три переменные отключают списки взаимных исключений, запретов на чтение и запись, условных переменных, которые используются отладчиком, уменьшая таким образом ненужные издержки при поддержке списков. В программах с использованием JNI Invocation API для них необходимо задавать значения OFF.

Если JNI-приложение является многопоточным, то особое внимание следует обратить на спецификации POSIX, которые используются в AIX. Некоторые спецификации POSIX неопределенны и оттого являются платформенно-зависимыми. Возможно, придется изменить "родной" код, использующий многопоточность, при переносе кода JNI с одной операционной системы (ОС) на другую. Опытные программисты могут воспользоваться рядом новых функций, которые содержит потоковая библиотека. Эти новые функции являются опциональными, и их реализация зависит от возможностей POSIX. Возможно, особое внимание следует уделить переносу приложения с не-AIX-платформы на AIX, или с одной версии AIX на другую.

Например, необходимо задать размер стека потоков pthread. В AIX этот атрибут определен. Он зависит от соответствующей настройки "stacksize" в POSIX, но это может быть не определено в других системах. Величина размера стека определяет минимальный размер стека, который отводится для потока. Метод pthread_attr_getstacksize() возвращает значение этого атрибута, а pthread_attr_setstacksize() устанавливает его равным какой-либо величине. Если размер стека меньше 96 Кбит, то по умолчанию в данной реализации AIX для библиотеки потоков должен быть выделен стек размером 96 Кбит. Размер выделенного стека всегда кратен 4 Кбит.

Размер стека основного потока контролируется с помощью параметра ulimit, зависящего от пользователя. Размером стека потоков, созданных JVM, таких как потоки обработки сигналов, окончания работы и т.д., управляет "родной" параметр Xss, по умолчанию имеющий значения:

  • 256 Кбит для JDK 122 и 130;
  • 512 Кбит для JDK 131 и JDK 1.4.1 32-битовых;
  • 1 Мбит для JDK 1.4.1 64-битовых.

Значения могут изменяться. Если поток создан, а размер стека не определен с помощью метода pthread_attr_setstacksize(), то размер стека для потока равен значению, принятому в AIX по умолчанию. Соединение с потоком, при создании которого размер стека не был определен (созданного посредством JNI_CreateJavaVM() с помощью AttachCurrentThread()), может привести к переполнению стека и сигналу о сбое в памяти (SIGSEGV). Стек небольшого размера, который принят по умолчанию в AIX, может не соответствовать требованиям сложных вычислений, связанным с потоками JVM. См. следующий фрагмент кода, который подытоживает этот раздел:

pthread_attr_t attr; pthread_t tid;
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM); 
 /* установка системной области для потоков */
pthread_attr_setstacksize(&attr, 512*1024); 
 /* установка размера стека в 512KБ по умолчанию */
    для 32-битовых потоков в JVM 1.4.1 и 1.3.1. */
pthread_create(&tid, &attr, launchThread, NULL);
pthread_join(tid,&status);

Компиляторы AIX

Очень важно иметь самую последнюю версию используемого компилятора IBM C/C++, а также установленной рабочей среды (RTE) C++ (файлов xlC.*). Базовые пакеты и обновления для среды xlC на момент написания статьи доступны по FTP: ftp://aix.software.ibm.com/aix/products/ccpp/.

Необходимо использовать поточно-ориентированные повторно используемые варианты компилятора VAC и VACPP, такие как xlC_r или xlc_r/cc_r, для компиляции кода на C++ или C соответственно в поточно-ориентированные совместно и повторно используемые объекты, которые должны быть отнесены к совместно используемой библиотеке. Для кода на С используется ld, а для кода на C++ makeC++SharedLib_r для последующей загрузки в пространство процессов JVM с помощью JNI. В противном случае придется вручную передать макросы в вариант компилятора, не поддерживающий повторное использование, и добавить библиотеки, используемые по умолчанию этим поточно-ориентированным компилятором.


Глоссарий

AIX - Advanced Interactive Executive Operating System

API - Application Programming Interface (интерфейс программирования приложений)

GC - Garbage Collector (сборщик мусора)

JCK - Sun Java Compatibility Kit (набор для совместимости c Java от Sun)

JDK - Java Development Kit (набор разработчика Java)

JNI - Java Native Interface (платформенно-зависимый интерфейс Java)

JRE - Java Runtime Environment (среда исполнения Java)

JVM - Java Virtual Machine (виртуальная Java-машина)

OS - Operating System (операционная система)

RTE - Run Time Environment (рабочая среда)

VAC - IBM Visual Age C Compiler (компилятор IBM Visual Age C)

VACPP - IBM Visual Age C++ Compiler (компилятор IBM Visual Age C++)

WORA - Write Once Run Anywhere (написать один раз и использовать везде)

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=AIX и UNIX
ArticleID=433672
ArticleTitle=Программирование JNI в AIX
publish-date=10082009