Avian JVM – z czym to się je

0

Zapewne każdy (a przynajmniej większość) odwiedzający m4txbloga wie, że jest coś takiego jak Java i że programy napisane w niej uruchamia się za pomocą wirtualnej maszyny Javy (w skrócie JVM); większość ma także taką maszynę wirtualną zainstalowaną i w zdecydowanej większości przypadków jest to Oracle JVM, bądź OpenJDK. Nie każdy jednak wie, że są alternatywy. Lepsze alternatywy.

Mowa oczywiście o tytułowym Avian JVM.

Czymże jednak jest ten Avian JVM? Z artykułu na anglojęzycznej Wikipedii dowiadujemy się, że Avian to otwarta i lekka implementacja maszyny wirtualnej Javy. Nie byłoby w niej jednak nic szczególnego (w końcu otwartą implementacją jest również OpenJDK, który jest, swoją drogą, implementacją wzorcową dla maszyn wirtualnych Javy). Avian JVM pozwala jednak na coś, na co nie pozwala żadna z dwóch najpopularniejszych implementacji JVM: pozwala tworzyć samowystarczalne pliki wykonywalne aplikacji. Daje to więc szereg nowych możliwości:

  • Pozwala tworzyć aplikacje które nie wymagają zainstalowanej maszyny wirtualnej Javy w systemie,
  • Pozwala na dostarczanie aplikacji tam, gdzie zainstalowanie JVM-a nie jest możliwe – najpopularniejszym przykładem jest tu chyba system iOS,
  • Pozwala zmniejszyć ilość danych koniecznych do uruchomienia aplikacji – zamiast aplikacji i wirtualnej maszyny Javy (która „waży” przecież swoje, bo około 70MB) dostarczamy wszystko w jednym pliku, a maszyna wirtualna jest dostosowana do naszej aplikacji (można bowiem usunąć z biblioteki standardowej wszystko to, co nie jest w danej aplikacji potrzebne, a biorąc pod uwagę obszerność i wszechstronność domyślnej biblioteki standardowej Javy, zyskujemy całkiem sporo),
  • Zwiększa nieco szybkość działania (a zwłaszcza uruchamiania) aplikacji, a także minimalnie zmniejsza zużycie pamięci.

Oprócz tego Avian JVM stosuje podstawowe techniki optymalizacyjne tak jak najpopularniejsze implementacje JVM – nie zabrakło więc m.in. kompilatora JIT.

Brzmi wspaniale? Oczywiście! Przejdźmy więc do części praktycznej wpisu…

Pobieranie i podstawowa konfiguracja Avian JVM

Pierwszym, co należy zrobić by móc poobcować z Avian JVM, jest oczywiście pobranie go. Najprościej zrobić to poprzez sklonowanie repozytorium gita (za pomocą komendy git clone https://github.com/ReadyTalk/avian.git) bądź poprzez pobranie ZIP-a z repozytorium na githubie.

(Uprzedzam także od razu, że korzystam z Linuksa, konkretniej Ubuntu 13.04 64-bit, więc prezentowane tutaj komendy będą przeznaczone właśnie na Linuksa)

Najpierw potrzebowali będziemy kompilatora i bibliotek niezbędnych do kompilacji Aviana. Wydajemy więc polecenie:
$ sudo apt-get install g++ zlib1g-dev openjdk-7-jdk
Następnie musimy podać ścieżkę do JDK oraz skompilować maszynę wirtualną:

$ export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64/ # W przypadku, gdy mam OpenJDK w wersji 7; w przeciwnym razie należy samemu zlokalizować miejsce instalacji JDK
$ make

Testujemy działanie Avian JVM:

$ build/linux-x86_64/avian -cp build/linux-x86_64/test Hello
hello, world!

Osoby, które z wirtualnymi maszynami Javy mają kontakt nie od dziś, z pewnością zauważą znacznie krótszy czas uruchamiania Aviana niż OpenJDK. Małe (i mało obiektywne co prawda, ale jednak) porównanie:

m4tx@m4tx-N56VZ:~/Programs/Java/Avian/avian-master$ time build/linux-x86_64/avian -cp build/linux-x86_64/test Hello
hello, world!

real	0m0.008s
user	0m0.000s
sys	0m0.004s
m4tx@m4tx-N56VZ:~/Programs/Java/Avian/avian-master$ time java -cp build/linux-x86_64/test Hello
hello, world!

real	0m0.072s
user	0m0.060s
sys	0m0.012s

(Porównanie Aviana 0.7 i OpenJDK 1.7.0_21)

Jak więc widać, różnica jest dość duża. Porównywanie szybkości jednak to nie temat tego wpisu – przejdźmy więc do następnej sekcji.

Tworzenie samowystarczalnych plików wykonywalnych

Posłużymy się, tak jak i w poprzedniej części, poradnikiem na stronie readme Aviana.

Wpierw tworzymy więc katalog „hello” i przechodzimy do niego. Rozpakowujemy do niego zawartość libavian.a, a także kopiujemy plik classpath.jar i zmieniamy jego docelową nazwę na boot.jar.

$ mkdir hello
$ cd hello
$ ar x ../build/linux-x86_64/libavian.a
$ cp ../build/linux-x86_64/classpath.jar boot.jar

Po całej operacji uzyskujemy mniej-więcej taką strukturę plików:

$ ls -la
razem 29064
drwxrwxr-x 2 m4tx m4tx    4096 lip 16 17:27 .
drwxrwxr-x 9 m4tx m4tx    4096 lip 16 17:26 ..
-rw-rw-r-- 1 m4tx m4tx  579026 lip 16 17:27 boot.jar
-rw-rw-r-- 1 m4tx m4tx  818896 lip 16 17:27 build_linux-x86_64_builtin.o
-rw-rw-r-- 1 m4tx m4tx 1479064 lip 16 17:27 build_linux-x86_64_classpath-avian.o
-rw-rw-r-- 1 m4tx m4tx  140064 lip 16 17:27 build_linux-x86_64_codegen_compiler_context.o
-rw-rw-r-- 1 m4tx m4tx  902840 lip 16 17:27 build_linux-x86_64_codegen_compiler_event.o
-rw-rw-r-- 1 m4tx m4tx  157368 lip 16 17:27 build_linux-x86_64_codegen_compiler_frame.o
-rw-rw-r-- 1 m4tx m4tx  143912 lip 16 17:27 build_linux-x86_64_codegen_compiler_ir.o
-rw-rw-r-- 1 m4tx m4tx 1349920 lip 16 17:27 build_linux-x86_64_codegen_compiler.o
-rw-rw-r-- 1 m4tx m4tx  190920 lip 16 17:27 build_linux-x86_64_codegen_compiler_promise.o
-rw-rw-r-- 1 m4tx m4tx  221208 lip 16 17:27 build_linux-x86_64_codegen_compiler_read.o
-rw-rw-r-- 1 m4tx m4tx  259544 lip 16 17:27 build_linux-x86_64_codegen_compiler_regalloc.o
-rw-rw-r-- 1 m4tx m4tx  188464 lip 16 17:27 build_linux-x86_64_codegen_compiler_resource.o
-rw-rw-r-- 1 m4tx m4tx  452584 lip 16 17:27 build_linux-x86_64_codegen_compiler_site.o
-rw-rw-r-- 1 m4tx m4tx  198376 lip 16 17:27 build_linux-x86_64_codegen_compiler_value.o
-rw-rw-r-- 1 m4tx m4tx   96536 lip 16 17:27 build_linux-x86_64_codegen_registers.o
-rw-rw-r-- 1 m4tx m4tx   96568 lip 16 17:27 build_linux-x86_64_codegen_targets.o
-rw-rw-r-- 1 m4tx m4tx  411504 lip 16 17:27 build_linux-x86_64_codegen_target_x86_assembler.o
-rw-rw-r-- 1 m4tx m4tx  132376 lip 16 17:27 build_linux-x86_64_codegen_target_x86_block.o
-rw-rw-r-- 1 m4tx m4tx  142736 lip 16 17:27 build_linux-x86_64_codegen_target_x86_context.o
-rw-rw-r-- 1 m4tx m4tx  136656 lip 16 17:27 build_linux-x86_64_codegen_target_x86_detect.o
-rw-rw-r-- 1 m4tx m4tx  549368 lip 16 17:27 build_linux-x86_64_codegen_target_x86_encode.o
-rw-rw-r-- 1 m4tx m4tx  218656 lip 16 17:27 build_linux-x86_64_codegen_target_x86_fixup.o
-rw-rw-r-- 1 m4tx m4tx  168616 lip 16 17:27 build_linux-x86_64_codegen_target_x86_multimethod.o
-rw-rw-r-- 1 m4tx m4tx  713872 lip 16 17:27 build_linux-x86_64_codegen_target_x86_operations.o
-rw-rw-r-- 1 m4tx m4tx  142968 lip 16 17:27 build_linux-x86_64_codegen_target_x86_padding.o
-rw-rw-r-- 1 m4tx m4tx 6880936 lip 16 17:27 build_linux-x86_64_compile.o
-rw-rw-r-- 1 m4tx m4tx     940 lip 16 17:27 build_linux-x86_64_compile-x86-asm.o
-rw-rw-r-- 1 m4tx m4tx  524976 lip 16 17:27 build_linux-x86_64_finder.o
-rw-rw-r-- 1 m4tx m4tx  787576 lip 16 17:27 build_linux-x86_64_heap_heap.o
-rw-rw-r-- 1 m4tx m4tx  421784 lip 16 17:27 build_linux-x86_64_java-io.o
-rw-rw-r-- 1 m4tx m4tx  421648 lip 16 17:27 build_linux-x86_64_java-lang.o
-rw-rw-r-- 1 m4tx m4tx  205912 lip 16 17:27 build_linux-x86_64_java-net.o
-rw-rw-r-- 1 m4tx m4tx  488768 lip 16 17:27 build_linux-x86_64_java-nio.o
-rw-rw-r-- 1 m4tx m4tx  170688 lip 16 17:27 build_linux-x86_64_java-util.o
-rw-rw-r-- 1 m4tx m4tx  252736 lip 16 17:27 build_linux-x86_64_java-util-zip.o
-rw-rw-r-- 1 m4tx m4tx 2746152 lip 16 17:27 build_linux-x86_64_jnienv.o
-rw-rw-r-- 1 m4tx m4tx 5397792 lip 16 17:27 build_linux-x86_64_machine.o
-rw-rw-r-- 1 m4tx m4tx  399840 lip 16 17:27 build_linux-x86_64_process.o
-rw-rw-r-- 1 m4tx m4tx  551960 lip 16 17:27 build_linux-x86_64_util.o
-rw-rw-r-- 1 m4tx m4tx  519888 lip 16 17:27 build_linux-x86_64_vm_system_posix.o
-rw-rw-r-- 1 m4tx m4tx    1157 lip 16 17:27 build_linux-x86_64_x86-asm.o

Następnie tworzymy plik Hello.java i piszemy kod źródłowy programu. W moim (naszym?) przypadku będzie to po prostu standardowy Hello world.

$ vim Hello.java
public class Hello {
    public static void main(String args[]) {
        System.out.println("Hello, world!");
    }
}

Następnie kompilujemy nasz program i tworzymy plik .JAR:

$ javac -bootclasspath boot.jar Hello.java
$ jar u0f boot.jar Hello.class

…potem zaś przekształcamy plik .JAR w plik .o:

$ ../build/linux-x86_64/binaryToObject/binaryToObject boot.jar boot-jar.o _binary_boot_jar_start _binary_boot_jar_end linux x86_64

Piszemy w C++ program, który uruchamia naszą maszynę wirtualną i wykonuje określoną funkcję.

$ vim embedded-jar-main.cpp

Ja oczywiście poszedłem na łatwiznę i skopiowałem kod z podanego wcześniej tutoriala… 🙂

#include "stdint.h"
#include "jni.h"
#include "stdlib.h" 

#if (defined __MINGW32__) || (defined _MSC_VER)
#  define EXPORT __declspec(dllexport)
#else
#  define EXPORT __attribute__ ((visibility("default"))) \
  __attribute__ ((used))
#endif

#if (! defined __x86_64__) && ((defined __MINGW32__) || (defined _MSC_VER))
#  define SYMBOL(x) binary_boot_jar_##x
#else
#  define SYMBOL(x) _binary_boot_jar_##x
#endif

extern "C" {
    extern const uint8_t SYMBOL(start)[];
    extern const uint8_t SYMBOL(end)[];

    EXPORT const uint8_t*
    bootJar(unsigned* size)
    {
        *size = SYMBOL(end) - SYMBOL(start);
        return SYMBOL(start);
    }
}

extern "C" void __cxa_pure_virtual(void) {
    abort();
}

int main(int ac, const char** av)
{
    JavaVMInitArgs vmArgs;
    vmArgs.version = JNI_VERSION_1_2;
    vmArgs.nOptions = 1;
    vmArgs.ignoreUnrecognized = JNI_TRUE;

    JavaVMOption options[vmArgs.nOptions];
    vmArgs.options = options;

    options[0].optionString = const_cast<char*>("-Xbootclasspath:[bootJar]");

    JavaVM* vm;
    void* env;
    JNI_CreateJavaVM(&vm, &env, &vmArgs);
    JNIEnv* e = static_cast<JNIEnv*>(env);

    jclass c = e->FindClass("Hello");
    if (!e->ExceptionCheck()) {
        jmethodID m = e->GetStaticMethodID(c, "main", "([Ljava/lang/String;)V");
        if (!e->ExceptionCheck()) {
            jclass stringClass = e->FindClass("java/lang/String");
            if (!e->ExceptionCheck()) {
                jobjectArray a = e->NewObjectArray(ac-1, stringClass, 0);
                if (!e->ExceptionCheck()) {
                    for (int i = 1; i < ac; i++) {
                        e->SetObjectArrayElement(a, i-1, e->NewStringUTF(av[i]));
                    }
                    e->CallStaticVoidMethod(c, m, a);
                }
            }
        }
    }

    int exitCode = 0;
    if (e->ExceptionCheck()) {
        exitCode = -1;
        e->ExceptionDescribe();
    }

    vm->DestroyJavaVM();
    return exitCode;
}

Możemy więc w końcu skompilować i zlinkować nasz program!

$ g++ -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -D_JNI_IMPLEMENTATION_ -c embedded-jar-main.cpp -o main.o
$ g++ -rdynamic *.o -ldl -lpthread -lz -o hello
$ strip --strip-all hello

Po wykonaniu pierwszych dwóch poleceń otrzymujemy plik wykonywalny hello o rozmiarze aż 8,3MB. Dlatego też wykonujemy także trzecią komendę, za pomocą której wycinamy z naszego pliku wszystko to, co nie jest potrzebne do jego poprawnego wykonania. Otrzymujemy plik o rozmiarze 1,3MB.

Na końcu sprawdzamy jeszcze, czy wszystko działa:

$ ./hello
Hello, world!

🙂

Zakończenie

W tym krótkim poradniku przedstawiłem po krótce zalety wirtualnej maszyny Avian oraz opisałem, w jaki sposób można utworzyć plik wykonywalny zawierający program napisany w Javie, jednak niewymagający zainstalowanej wirtualnej maszyny Javy w systemie. Oczywiście, wszystkie aspekty potraktowałem dość skrótowo, bo m.in. nie opisałem, jak użyć LZMA, by jeszcze zmniejszyć rozmiar pliku wykonywalnego, jednak wpis miał mieć charakter bardzo krótkiego i niezbyt zaawansowanego wprowadzenia do tematu „czym jest Avian JVM, oraz do czego i jak go można użyć”. Być może za jakiś czas pojawią się na m4txblogu kolejne wpisy dotyczące Aviana – chociażby o tym, jak użyć zewnętrznych bibliotek, jak skompresować plik wykonywalny oraz poruszające inne ciekawe aspekty 🙂

Pozdrawiam.

Zostaw komentarz

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>