KVM,本意:Kilobytes Virtual Mechine,形象点,可以称作Kjava Virtual
Mechine
是SUN当年为移动设备(大多是手机)开发的一套小型JAVA虚拟机。
KVM的代码后来被SUN开源了,但这个KVM的源代码只能算一个原型代码,并不是大家手机上目前使用的KJAVA虚拟机,几个大的手机厂商,如
Nokia,Motorola等都在这个基础上开发了自己的KVM,包括IBM为PDA开发的J9。
这些KVM虽然实现的都不同,但其核心原理和大体的代码不会离SUN的KVM原型代码差多远,毕竟他们都要符合KVM Spec。
我阅读这套原型代码的目的不是为了工作,只是想在一些嵌入/移动设备上能跑我自己写的KJAVA程序,比如作个能走出迷宫的机器人。
KVM的代码是C写的,它的部分实现必须依赖于底层操作系统。所以KVM的代码结构上分成了:
Common
Extra
Unix/Linux
Win/WinCE
其中Common和Extra是ANSI的代码部分,意味着在所有OS平台上都能正常编译运行,而依赖OS部分代码被分别放在Unix/Linx/Win
/WinCE里了。
而以上其实只包括了KVM中CLDC(Connected Limited Device
Configuration)的部分。KJAVA的本意是将KVM内部再切分为两块:CLDC和MIDP(Mobile Information
Device Profile),其中CLDC是核心。
KVM在完成了对JAVA字节码(也就是大家写的JAVA程序被编译后的码,称作bytecode)的解释执行以及JAVA核型包:
java.lang后,也就基本做到了CLDC的核心功能定义。
可以这么说,在不考虑
java.io和java.util的前提下,java.lang已经能完成很多核心功能了,是不是java.lang很简单呢?
NO
看看java.lang里包括的类吧,其中:
java.lang.Thread
就是KVM中比较复杂的代码部分了。想想也能意识到,线程这个东西是那么容易实现的吗?
可能有不少JAVA初学者对各种package如java.lang,java.io和java.util等等同底层的JAVA虚拟机关系搞不明白,它们
究竟是什么关系?我们写的JAVA和它们又是什么关系?
在这里我作个比较:
(Hardware/OS) +C Lib + C Compiler + C Application
KVM + JAVA Lib + JAVA Compiler + Java Application
大致能表达出经典的C程序和JAVA程序所涉及的范围。为什么要列出上面这个关系对比,是因为在后面阅读KVM代码时,有些概念和框架必须一开始就明确,
而不是稀里糊涂的就一头埋到代码里去了。
1。这里可以看出:KVM = Hardware/OS。就是说KVM的核心是对硬件(主要是CPU)的模拟。
2。C Lib是什么?它向C程序员提供了最基础的(AnisC)的所有函数,最直观的感受你写C代码时import的那些头文件。
JAVA lib是什么?可以当作你写JAVA代码时import的那些package。
大致有些感受了吧
C lib和JAVA lib是必须的吗?
No!
看看下面的C代码和JAVA代码
C:
int plus(int _input)
{
return _input++;
}
JAVA:
public int plus(int _input)
{
return _input++;
}
如果大家写的代码就上面这几行,我们还需要去import任何lib吗?根本没必要,因为这几行代码属于“语言自身的能力”,C
Compile和JAVA Compiler直接把上面代码编译成"CPU"指令就可以执行了。为什么上面会讨论到lib的这些细节,因为后面说到KVM代码时,我们会发现KVM对“普通执行码”也就是最接
近CPU指令的那些代码是一种处理方式,而另一些被import的代码部分是以另外的方式处理的。
3。CCompiler:GCC
JAVAcompiler:JAVAC
它们把程序员写的C和JAVA代码翻译为“机器码”。
JAVA
Compiler并不属于KVM,但阅读KVM代码时如果不理解JAVAC的工作机制可以说是步步为艰了。因为JAVAC几乎为KVM预先做好了一半的工
作。但对JAVAC,我们并不需要去了解它的内部代码和实现,只要关心的它的实现规范,也就是 Class Bytecodes
Spec就可以了,也就是大家编译后的Class文件内容。
进入KVM代码部分了。
作任何事情,我们都希望知道怎么开头的,KVM代码看起来很复杂,但有几根主线,我们顺着线头把它们拎起来理清楚,就能了解它大致的工作机制了。
当我们写好的JAVA程序被编译成CLASS字节码后,要使用KVM将它加载到内存里运行起来,首先,KVM自身要作一个初始化工作,这段代码位于:
./common/StartJVM.c
中,这个文件的自我描述是:
System initialization and start of JVM
关键函数:
KVM_Start
{
初始化OS相关数据
初始化内存管理
初始化事件管理
加载用户类
解析虚拟机初始参数
初始化线程管理
初始化Java Lib
将执行权转交JAVA字节码解释器
}
学过编程的大都知道,如果上面的KVM_Start就这么从上到下执行掉了,意味着加载了用户的代码后程序就退出了。
其实看一下这行:
将执行权转交JAVA字节码解释器
这调用了另一个重要文件里的核心函数:
execute.c
里的Interpret(),而这个函数真正的实现体是:
FastInterpret()
它是一个循环体,意味着从KVM_Start过来后,这里就不断循环在解释执行用户代码。也就是这里涉及到了KVM的两个关键功能:
1。线程管理
2。执行字节码在看KVM源码前,我一直没搞明白一件事:
字节码执行器和线程的关系。
究竟是字节码执行器拥有所有线程对象并分配每个线程的执行时间片,还是线程对象拥有字节码执行器并在自己的时间片中调用字节码执行器去解释此线程内的代码
段?
看了execute.c中的
SlowInterpret()和FastInterpret()我终于明白了:
1。KVM中的线程分为内核线程和JAVA线程两个概念,我们通常在写JAVA程序时接触的是JAVA线程
2。字节码执行器拥有主控制权,它从代码级对线程的调度作控制。
解释这两点:
1。KVM为什么要把线程搞成两个,不是变复杂了么?
从系统设计的角度来说,清晰的概念将决定清晰可读的代码,KVM的核心工作职责是对字节码的解释执行,如果把JAVA
Lib这个层次上的线程(java.lang.Thread)同这个核心工作混杂在一个级别那就是概念不清晰。
但KVM内部又的确是需要对线程作管理的,这就基本上导致系统内出现两种线程,一个是靠近KVM的核心线程,一个是向上为JAVA程序员提供的JAVA线
程。
这两个线程在KVM代码内都有相应结构(C strcuct):
位于文件thread.h中
struct threadQueue:核心线程
{
.....
运行时间片
javaThreadStruct* javaThread;
.......
}
struct javaThreadStruct:面向JAVA开发人员的线程
{
.....
}
可以看出,核心线程拥有指向JAVA线程的指针。
KVM的字节码执行器(excute.c)就是通过这两个结构了解每个线程的生命周期和各种属性,
interpret()正是通过threadQueue.运行时间片了解每个线程占用“CPU”的状况,以此决定是否要切换到另一个等待线程上还是继续运
行这个线程里其它的代码段
用一段伪代码我描述一下KVM内线程的管理机制:
void interpret()
{
while(!quitFlag)
{
if(当前线程的时间片是否全部使用完)
{
if(有其它等待线程)
{
运行其它等待线程的代码段;
}
else
{
继续执行当前线程的代码段
}
}
else
{
当前线程的时间片--;
继续执行当前线程的代码段;
}
}
}
可以看出,KVM内的线程是假线程,并不是真正的OS线程。
KVM和JVM面向不同的硬件环境的,JVM是我们通常在服务器和桌面运行的标准JAVA虚拟机,它不可能用这种假线程,因为目前PC/Server大多
有多核或者多颗CPU,最适合运行多进程和多线程的程序了(包括操作系统自身),JVM的源代码我没看过,但肯定把JAVA的线程实现映射到了真实OS线
程或进程上,那么JVM也就不会出现上面这样的伪代码实现机制。
毕竟KVM面向的移动设备大多是Symbian,嵌入式linux系统或者MTK,它们在有限的硬件上能否实现OS级线程都很难说。
KVM的字节码执行方式:
非常类似CPU处理机器指令的过程(KVM本来就是要模拟这个环节的),它们{zd0}的区别在于对寄存器的使用。
程序员在编写汇编代码的时候,是很清楚如何控制CPU的所有寄存器。KVM因为考虑低端的硬件设备,有些CPU只有很少的几个寄存器,那么KVM在被设计
的时候就不能被随便的设计为:支持具备N个寄存器的CPU。
KVM对此的解决方法是用“栈”来实现这个功能,汇编中指令的操作数大多存在寄存器中,运算的结果通常也放在寄存器中,KVM把这些都放在了“栈”中。它
被称为"操作栈“(Operand Stack),是个标准的FIFO。
这里举个使用"栈"来处理字节码的例子:
IADD:整数相加。
在汇编中,我们通常将加数和被加数以及结果都放到寄存器中:
ADD AX,BX //(AX+BX -> AX)
而KVM中,IADD的做法是:
int tmp=popStack(); //从"栈"中取被加数
*(int *SP)+=tmp; //从"栈"中取加数并和tmp相加,结果仍存到加数所在的"栈“地址,其实就是栈顶了
以上的popStack和SP都是"栈"操作
上面描述了KVM对"寄存器"概念的独特设计方法。
从整体来看,字节码的执行操作大致如下(以单线程为参考)
void interpret()
{
while(!quitFlag)
{
int operand = *ip;//从代码区取当前指令
swich(operand)
{//通过指令类别分别处理
case IADD:
int tmp=popStack();
*(int *SP)+=tmp;
break;
case ISUB:
............
break;
....................
default:
.........
break;
}
ip++;
}
}说道字节码,可能还是没法让我们认识到JAVA Lib层的实现方式,其实这个问题也是大部分研究KVM的人最关心的事:
如果搞明白了JAVA
Lib是怎么实现的,就可以在KVM中添加自己需要的新功能,比如让KVM支持USB!那样就可以在KJAVA里写程序来控制一些USB设备了。
回到JAVA代码中,我们先写几行代码:
public class test
{
public void testFunc()
{
abc();
}
}
这里的abc()可以看作调用test类的某个成员函数,或者是调用java.lang中某个类的成员函数。
那么在经过JavaCompiler编译后这段代码变成了什么样的字节码呢?大致如下:
Method testFunc()
0 aload_0
1 invokespecial #5 <Method abc()>
return
"0 aload_0"这个先别管了(将test intance自身入"栈"),看下面那行
1 invokespecial #5 <Method abc()>
这个“invokespecial“就是一个KVM指令,地位上等同于上面的IADD,它要干嘛?它执行一段字节码就了事了?
其实它要调用另外一个非常重要的代码段来完成对“JAVA级别的函数”调用,而不是停留在字节码这个层次上。
callMethod_general
{
........
}
真是这个代码段做到对abc()的实际调用。java.lang
中的类,从某种意义上来讲,它相当于C Lib里的头文件,却不是真实的lib。
它的实现体位于KVM中,更具体的说,位于:
nativeCore.c
这个文件里,那么我们调用java.lang的过程是怎么回事?
上面那段代码里谈到callMethod_general时就差不多涉及到我们的问题核心了。
callMethod_general会查看你是否调用的java.lang内功能,如果是,它将此调用映射到
nativeCore.c
中,通过nativeCore.c内对java.lang的具体实现来达到目的。
到这里为止,已经基本了解
(Hardware/OS) +C Lib + C Compiler + C Application
KVM + JAVA Lib + JAVA Compiler + Java Application
这个对比的含义了。我们能通过KVM的代码“感受”到各个阶段和模块的用意。
那么其它的java package是否也是这么实现的?
不是,尽管 java.io和java.util内某些功能仍依赖于在KVM中实现,但不是全部。
比如Vector这个类,它本来是可以在KVM里去作,但那样nativeCore.c会变得很复杂。
其实只要java.lang完成了,那么java.util.Vector就可以用纯粹的JAVA来实现,而不是用C。
大家在安装好JDK后,可以看看JDK根目录下有个src.zip这个文件,解压后可以看到,很多java的package都根本就是用JAVA写的了,
根本不涉及到JVM那么底层。那是因为
java.lang
java.io
java.util
3大基础package有了后,它们已经能够“组装”出更多其它功能的package了。