JNI(Java Native Interface)란? (feat. 자바 스레드 생성)
JNI란?
Java Native Interface의 약자로, 자바 코드에서 네이티브 코드(하드웨어와 운영체제가 직접 실행할 수 있는 기계어 또는 바이너리 코드를 의미)를 호출하거나, 반대로 네이티브 코드에서 Java 코드를 호출할 수 있게 해주는 프레임워크를 말합니다. C언어 또는 C++언어로 작성된 코드는 컴파일 시 해당 하드웨어에서 직접 구동될 수 있는 기계어로 컴파일되기 때문에 네이티브 코드에 해당되는데요(자바는 플랫폼에 독립적인 바이트코드로 컴파일된다는 점을 참고하면 좋습니다). 따라서 자바 코드에서 JNI를 통해 C언어 또는 C++언어로 작성된 코드를 실행할 수 있게 됩니다.
주로 다음과 같은 상황들에 JNI를 활용할 수 있습니다.
- 성능 최적화 : 성능이 중요한 부분을 C/C++로 구현하고, 이를 자바 코드에서 호출할 때 사용합니다.
- 기존 라이브러리 사용 : 이미 C/C++로 작성된 기존 라이브러리나 API를 자바 애플리케이션에서 활용하고자 할 때 사용
- 플랫폼 종속 기능 : 플랫폼에 특화된 기능(시스템콜 호출, 하드웨어 제어 등)이 필요한 경우 사용
사용 방법
메서드 앞에 native를 붙임으로써 네이티브 메서드임을 명시할 수 있습니다.
public class Jofe {
public native void nativeMethod();
static {
System.loadLibrary("jofe");
}
public static void main(String[] args) {
new Jofe().nativeMethod();
}
}
native로 선언된 nativeMethod는 해당 메서드가 네이티브 라이브러리에서 구현됨을 의미합니다. Jofe클래스가 처음 로드될 때 네이티브 라이브러리인 jofe를 로드하는 것도 확인 가능합니다. main 메서드에서 Jofe 객체를 생성하고 nativeMethod를 호출하면, JVM은 네이티브 코드로 연결되어 해당 네이티브 메서드가 실행되게 됩니다.
다음 명령어의 실행을 통해 JNI를 사용하기 위한 헤더 파일을 만들 수 있습니다.
javac -h {헤더파일을 둘 위치} {Jofe.java의 경로}
# ex : javac -h . Jofe.java
그러면 다음과 같이 헤더파일이 만들어집니다.
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Jofe */
#ifndef _Included_Jofe
#define _Included_Jofe
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: Jofe
* Method: nativeMethod
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_Jofe_nativeMethod
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
이 헤더파일을 기반으로, c언어로 해당 네이티브 메서드를 다음과 같이 구현할 수 있습니다.
#include <jni.h>
#include "Jofe.h"
JNIEXPORT void JNICALL Java_Jofe_nativeMethod(JNIEnv *env, jobject obj) {
printf("이거 하나 출력할라고 개고생중입니다");
}
이 c파일을 컴파일하여 다음과 같이 라이브러리를 만들 수 있습니다. 저는 macos여서 다음 커맨드를 사용했습니다.
gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/darwin -shared -m64 -o {생성할 라이브러리 이름} {c파일 경로}
# ex : gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/darwin -shared -m64 -o libjofe.dylib Jofe.c
그리고 컴파일된 Jofe.class를 다음과 같이 네이티브 라이브러리의 경로를 지정해서 실행해줍니다.
java -Djava.library.path={생성한 라이브러리 위치} -classpath {컴파일된 클래스파일위치} {실행할 클래스명}
# java -Djava.library.path=/Users/jofejofe/Development/JNI/src/main/java/org/example -classpath . Jofe
그러면 c파일에서 구현된 메서드가 실행되는 걸 확인할 수 있습니다.
실제로 자바에선 JNI가 어떻게 활용되고 있을까?
대표적으로 스레드의 생성 등에 활용하고 있습니다. 다음과 같이 Thread의 start()메서드를 까보면, 내부적으로 start0()이란 네이티브 메서드를 호출하고 있음을 볼 수 있습니다.
// Thread.java
public void start() {
synchronized (this) {
// zero status corresponds to state "NEW".
if (holder.threadStatus != 0)
throw new IllegalThreadStateException();
start0();
}
}
private native void start0();
해당 메서드의 구현체는 다음과 같습니다.
#include "jni.h"
#include "jvm.h"
#include "java_lang_Thread.h"
#define THD "Ljava/lang/Thread;"
#define OBJ "Ljava/lang/Object;"
#define STE "Ljava/lang/StackTraceElement;"
#define STR "Ljava/lang/String;"
#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))
static JNINativeMethod methods[] = {
{"start0", "()V", (void *)&JVM_StartThread},
{"isAlive0", "()Z", (void *)&JVM_IsThreadAlive},
{"setPriority0", "(I)V", (void *)&JVM_SetThreadPriority},
{"yield0", "()V", (void *)&JVM_Yield},
{"sleep0", "(J)V", (void *)&JVM_Sleep},
{"currentCarrierThread", "()" THD, (void *)&JVM_CurrentCarrierThread},
{"currentThread", "()" THD, (void *)&JVM_CurrentThread},
// ...생략
네이티브 메서드의 이름과 c로 작성된 함수의 포인터를 매핑하고 있는 걸 볼 수 있습니다.
JVM_StartThread는 다음과 같이 구현되어 있습니다.
// jobject : 여기선 Java 스레드 객체(java.lang.Thread)를 나타냄. 이를 통해 스레드를 시작
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
#if INCLUDE_CDS
// .. 생략
#endif
// JVM 내에서 자바스레드(유저스레드)를 표현하고 관리하는 핵심 클래스
// 자바스레드(유저스레드)와 커널 스레드를 연결하는 역할
JavaThread *native_thread = NULL;
// 이미 시작된 스레드를 다시 시작하려고 하는 경우 예외를 던지는지 여부를 결정하는 플래그
bool throw_illegal_thread_state = false;
{
// 뮤텍스 잠그고 시작
MutexLocker mu(Threads_lock);
// 해당 Java 스레드 객체가 이미 시작된 스레드라면 => 플래그를 설정
if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
throw_illegal_thread_state = true;
} else {
// 스레드가 이미 시작되지 않은 경우, 스택 크기를 가져와서 새로운 JavaThread 객체 생성
jlong size =
java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
//
NOT_LP64(if (size > SIZE_MAX) size = SIZE_MAX;)
size_t sz = size > 0 ? (size_t) size : 0;
// 여기서 커널스레드가 새로 만들어짐
native_thread = new JavaThread(&thread_entry, sz);
// 커널스레드가 만들어졌다면, 자바스레드(유저스레드)를 매핑
if (native_thread->osthread() != NULL) {
native_thread->prepare(jthread);
}
}
}
// 플래그 설정에 따른 예외 던지기
if (throw_illegal_thread_state) {
THROW(vmSymbols::java_lang_IllegalThreadStateException());
}
assert(native_thread != NULL, "Starting null thread?");
// 일종의 예외 처리
if (native_thread->osthread() == NULL) {
ResourceMark rm(thread);
log_warning(os, thread)("Failed to start the native thread for java.lang.Thread \"%s\"",
JavaThread::name_for(JNIHandles::resolve_non_null(jthread)));
native_thread->smr_delete();
if (JvmtiExport::should_post_resource_exhausted()) {
JvmtiExport::post_resource_exhausted(
JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS,
os::native_thread_creation_failed_msg());
}
THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
os::native_thread_creation_failed_msg());
}
JFR_ONLY(Jfr::on_java_thread_start(thread, native_thread);)
// 스레드 시작
Thread::start(native_thread);
JVM_END
주석에도 명시했지만, new JavaThread()에서 다음과 같이 커널스레드가 생성되는 과정도 볼 수 있습니다.
JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) : JavaThread() {
_jni_attach_state = _not_attaching_via_jni;
set_entry_point(entry_point);
os::ThreadType thr_type = os::java_thread;
thr_type = entry_point == &CompilerThread::thread_entry ? os::compiler_thread :
os::java_thread;
os::create_thread(this, thr_type, stack_sz);
}
이와 같이.. 자바 코드에서 시스템콜이 필요해지는 경우, 개발자가 직접 사용하든 안 하든 JNI를 활용해 네이티브 코드를 호출하여 시스템콜을 수행하게 됩니다. System.out.println도 다음과 같이 JNI를 사용합니다.
private native void write(int b, boolean append) throws IOException;