玩转JVM – 为wrtnode编译一个嵌入式JVM

玩转JVM – 为wrtnode编译一个嵌入式JVM

摩尔定律到达顶峰时期,物联网时代正在袭来,各种智能硬件玲琅满目,这让硬件和软件的的界限越来越模糊,硬件工程师可以不再面对元件一一焊起,软件工程师也可以轻易买到各种开发板,它已经”超乎“了嵌入式的概念,因为它完全是台小电脑。

从软件角度,当前NodeJS和Python等语言由于其轻量级内核(例如Python内核仅仅861Kbytes),因此厂商们为了吸引更多的爱好者和开发者使用产品,很多板子都会搭载NodeJS,例如,MTK的Sed Stadio,wrtnode等等。作为资深Java工程师,我开始着手研究能否在这些板子上跑JVM呢?本人以wrtnode r2 http://www.wrtnode.cc/ 入手,做了一些研究,成功运行起JVM。值得提起的是,我希望是能够支持比较完整的JavaSE API的JVM,我不希望是安卓,或者是JavaME方式,因为那样改变了我的编程方式,我希望我可以从public static void main(String args[]) 开始编写Java程序。

我们来看看wrtnode 2r的硬件参数,这里只关注CPU,RAM和ROM,IO和其他定时器等不需要考察:

  • CPU:MTK MT7688AN mips24k 主频580M,
  • DDR2 256MB RAM,
  • NOR FLASH 32MB ROM
  • Mips架构,el小端存储

可以看见其CPU基本和2000年左右的奔腾II差不多性能,RAM高达256M,运行JVM绰绰有余,只是ROM,在搭载其嵌入式操作系统占用5.6M后,可用的ROM只有约20M,换言之,我们必须把我们的JVM剪裁到20M以内。另外,wrtnode把/tmp/目录做了内存文件映射,如果实在不行,可以考虑把非关键组件放到其中,问题不言而喻,掉电就丢失。

开始着手研究:

第一步,能否把OpenJDK或者OracleJDK对于的架构呢?我很快就否定了他们,因为他们是在太庞大了,很难清理出一个干净的JVM。

第二部,那么既然这些常用的虚拟机,OpenJDK,OracleJDK, JRokit IceTea等等都太大,难以剪裁,那么会不会有开源爱好者,自行按照Java委员会标准编写虚拟机呢?拓拓的开始Google一把,果然大有人在,心中甚喜。这里贴一个非常用Java虚拟机实现,http://www.gnu.org/software/classpath/stories.html#jvm,综合比较后,我选择了JamVM,理由很简单:


 

JamVM is an open-source Java Virtual Machine that aims to support the latest version of the JVM specification, while at the same time being compact and easy to understand.

JamVM “Features”

For those interested in the design of virtual machines, JamVM includes a number of optimisations to improve speed and reduce foot-print. A list, in no particular order, is given below.

  • Execution engine supports many levels of optimisation from basic switched interpreter to inline-threaded interpreter with stack-caching (equivalent performance to a simple JIT).
  • Uses native threading (posix threads). Full thread implementation including Thread.interrupt()
  • Object references are direct pointers (i.e. no handles)
  • Supports class loaders
  • Efficient thin locks for fast locking in uncontended cases (the majority of locking) without using spin-locking
  • Two word object header to minimise heap overhead (lock word and class pointer)
  • Stop-the-world garbage collector, with separate mark/sweep and mark/compact phases to minimise heap fragmentation
  • Thread suspension uses signals to reduce suspend latency and improve performance (no suspension checks during normal execution)
  • Full object finalisation support within the garbage collector (with finaliser thread)
  • Full support for Soft/Weak/Phantom References (with Reference Handler thread)
  • Full support for class and class-loader garbage-collection and unloading (including associated shared libraries)
  • Garbage collector can run synchronously or asynchronously within its own thread
  • String constants within class files are stored in hash table to minimise class data overhead (string constants shared between all classes)
  • Supports JNI and dynamic loading for use with standard libraries
  • Uses its own lightweight native interface for internal native methods without overhead of JNI
  • VM support for invokedynamic (JSR 292)
  • VM support for type annotations (JSR 308)
  • VM support for lambda expressions (JSR 335)
  • VM support for method parameter reflection
  • JamVM is written in C, with a small amount of platform dependent assembler, and is easily portable to other architectures.

 

它竟然可以支持最新的JVM规范,换言之,他是完完整整的JavaSE。再看其特性,简直了….

敲定方案:JamVM + Classpath方案

第三步,那就的寻找wrtnode官方的交叉编译工具了,很快找到官方WIKI:

http://wiki.wrtnode.com/index.php?title=How_to_compile_with_the_openwrt_toolchain/zh-cn

迅速在我的Ubuntu上安装。

第四步,下载JamVM http://jamvm.sourceforge.net/ 开始交叉编译JVM可执行程序

第五步,下载Classpath http://www.gnu.org/software/classpath/ 编译出类目录,可以理解为rt.jar。

第六步,把编译出来的JVM打zip包,此时zip包的文件一共仅仅22M左右,拷贝到本地进行精简,例如删除里面的Sample,不必要的include和.html等文件。最终整个压缩包仅仅15M左右,惊呼!

第七步,把JVM SCP到wrtnode上面,我写了一个简单的测试程序,用一个for循环循环10000次,每次sleep(1, 2, 3, …),最终计算一下线程调度的的延迟,只是偶尔出现1ms的误差,不得不佩服其性能。最小-Xms768k能够启动其JVM ! 我可以用Java  + JNI来编写我的嵌入式程序了,太棒了


 

当然,我这里一二三四五六七写的很简单,但是真正的摸索和交叉编译过程会遇到各种坎,这里写下来和大家分享:

  • 每一步完成后做必要的检查后,再进行下一步,例如,在安装完交叉编译链后,应该自己写一个helloword.c文件,编译出来,然后移植到板子跑一下,至少需要file一下,看看文件是否是目标平台的字节码。
  • 有zlib依赖,先可以用apt-get install zlib1g-dev给ubuntu系统安装,然后再通过源代码交叉编译给工具链,注意用file命令检查,是否为目标平台字节码

这里付上一个翻墙找到的好帖子,我的成功基本按照其流程完成,想动手的小伙伴可以试试。

This project dealt with:

  • Processor: MIPS 24Kc
  • JVM: JamVM 1.5.4
  • Classpath 0.98 (Could have been 0.99)

Here are the adapted steps, assuming the cross-compile tools are built and setup beforehand.

  1. zlib
Download zlib 1.2.8 from http://www.zlib.net/
Build:
CHOST=mips-example-linux-gnu ./configure
Before make, edit the Makefile to add -fPIC to the SFLAGS that’s used to build the static lib. Otherwise will encounter error when building JamVM.
vim Makefile
    SFLAGS=-O3  -fPIC -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN
make
Install the lib and headers to the toolchains directory:
sudo cp libz.a [toolchains_path]/lib
sudo cp zlib.h [toolchains_path]/include
sudo cp zconf.h [toolchains_path]/include
Edit
  1. JamVM
Download from http://jamvm.sourceforge.net/
Edit the configure to support mips-*-linux compiler:
vim configure   
   mips-*-linux*) host_cpu=mips host_os=linux ;;
Configure and build:
./configure --host=mips-example-linux-gnu --with-classpath-install-dir=/usr/local --prefix=/usr/local --enable-shared --enable-int-threading --enable-int-direct --enable-int-caching --enable-int-prefetch --enable-runtime-reloc-checks --enable-tls --enable-dependency-tracking
make
I think the -install-dir and -prefix is whatever will be like at the target system. Here I just follow convention.
This is what will be printed when running “jamvm -version” on target later on.
Boot Library Path: /usr/local/lib/classpath
Boot Class Path: /usr/local/share/jamvm/classes.zip:/usr/local/share/classpath/glibj.zip
Strip down the binary (I think this step can be skipped if the target build will strip it when making the rootfs):
cd src
mips-example-linux-gnu-strip jamvm

Deploy to target at:

  • src/jamvm -> /usr/sbin/jamvm
  • lib/classes.zip -> /usr/local/share/jamvm/
Edit
  1. Classpath
JamVM 1.5.4 requires GNU classpath.
Download 0.98 at http://www.gnu.org/software/classpath/
Configure and build:
./configure --host=mips-example-linux-gnu --without-x --disable-gtk-peer --disable-plugin --disable-dssi --enable-jni --disable-gconf-peer --enable-default-preferences-peer=memory --enable-default-toolkit --disable-examples --disable-tools --disable-Werror --disable-alsa
make
The resulted lib/glibj.zip is ~9MB.
This can be reduced further. Unzip the glibj.zip, remove “unneeded” (for now) class and zip back.
Depends on the apps to run, some cannot be removed.
For example, these classes can be removed:
– gnu/CORBA
– gnu/java/awt
– gnu/javax/imageio
– gnu/javax/sound
– gnu/javax/swing
– java/sql/
– javax/imageio
– javax/sound
– javax/sql
– javax/swing
Zip the reduced classpath. This is reduced to about 6.5MB.
zip glibj.zip -r *

Deploy to target:

  • lib/glibj.zip -> /usr/local/share/classpath/glibj.zip
All the native libraries must also be deployed on target:
find ./native/ -name "*.so" -exec cp {} ${ROOT_PATH}/usr/local/lib/classpath \;

After completed the above step, should be able to just run the java apps by:

jamvm -jar hello.jar

发表评论

您的电子邮箱地址不会被公开。