기존에 C언어 기반으로 개발된 프로그램에서 NoSQL, 메시지 서버, 분산 저장소 등에 데이터를 저장해야 할 일이 생겼다. 이들 저장소는 C언어 기반의 인터페이스(라이브러리)를 제공하긴 하지만, 대부분이 인터프리터 기반 언어의 API를 기본으로 제공하기 때문에 기능 확장과 안정성 측면에서 자바 기반의 인터페이스를 사용하고자 한다. 대부분의 기능(데이터 추출 및 가공)이 C 기반의 애플리케이션(프로세스)에서 수행되고 그 결과를 자바 자바 기반의 프로그램(프로세스)을 사용하여 목적(Target) 저장소에 데이터를 저장하고자 한다. 이 두 프로세스 간의 통신을 위해서 파일, 소켓, 표준입출력(Standard Input/Output) 채널, 그 밖의 운영체제에서 제공하는 IPC 기능을 사용할 수 도 있다. 하지만, 아래와 같은 이유로 C 기반의 프로그램에서 JVM을 내장화(Embedded)하여 자바 프로그램을 구동하고자 한다.
- 관리대상 프로세스의 최소화 및 리소스 사용량 감소
- 프로세스 2개를 관리하는 것 보다는 1개를 사용하는 것이 관리 및 최적화, 리소스 사용량 감소에 유리
- 프로세스 간 통신 채널의 성능
- 메모리 공유를 통해서 C와 Java 스레드 간의 통신 성능 향상
JAVA Invocation API
JNI(Java Native Interface)는 보통 Java에서 Native(C/C++) Code를 호출하기 위해서 사용한다고 알고 있다. 하지만, JNI의 Java Invocation API를 사용하여 그 반대의 경우도 가능하다. 즉, C/C++ 프로그램에서 JVM을 내부에 생성하고 자바 코드를 호출할 수 있는 것이다. Java Invocation API는 아래와 같은 경우에 활용할 수 있을 것이다.
- 이미 Java로 개발된 애플리케이션이 있고, 새롭게 C/C++로 개발하는 어플리케이션에서 해당 기능을 재개발하지 않고 이용하고 싶을 경우
- 이미 Native 코드(C/C++)로 개발된 어플리케이션에서 특수 목적(본인은 외부 스토리지 연동과 풍부한 기능 셋 사용)을 위해 일부 코드를 자바로 개발할 필요가 있을 경우
JVM 인스턴스 생성과 소멸
Native(C/C++) 어플리케이션에서 Java Invocation API를 사용하여 Java 코드를 호출하는 것은, JVM 인스턴스를 Native 어플리케이션 내부에 생성한다는 것을 의미한다. 기존의 자바 애플리케이션을 java 명령어를 사용하여 실행하게 될 경우 JVM이 단독 프로세스로 실행되지만, Java Invocation API를 사용할 경우에 JVM 프로세스가 별도로 생성되는 것이 아니라 Native 어플리케이션 프로세스가 JVM 인스턴스를 내부에 품고 있는 것이다.
JVM 인스턴스를 생성하고 초기화하기 위해서 호출하는 Java Invocation API는 JNI_CreateJavaVM이다.
jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args); |
생성된 JVM에 대한 객체 반환(p_vm)과 Java Native Interface 포인터 반환(p_env), VM 설정과 관련된 vm_args 매개변수를 가진다. JNI_CreateJavaVM API를 사용하여 JVM 인스턴스를 생성할 경우, 해당 API를 호출한 Native 스레드는 자바의 main 스레드가 된다. 또한 해당 API를 호출하고 반환된 JNIEnv 객체는 이를 호출한 Native 스레드에서만 사용 가능하다.
매개변수 중에서 vm_args는 JavaVMInitArgs 구조체인데 아래와 같은 필드로 구성된다.
typedef struct JavaVMInitArgs { jint version; jint nOptions; JavaVMOption *options; jboolean ignoreUnrecognized; } JavaVMInitArgs; |
version은 어떠한 버전의 Java Native Interface를 사용할지 정의한다. 각 JNI의 버전 별 상세 스펙은 검색을 통해 확인 가능하다(JNI_VERSION_9 참고). JNI의 버전은 Java 버전과 유사하지만, 아래의 표를 참고하여 Java 버전과 JNI 버전을 매핑 해보길 바란다.
Java SE Platform |
JNI Version |
1.1 |
JNI_VERSION_1_1 |
1.2 |
JNI_VERSION_1_2 |
1.3 |
JNI_VERSION_1_2 |
1.4 |
JNI_VERSION_1_4 |
5.0 |
JNI_VERSION_1_4 |
6 |
JNI_VERSION_1_6 |
7 |
JNI_VERSION_1_7 |
8 |
JNI_VERSION_1_8 |
9 |
JNI_VERSION_9 |
nOptions는 options를 통해 제공되는 옵션의 개수를 의미한다. options는 JavaVMOption 구조체의 배일이다. JavaVMOption 구조체는 아래와 같이 구성된다.
typedef struct JavaVMOption { char *optionString; void *extraInfo; } JavaVMOption; |
optionString은 VM 옵션 문자열을 의미하고 extraInfo는 추가적인 정보인데 옵션에 따라서 사용 여부가 결정된다. optionString은 아래의 표를 통해 정리한다.
optionString |
의미 |
-D<name>=<value> |
시스템 속성 정의 |
-verbose[:class|gc|jni] |
VM의 Verbose output의 활성화. Class = 클래스 로딩 관련 메시지, gc = Garbage collection 관련 메시지, jni = JNI 관련 메시지. |
vfprintf |
extracInfo에 vfprintf hook 등록 |
exit |
extracInfo에 exit hook 등록 |
abort |
extraInfo에 abort hook 등록 |
ignoreUnrecognized가 FALSE일 경우 위의 optionString에 인식(지정)되지 않은 옵션(예를 들어 -X로 시작하는 옵션)을 사용할 경우 JNI_ERR가 반환된다. TRUE일 경우에는 인식되지 않는 옵션을 사용할 경우 아무런 에러를 반환하지 않는다.
JVM 인스턴스를 소멸하고 자원을 회수하기 위해서는 JavaVM의 DestroyJavaVM 함수를 사용한다. 해당 함수를 호출할 경우, 함수를 호출한 스레드(Attached native thread이며 JVM 관점에서는 자바 스레드)를 제외한 비 데몬(non-daemon)스레드가 모두 종료되기를 기다린 이후에 JVM 인스턴스를 소멸한다. JVM은 다른 사용자 스레드들이 사용 중인 소켓, 파일, Window와 같은 시스템 자원을 회수할 수 없으므로 이러한 일은 프로그래머의 책임이며, DestroyJavaVM은 단지 모든 사용자 스레드(즉, non-daemon 스레드)가 종료되기를 기다린 이후에 JVM을 회수하는 것이다.
예제 코드 및 컴파일
C++ 소스코드 (test.cpp)
#include <jni.h>
#include <stdio.h>
int main() {
JavaVM *vm;
JNIEnv *env;
JavaVMInitArgs vm_args;
JavaVMOption options[1];
options[0].optionString = "-Djava.class.path=/home/test/jvm";
vm_args.version = JNI_VERSION_1_8;
vm_args.options = options;
vm_args.nOptions = 1;
vm_args.ignoreUnrecognized = 1;
jstring jstr;
jobjectArray args;
jint res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args);
if (res < 0) {
printf("Can't create Java VM\n");
return -1;
}
jclass cls = env->FindClass("HelloWorld");
if (cls == 0) {
printf("HelloWorld class not found\n");
return -1;
}
jmethodID mid = env->GetStaticMethodID(cls, "main", "([Ljava/lang/String;)V");
if (mid == 0) {
printf("main() method not found\n");
return -1;
}
jstring argString = env->NewStringUTF("FOR TEST");
args = env->NewObjectArray(1, env->FindClass("java/lang/String"), argString);
if (args == 0) {
printf("Out of memory\n");
return -1;
}
env->CallStaticVoidMethod(cls, mid, args);
vm->DestroyJavaVM();
return 0;
}
Java 소스코드 (HelloWorld.java)
public class HelloWorld {
public static void main(String[] args) throws Exception{
int i = 0;
while (i++ < 100) {
System.out.println("Loop : " + i);
}
}
}
Java 코드 컴파일
javac HelloWorld.java
C 코드 컴파일
g++ -I/usr/local/java/jdk1.8.0_101/include -I/usr/local/java/jdk1.8.0_101/include/linux -L/usr/local/java/jdk1.8.0_101/jre/lib/amd64/server –o test1 test.c -ljvm
실행
컴파일된 실행하면 HelloWorld.class의 main을 호출하여 표준 출력으로 메시지가 출력되기 시작한다. 이때 리눅스의 ps 명령어와 jps 명령어를 사용하여 프로세스를 확인해 보도록 하자.
해당 프로세스(프로세스 아이디 : 6693)가 리눅스 명령어인 ps와 자바 명령어인 jps 모두에서 조회되는 것을 알 수 있다.
References)
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html
https://www.developer.com/java/data/how-to-create-a-jvm-instance-in-jni.html
댓글