Java JVM、JNI、Native Function Interface、Create New Process Native Function API Analysis
目录
1. JAVA JVM
2. Java JNI: Java Native Interface
3. Java Create New Process Native Function API Analysis In Linux
4. Java Create New Process Native Function API Analysis In Windows
1. JAVA JVM
0x1: JVM架构简介
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够"一次编译,到处运行"的原因
JVM的设计目标是提供一个基于抽象规格描述的计算机模型,为解释程序开发人员提很好的灵活性,同时也确保Java代码可在符合该规范的任何系统上运行。JVM对其实现的某些方面给出了具体的定义
1. Java可执行代码,即字节码(Bytecode)格式,包括
1) 操作码的语法和数值
2) 操作数的语法和数值
3) 标识符的数值表示方式
4) Java类文件中的Java对象、常量缓冲池在JVM的存储映象
//这些定义为JVM解释器开发人员提供了所需的信息和开发环境
2. JVM指令系统
3. JVM寄存器
4. JVM栈结构
5. JVM碎片回收堆
6. JVM存储区
JVM是JAVA的核心和基础,在JAVA编译器和OS平台之间的虚拟处理器。它是一种基于下层的操作系统和硬件平台并利用软件方法来实现的抽象的计算机,可以在上面执行JAVA的字节码程序
JAVA编译器只需面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译器,编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行
//JVM执行程序的过程
1. 加载.class文件
2. 管理并分配内存
3. 执行垃圾收集
JRE(Java Runtime Environment)包含JVM的java程序的运行环境,即JVM是整个JRE的一部分
JVM是Java程序运行的容器,但是他同时也是操作系统的一个进程,因此他也有他自己的运行的生命周期,也有自己的代码和数据空间
JVM在整个JDK中处于最底层,负责与操作系统的交互,用来屏蔽操作系统环境,提供一个完整的Java运行环境,因此也就虚拟计算机。操作系统装入JVM是通过JDK中Java.exe来完成,通过下面4步来完成JVM环境(windows)
1. 创建JVM装载环境和配置
2. 装载JVM.dll
3. 初始化JVM.dll,并挂接到JNIENV(JNI调用接口)实例
4. 调用JNIEnv实例装载并处理class类
Relevant Link:
http://baike.baidu.com/view/160708.htm
http://zh.wikipedia.org/wiki/Java%E8%99%9A%E6%8B%9F%E6%9C%BA
http://en.wikipedia.org/wiki/Java_virtual_machine
2. Java JNI: Java Native Interface
0x1: 本地方法栈(Native Method Stacks)
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用很类似,区别是
1. 虚拟机栈为虚拟机执行Java方法(字节码)服务
2. 本地方法栈为虚拟机使用到的Native方法服务
虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(例如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一,与虚拟机一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常
0x2: Java本地接口
Java本地接口(JNI)是一个"编程框架"使得运行在Java虚拟机上的Java程序调用或者被调用特定于本机硬件与操作系统的用其它语言(C、C++或汇编语言等)编写的程序
1. JNI允许用本地代码(和具体操作系统平台相关的代码,例如用glibc编译、和用CRT编译的平台相关代码)来解决纯粹用Java编程不能解决的平台相关的特性。也用于改造已存在的其它语言写的应用程序,供Java程序访问。许多使用了JNI的标准库提供了文件I/O与其它功能。标准库中性能敏感或平台敏感的API实现允许所有Java应用程序安全且平台独立地访问这些功能
2. JNI框架使得本地方法可以访问Java对象,就如同Java程序访问这些本地对象。本地方法可以创建Java对象,然后检查、使用这些对象执行任务。本地方法也可以检查并使用由Java程序创建的对象
3. Java开发人员称JNI为逃生门("escape hatch"),因为JINI允许增加标准Java API不能提供的功能。也可以用于时间关键的计算或者如解复杂数学方程,因为本地方法的运算比JVM更快。也可以在安卓上重用已存在的C/C++编写的库
0x3: 使用JNI存在的问题
1. 使用JNI的精细的错误会使JVM不稳定,并且难以再现与调试
2. 仅有应用程序与signed applet可以调用JNI
3. 依赖于JNI的应用程序失去了Java的平台移植性(部分解决之道是对每一平台写专门的JNI代码,然后Java检测操作系统并载入当前的JNI代码)
4. JNI框架对本地端执行代码分配的非JVM资源,不提供任何自动的垃圾收集(例如C/C++代码)。因此,本地端代码负责释放任何在本地获取的资源
5. Linux与Solaris平台,如果本地代码登记了自身作为信号处理器,会拦截原本发给JVM的信号。解决方法是应该使用信号链式的本地代码更好地与JVM合作
6. Windows平台上,结构化异常处理(SEH)可用于把本地代码包裹在SEH try/catch块中捕捉机器(CPU/FPU)生成的软中断(如: 空指针访问冲突、除0操作等),在这些中断传播到JVM之前就得到处理,使JVM无法正常捕获到这些错误异常
0x4: JNI Code Example
在JNI框架柱,本地函数一般在单独的.c或.cpp文件中实现。当JVM调用这些函数,就传递
1. JNIEnv指针
2. jobject的指针
env指向一个结构包含了到JVM的界面,包含了所有必须的函数与JVM交互、访问Java对象。基本上,Java程序可以做的任何事情都可以用JNIEnv做。下面代码把Java字符串转化为本地字符串
//C++ code
extern "C"
JNIEXPORT void JNICALL Java_ClassName_MethodName (JNIEnv *env, jobject obj, jstring javaString)
{
//Get the native string from javaString
const char *nativeString = env->GetStringUTFChars(javaString, 0);
//Do something with the nativeString
//DON'T FORGET THIS LINE!!!
env->ReleaseStringUTFChars(javaString, nativeString);
}
/*C code*/
JNIEXPORT void JNICALL Java_ClassName_MethodName (JNIEnv *env, jobject obj, jstring javaString)
{
/*Get the native string from javaString*/
const char *nativeString = (*env)->GetStringUTFChars(env, javaString, 0);
/*Do something with the nativeString*/
/*DON'T FORGET THIS LINE!!!*/
(*env)->ReleaseStringUTFChars(env, javaString, nativeString);
}
/*Objective-C code*/
JNIEXPORT void JNICALL Java_ClassName_MethodName (JNIEnv *env, jobject obj, jstring javaString)
{
/*DON'T FORGET THIS LINE!!!*/
JNF_COCOA_ENTER(env);
/*Get the native string from javaString*/
NSString* nativeString = JNFJavaToNSString(env, javaString);
/*Do something with the nativeString*/
/*DON'T FORGET THIS LINE!!!*/
JNF_COCOA_EXIT(env);
}
本地数据类型与Java数据类型可以互相映射。对于复合数据类型,如对象,数组,字符串,就必须用JNIEnv中的方法来显示地转换
Relevant Link:
http://zh.wikipedia.org/wiki/Java%E6%9C%AC%E5%9C%B0%E6%8E%A5%E5%8F%A3
http://www.cnblogs.com/mandroid/archive/2011/06/15/2081093.html
http://www.cnblogs.com/icejoywoo/archive/2012/02/22/2363709.html
http://www.ibm.com/developerworks/cn/java/j-jni/
3. Java Create New Process Native Function API Analysis In Linux
0x1: Java创建新进程涉及到的类、方法
1. java.lang.Runtime.exec(String[] cmdarray, String[] envp);
package com.tutorialspoint;
public class RuntimeDemo {
public static void main(String[] args) {
try {
// create a new array of 2 strings
String[] cmdArray = new String[2];
// first argument is the program we want to open
cmdArray[0] = "ls -l";
// second argument is a parameter
cmdArray[1] = ".";
// print a message
System.out.println("listing the current directory");
Process process = Runtime.getRuntime().exec(cmdArray, null);
// print another message
System.out.println("list directory is ok");
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
Relevant Link:
http://docs.oracle.com/javase/7/docs/api/java/lang/Runtime.html#exec(java.lang.String[],%20java.lang.String[],%20java.io.File)
http://www.tutorialspoint.com/java/lang/runtime_exec_envp.htm
2. Process p = new ProcessBuilder("myCommand", "myArg").start();
package com.javacodegeeks.process;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class ProcessBuilderExample
{
public static void main(String[] args) throws InterruptedException, IOException
{
ProcessBuilder pb = new ProcessBuilder("echo", "This is ProcessBuilder Example from JCG");
System.out.println("Run echo command");
Process process = pb.start();
int errCode = process.waitFor();
System.out.println("Echo command executed, any errors? " + (errCode == 0 ? "No" : "Yes"));
System.out.println("Echo Output:\n" + output(process.getInputStream()));
}
private static String output(InputStream inputStream) throws IOException
{
StringBuilder sb = new StringBuilder();
BufferedReader br = null;
try
{
br = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
while ((line = br.readLine()) != null)
{
sb.append(line + System.getProperty("line.separator"));
}
}
finally
{
br.close();
}
return sb.toString();
}
}
Relevant Link:
http://examples.javacodegeeks.com/core-java/lang/processbuilder/java-lang-processbuilder-example/
http://m.alvinalexander.com/java/java-exec-processbuilder-process-2
http://stackoverflow.com/questions/3468987/executing-another-application-from-java
0x2: difference between ProcessBuilder and Runtime.exec()
The various overloads of Runtime.getRuntime().exec(...) take either an array of strings or a single string. The single-string overloads of exec() will tokenise the string into an array of arguments, before passing the string array onto one of the exec() overloads that takes a string array. The ProcessBuilder constructors, on the other hand, only take a varargs array of strings or a List of strings, where each string in the array or list is assumed to be an individual argument. Either way, the arguments obtained are then joined up into a string that is passed to the OS to execute.
java下创建新进程的2种不同的方法的区别简单来说是这样的
1. exec
Runtime.getRuntime().exec("C:\DoStuff.exe -arg1 -arg2");
2. ProcessBuilder
rocessBuilder b = new ProcessBuilder("C:\DoStuff.exe", "-arg1", "-arg2");
List<String> params = java.util.Arrays.asList("C:\DoStuff.exe", "-arg1", "-arg2");
ProcessBuilder b = new ProcessBuilder(params);
/*
1. ProcessBuilder.start() 和 Runtime.exec()传递的参数有所不同
1) Runtime.exec()可接受一个单独的字符串,这个字符串是通过空格来分隔可执行命令程序和参数的,也可以接受字符串数组参数
2) ProcessBuilder的构造函数是一个字符串列表或者数组。列表中第一个参数是可执行命令程序,其他的是命令行执行是需要的参数
2. 每个ProcessBuilder实例管理一个进程属性集。ProcessBuilder的start()方法利用这些属性创建一个新的Process实例。start()方法可以从同一实例重复调用,以利用相同或者相关的属性创建新的子进程
3. ProcessBuilder还提供了一些额外的功能,例如
1) 重复执行
2) 设置环境变量
*/
Relevant Link:
http://www.java-tips.org/java-se-tips/java.util/from-runtime.exec-to-processbuilder.html
http://www.scala-lang.org/old/node/7146
http://stackoverflow.com/questions/6856028/difference-between-processbuilder-and-runtime-exec
http://lavasoft.blog.51cto.com/62575/15662/
0x3: Runtime.exec()源代码分析
/*
ProcessBuilder#start()} is now the preferred way to start a process with a modified environment.
*/
public Process exec(String[] cmdarray, String[] envp, File dir) throws IOException
{
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}
在JDK的实现中,Runtime.exec是只是一个包装函数,最终是通过调用ProcessBuilder实现的新进程创建
Relevant Link:
http://www.docjar.com/html/api/java/lang/Runtime.java.html
0x4: ProcessBuilder源代码分析
public Process start() throws IOException {
// Must convert to array first -- a malicious user-supplied
// list might try to circumvent the security check.
String[] cmdarray = command.toArray(new String[command.size()]);
cmdarray = cmdarray.clone();
for (String arg : cmdarray)
if (arg == null)
throw new NullPointerException();
// Throws IndexOutOfBoundsException if command is empty
String prog = cmdarray[0];
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkExec(prog);
String dir = directory == null ? null : directory.toString();
try {
return ProcessImpl.start(cmdarray,
environment,
dir,
redirects,
redirectErrorStream);
} catch (IOException e) {
// It's much easier for us to create a high-quality error
// message than the low-level C code which found the problem.
throw new IOException(
"Cannot run program \"" + prog + "\""
+ (dir == null ? "" : " (in directory \"" + dir + "\")")
+ ": " + e.getMessage(),
e);
}
}
继续跟进ProcessImpl.start
// Only for use by ProcessBuilder.start()
static Process start(String[] cmdarray,
java.util.Map<String,String> environment,
String dir,
ProcessBuilder.Redirect[] redirects,
boolean redirectErrorStream)
throws IOException
{
...
return new UNIXProcess
(toCString(cmdarray[0]),
argBlock, args.length,
envBlock, envc[0],
toCString(dir),
std_fds,
redirectErrorStream);
...
}
继续跟进UNIXProcess
//构造函数
UNIXProcess(final byte[] prog,
final byte[] argBlock, final int argc,
final byte[] envBlock, final int envc,
final byte[] dir,
final int[] fds,
final boolean redirectErrorStream)
throws IOException
{
pid = forkAndExec(prog,
argBlock, argc,
envBlock, envc,
dir,
fds,
redirectErrorStream);
try
{
AccessController.doPrivileged
(new PrivilegedExceptionAction<Void>()
{
public Void run() throws IOException
{
initStreams(fds);
return null;
}
}
);
}
catch (PrivilegedActionException ex)
{
throw (IOException) ex.getException();
}
}
继续跟进forkAndExec
/**
* Create a process using fork(2) and exec(2).
*
* @param fds an array of three file descriptors.
* Indexes 0, 1, and 2 correspond to standard input,
* standard output and standard error, respectively. On
* input, a value of -1 means to create a pipe to connect
* child and parent processes. On output, a value which
* is not -1 is the parent pipe fd corresponding to the
* pipe which has been created. An element of this array
* is -1 on input if and only if it is <em>not</em> -1 on
* output.
* @return the pid of the subprocess
*/
private native int forkAndExec(byte[] prog,
byte[] argBlock, int argc,
byte[] envBlock, int envc,
byte[] dir,
int[] fds,
boolean redirectErrorStream)
throws IOException;
/**
* The thread factory used to create "process reaper" daemon threads.
*/
private static class ProcessReaperThreadFactory implements ThreadFactory
{
private final static ThreadGroup group = getRootThreadGroup();
private static ThreadGroup getRootThreadGroup()
{
return AccessController.doPrivileged(new PrivilegedAction<ThreadGroup> ()
{
public ThreadGroup run()
{
ThreadGroup root = Thread.currentThread().getThreadGroup();
while (root.getParent() != null)
root = root.getParent();
return root;
}
}
);
}
public Thread newThread(Runnable grimReaper)
{
// Our thread stack requirement is quite modest.
Thread t = new Thread(group, grimReaper, "process reaper", 32768);
t.setDaemon(true);
// A small attempt (probably futile) to avoid priority inversion
t.setPriority(Thread.MAX_PRIORITY);
return t;
}
}
forkAndExec是一个JNI函数,即它的实现代码是通过外部的.so文件实现的
//本机上是这个
./usr/lib/jvm/java-1.7.0-openjdk-1.7.0.75.x86_64/jre/lib/amd64/libjava.so
./usr/lib/jvm/java-1.8.0-openjdk-1.8.0.31-1.b13.el6_6.x86_64/jre/lib/amd64/libjava.so
./usr/lib/jvm/java-1.6.0-openjdk-1.6.0.34.x86_64/jre/lib/amd64/libjava.so
objdump -tT /usr/lib/jvm/java-1.7.0-openjdk-1.7.0.75.x86_64/jre/lib/amd64/libjava.so | grep forkAndExec
0000000000018330 g DF .text 00000000000006fd SUNWprivate_1.1 Java_java_lang_UNIXProcess_forkAndExec
\openjdk\jdk\src\solaris\native\java\lang\UNIXProcess_md.c
JNIEXPORT jint JNICALL Java_java_lang_UNIXProcess_forkAndExec(JNIEnv *env,
jobject process,
jbyteArray prog,
jbyteArray argBlock, jint argc,
jbyteArray envBlock, jint envc,
jbyteArray dir,
jintArray std_fds,
jboolean redirectErrorStream)
{
...
//创建新的子进程
resultPid = startChild(c);
assert(resultPid != 0);
...
}
跟进resultPid = startChild(c);
static pid_t startChild(ChildStuff *c)
{
#if START_CHILD_USE_CLONE
...
return clone(childProcess, c->clone_stack + START_CHILD_CLONE_STACK_SIZE, CLONE_VFORK | CLONE_VM | SIGCHLD, c);
#else
volatile pid_t resultPid = vfork();
#else
//fork从当前进程中复制出子进程,fork是glibc库提供的包装函数,底层实现了fork()系统调用
pid_t resultPid = fork();
#endif
if (resultPid == 0)
//新创建的子进程需要继续调用exec() API系列
childProcess(c);
assert(resultPid != 0); /* childProcess never returns */
return resultPid;
#endif /* ! START_CHILD_USE_CLONE */
}
继续跟进childProcess(c);
/**
* Child process after a successful fork() or clone().
* This function must not return, and must be prepared for either all
* of its address space to be shared with its parent, or to be a copy.
* It must not modify global variables such as "environ".
*/
static int childProcess(void *arg)
{
...
JDK_execvpe(p->argv[0], p->argv, p->envv);
...
}
继续跟进JDK_execvpe(p->argv[0], p->argv, p->envv);
/**
* 'execvpe' should have been included in the Unix standards,
* and is a GNU extension in glibc 2.10.
*
* JDK_execvpe is identical to execvp, except that the child environment is
* specified via the 3rd argument instead of being inherited from environ.
*/
static void JDK_execvpe(const char *file, const char *argv[], const char *const envp[])
{
if (envp == NULL || (char **) envp == environ)
{
//调用glibc的execvp,将新的进程文件载入内存,进行执行
execvp(file, (char **) argv);
return;
}
...
}
总结一下
1. Runtime.getRuntime().exec(cmd)的执行流程:
java.lang.Runtime.exec(cmd);
--java.lang.ProcessBuilder.start();
----java.lang.ProcessImpl.start();
------Java_java_lang_UNIXProcess_forkAndExec() in j2se/src/solaris/native/java/lang/UNIXProcess_md.c
--------1). fork();
--------2). execvp();
2. Process p = new ProcessBuilder("myCommand", "myArg").start();的执行流程
--java.lang.ProcessBuilder.start();
----java.lang.ProcessImpl.start();
------Java_java_lang_UNIXProcess_forkAndExec() in j2se/src/solaris/native/java/lang/UNIXProcess_md.c
--------1). fork();
--------2). execvp();
/*
需要注意的是,fork产生的子进程需要复制父进程在内存中的所有数据内容(代码段、数据段、堆栈段),由于全部复制开销较大,因此Linux已经采用copy-on-write机制,即只是复制页表,共享内容,在有改变的时候再去申请内存和复制数据
*/
Relevant Link:
https://searchcode.com/codesearch/view/17990470/
http://www.docjar.com/html/api/java/lang/ProcessBuilder.java.html
https://searchcode.com/codesearch/view/17994182/
https://searchcode.com/codesearch/view/17966870/
http://blog.csdn.net/bigdatahappy/article/details/40115077
4. Java Create New Process Native Function API Analysis In Windows
待研究
Relevant Link:
http://www.ibm.com/developerworks/cn/java/j-lo-processthread/
http://docs.oracle.com/javase/7/docs/api/java/lang/ProcessBuilder.html
http://www.ibm.com/developerworks/cn/java/j-lo-processthread/
http://openjdk.java.net/groups/hotspot/
Copyright (c) 2014 LittleHann All rights reserved