GraalVM — Make Java Great Again

Nam Vu
11 min readFeb 26, 2020

Sau 8 năm phát triển tích cực cuối cùng vào tháng 5/2019 các kỹ sư ở Oracle đã ra mắt GraalVM — một máy ảo “đa ngôn ngữ” hiệu xuất cao và có tính năng rất đặc biệt là có thể biên dịch ứng dụng Java thành mã máy (native image) để ứng dụng có thể chạy trực tiếp trên mỗi nền tảng mà không cần cài đặt JRE hay các Webserver như Tomcat, Jetty …

GraalVm là gì ?

GraalVM là một máy ảo (virtual machine) cho phép chạy các chương trình viết trên rất nhiều ngôn ngữ khác nhau như JavaScript, Python, Ruby, R, những ngôn ngữ chạy trên máy ảo Java (JVM) như là Java, Scala, Groovy, Kotlin, Clojure, hay các ngôn ngữ chạy trên LLVM như C and C++.

Nói một cách vui vẻ thì GraalVM giống như chiếc nhẫn chúa trong Lord of the Rings vậy, một chiếc nhẫn chúa (GraalVM) thống trị tất cả (Java, Python, Ruby, NodeJS …) :D

Điều này rất tuyệt vời nếu bạn làm việc với rất nhiều ngôn ngữ khác nhau mà chỉ cần một compiler và runtime duy nhất :D.

Nếu ta chỉ xét với các ứng dụng viết trên nền tảng Java thì GraalVM cung cấp cho ta hai lợi thế sau:

1. Giúp chương trình Java chạy nhanh hơn so với máy ảo Java truyền thống (JVM) bằng cách cung cấp một bộ JIT (just-in-time) hoàn toàn mới. Ngoài GraalVM thì các bộ máy ảo khác như Adopt OpenJDK hay Zulu OpenJDK cũng cung cấp các bộ JIT mới và họ cũng tự quảng bá ràng nó nhanh hơn :P . Với người viết tính năng này không đáng chú ý cho lắm :P .

2. Giúp biên dịch mã Java trực tiếp thành mã máy (machine code — native code — native images), có nghĩa là ta có thể chạy trực tiếp chương trình viết trên Java trên mọi nền tảng (Linux — Window) mà không cần đến JRE. Quá trình này gọi là “ahead-of-time (AOT) compilation”, và tất nhiên với AOT thì không phải bàn cãi rằng chương trình sẽ khởi động rất nhanh (faster startup) và tất nhiên tốc độ và độ ổn định cũng sẽ được cải thiện, ngoài ra cũng giúp tiết kiệm tài nguyên hệ thống hơn (smaller footprint).

Java fat and to slow

AOT là một tính năng cực kỳ quan trọng với các ứng dụng Java chạy trên nền kiến trúc Serverless và Micro-services, bởi vì về bản chất Java rất nặng và chậm chạp (Java fat and to slow).

Quay về những năm 1999 đây sẽ là cái giá để chúng ta chạy một ứng dụng Java Web App cơ bản là 463k $ — gần nửa triệu đô (nguồn từ Burrsutter — Red Hat engineer) để mua những con Sun Solaris server với cấu hình khủng và to như cái máy giặt :P .

Và rất vui cho chúng ta là 20 năm sau chi phí để bắt đầu chạy một hệ thống tương tự chỉ tốn vài đô la và tùy vào nhu cầu sử dụng mà chi phí sẽ tăng lên nhờ vào cuộc cách mạng của Cloud Computing (Amazon Lambda — EC2).

Vậy tại sao ứng dụng Java lại nặng nề chập chạp như vậy thì ta phải nhìn vào cách hoạt động của JVM khi một ứng dụng bắt đầu chạy. JVM sẽ đọc các file cấu hình và load các jar files lên bộ nhớ (memory), quét toàn bộ annotation và build các meta model lên memory … quá trình này tốn thời gian và bộ nhớ và chúng ta sẽ không lấy lại tài nguyên đó, trừ khi khởi động lại JVM (restart web server). Quá trình reset JVM khá quan trọng để hệ thống có thể thu hồi lại được memory trên RAM, các thread pool trên CPU hoặc các connection pool của Database, nói đơn giản là phục hồi lại tài nguyên của hệ thống.

Nhưng bây giờ với Mirco-service hay Serverless chúng ta sẽ đưa ứng dụng vào một thứ mới mẻ gọi là Container, có nghĩa là ta sẽ nhốt (lock it down) ứng vào trong một Linux Container, ví dụ điển hình là sử dụng Docker để tạo ra các container và quản lý (deployment, scaling, and management) các container bằng Kubernetes.

Và vấn đề cũng từ đó mà ra, với Container chúng ta có thể giới hạn được Memory hay CPU và mặc định với ứng dụng Java (đã vốn rất nặng)khi ứng dụng sử dụng nhiều hơn số tài nguyên được cấp thì BOOMM!! OOM (out of memory) KILLER sẽ xuất hiện khi đó ứng dụng hoặc web-app server (Tomcat, Jetty, JBoss, Weblogic …) sẽ shut down.
Chẳng còn các nào khác là chúng ta phải restart lại JVM hoặc Web-server, và có một điều thú vị là Kubernetes vốn đã tự động làm được điều này (restart JVM), chúng ta sẽ nghĩ rằng: “Well… ít ra thị hệ thống cũng tự restart lại và ứng dụng vẫn tiếp tục chạy”.
Mặc dù thực tế thì chúng ta chẳng thể biết ứng dụng tại sao lại shut-down và quản lý (mananger) của chúng ta sẽ nghĩ rằng “đội ngũ LTV của tôi đã viết những mã nguồn tệ hại (bad code), khi ứng dụng tự động restart nhờ K8S tôi chẳng có thời gian để operator & monitoring điều gì đã xảy ra và thực ra hệ thống không được phép tự động restart lại như vậy”.

Khi nhu cầu thị trường tăng cao và chúng ta phải scale lên nhiều container lúc đó vấn đề sẽ trở nên thực sự thử thách với việc quản lý. Với 1 JVM việc quản lý tương đối dễ nhưng hãy thử tưởng tượng 20, 50 hay 100 JVM thì chúng ta sẽ phải tốn khối lượng làm việc khổng lồ để quản lý toàn bộ chúng.

Đó chính là lý do chúng ta hầu như không sử dụng nền tảng Java với các kiến trúc như Serverless (Amazon Lambda) hay Micro-services, thay vào đó thường chúng ta sẽ chọn sử dụng các nền tảng gọn nhẹ như NodeJS, Go, Python hay C++ thay thế.

Nhưng với GraalVM với tính năng build ứng dụng Java ra mã máy (native code) và có thể chạy độc lập không cần JVM hay các Web-App Server như Tomcat hay Jetty … Điều này sẽ giúp ứng dụng khởi động và chạy nhanh và ít tốn tài nguyên hơn, nguyên việc bỏ JVM hay Web-Server đi đã tiết kiệm cho tài nguyên hệ thống kha khá rồi và giúp việc khởi tạo các Container cũng trở nên vô cùng đơn giản.

Ngoài ra GraalVM cũng có cơ chế optimized source code như xóa toàn bộ những dead code như web-server Jetty hay bộ XML parsers mà chúng ta chẳng bao giờ dùng đến, và sự thật là JVM trong quá trình build nó đã đính kèm (include) hàng tỷ những dead code đó. Để cho dễ hiểu hơn ta hãy xét ví dụ ứng dụng của chúng ta sử dụng Hibernate, và mặc định nó include tất cả các lớp (class) ta sử dụng để kết nối (connect) với các DB như MySQL, PostgreSQL hay Oracle. Nhưng chúng ta chỉ sử dụng Oracle trong ứng dụng và chúng ta cũng chẳng cần quan tâm tới các DB khác như MySQL hay PostgreSQL vậy những class để connect tới đó là dead class và GraalVM sẽ xóa nó ra khỏi ứng dụng nhờ vậy ứng dụng sẽ đỡ tốn tài nguyên bộ nhớ hơn.

GraalVM cung cấp hai tùy chọn là phiên bản Community Edition sử dụng miễn phí dưới giấy phép GPLv2 và phiên bản tính phí là Enterprise Edition.

Để cài đặt GraalVM Community Edition truy cập vào link: https://www.graalvm.org/downloads/ tìm phiên bản cho hệ điều hành của bạn và làm theo hướng dẫn ở https://www.graalvm.org/docs/getting-started/. Ở thời điểm bài viết này (2/2020) thì GraalVM mới release phiên bản 20.0.0 hỗ trợ phiên bản Java 11.

Sau khi cài đặt khi gõ java -version output sẽ hiện thị bộ compiler hiện tại đang là GraalVM như bên dưới.

Để sử dụng tính năng AOT ta cần phải cài đặt thêm bộ Native Image của Graal bằng câu lệnh gu install native-image

Ngoài ra Graal AOT yêu cầu hệ thống phải cài đặt các bộ thư viện glibc-devel, zlib-develgcc, ví dụ với Ubuntu ta sử dụng gói apt-get cài đặt hai gói zlib1g-devbuild-essential

Giờ hãy thử với một ứng dụng HelloWorld .java đơn giản sau

Sau đó thử compile và build sang native image:

Sau khi build sang native image ta sẽ có một file thực thi helloworld với Linux hoặc helloworld.exe với MS Windows. Khi chạy thử màn hình sẽ hiển thị output: "Hello, World!"

Với file thực thi helloworld tương đối nặng (3.5Mb) với chỉ tính năng cơ bản là in ra màn hình "Hello, World!" và thời gian build cũng khá lâu (11s), đó là nhược điểm của AOT để đánh đổi lại tốc độ thực thi và tiết kiệm tài nguyên bộ nhớ như đề cập bên trên.

Những hạn chế của GraalVM

Substrate VM một thành phần trong GraalVM để sử dụng tính năng ahead-of-time (AOT) cho ứng dụng Java, và nó có những hạn chế và không hỗ trợ các tính năng như sau:

1. Dynamic Class Loading / Unloading: Substrate VM không hỗ trợ Dynamic Class Loading, ví dụ như sau:

2. Java Native Interface (JNI): Substrate VM không hỗ trợ có thể gọi các hàm/chương trình viết bằng ngôn ngữ native (ngôn ngữ dành cho hệ thống như C/C++, Assembly) hoặc được gọi từ các chương trình native đó.

3. Unsafe Memory Access: Substrate VM không hỗ trợ sử dụng gói thư viện sun.misc.Unsafe để lấy địa chỉ đối tượng trong Java hoặc sử dụng làm Memory Barrier trong lập trình đa luồng.

4. InvokeDynamic Bytecode and Method Handles: Substrate VM không hỗ trợ tính năng InvokeDynamic hay Method Handles được sử dụng trong gói thư viện java.lang.invoke.* , tuy nhiên invokedynamic trong Lambda Expressions vẫn được hỗ trợ. Method Handles là một phương thức cấp thấp (low-level mechanism) dùng để gọi các method hay field từ một class bất kỳ nó cũng gần giống với Reflection nhưng có performance tốt hơn.
Ví dụ ta có thể sử dụng Method Handles để gọi tới method hay field của lớp Book.java

5. Finalizers: Trong lớp Object luôn có một method là finalize() , nó luôn được gọi khi Garbage Collector bắt đầu dọn dẹp các đối tượng. SubstrateVM sẽ không hỗ trợ finalize() , có nghĩa là mỗi khi GC dọn dẹp finalize() sẽ không bao giờ được gọi tới.

6. Serialization: Substrave VM không hỗ trợ sử dụng interface java.io.Serializable để chuyển một Object sang một mảng bytes chứa các thông tin của Class của Object, dùng để lưu trữ Object rồi chuyển đổi và truyền chúng đi thành các thành phần khác như file (json, xml, text) hay các table trong Database. Serialization thường được sử dụng trong các DTO (data transfer object) dùng để đọc hoặc ghi dữ liệu từ ứng dụng Java lên databse hay file (json, xml, text).
Chú ý rằng SubstraveVM cũng sẽ không hỗ trợ các thư viện sử dụng Serializable ví dụ như gói java.rmi

7. Security Manager: Substrave VM không hỗ trợ gói java.lang.SecurityManager để thực hiện các thao tác kiểm tra các kết nối với các resource thông qua mạng, như kiểm tra kết nối (checkConnect) hay kiểm tra quyền (checkPermission) trong chương trình. Mặc định trong ứng dụng Java - SecurityManager không được bật.

8. JVMTI, JMX, other native VM interfaces: Substrave VM không hỗ trợ sử dụng các Java Virtual Machine Tool Interface (JVM TI) bằng cách sử dụng các gói java.lang.instrument hay Java Management Extensions (JMX) bằng cách sử dụng JConsole để monitoring hay management hoạt động của các ứng dụng Java.

9. References: Substrave VM không hỗ trợ các kiểu tham chiếu trong gói java.lang.ref như weak, soft và phantom references. Ví dụ về một Weak Reference (tham chiếu yếu).

10. Reflection: Substrave VM có hỗ trợ sử dụng gói java.lang.reflect.* như Object.getClass() hay Object.getClass().getMethod()để gọi method hoặc chỉnh sửa các filed trong một class động, có nghĩ là việc quyết định method nào được gọi từ class nào sẽ quyết định ở thời điểm run time, nó cũng gần giống với Dynamic Class Loading vậy. Tuy nhiên để sử dụng Java Reflection ta phải cấu hình ra một file có format theo chuẩn JSON trong quá trình build navtive image với tham số:
-H:ReflectionConfigurationFiles=/path/to/reflectionFile

Ví dụ ta sẽ có một file cấu hình refection tự động tạo meta data cho toàn bộ các class method và fields trong cả project.

Hoặc ví dụ Java Reflection được ứng dụng vào việc chạy QuartJob động sau:

Và tao chỉ muốn tạo ra một file reflection cấu hình chỉ riêng cho trường hợp này cho các class Job là com.batnamv.job.JobInterruptHelloJob method là execute các tham số truyền vào cho method là java.util.concurrent.atomic.AtomicBooleanorg.quartz.JobExecutionContext , cấu hình sẽ như sau:

Nếu trong trường hợp trong class com.batnamv.job.JobInterruptHelloJob ta có một filed tên là label ta sẽ cấu hình thêm như sau:

Hoặc đơn giản hơn là sử dụng allPublicConstructors, allDeclaredConstructors, allPublicMethods, allDeclaredMethods, allPublicFields, allDeclaredFields, allPublicClassesallDeclaredClasses ta sẽ chỉ phải quan tâm liệt kê tới các Class muốn reflection và không cần liệt kê các method hay filed muốn reflection.

Xem thêm cách cấu hình cho Reflection: https://github.com/oracle/graal/blob/master/substratevm/REFLECTION.md

11. Dynamic Proxy: Tương tự như Java Reflection Substrave VM là có hỗ trợ Java Dynamic Proxy hay gọi đơn giản là proxy class: là một lớp implement nhiều Interface khác nhau, tuy nhiên khác với bình thường, đó là các việc class này được tạo ra ở thời điểm runtime của chương trình, có nghĩa là ở giai đoạn lập trình hay compile nó chưa được tạo ra. Nhưng cũng yêu cầu file cấu hình riêng trong quá trình build native image.
Dynamic class thường được sử dụng để Logging, khi một lớp implement nhiều Interface khác nhau và thực hiện Lazy Loading — Lazy loading của Framework Hibernate là một ví dụ điển hình.

Ví dụ một Proxy class dùng để logging khi gọi hay kết thúc một phương thức dùng cho mục đích debug:

Để cấu hình Dynamic Proxy ta sử dụng tham số -H:DynamicProxyConfigurationFiles=/patch/to/dynamicProxy.json khi build native image, ví dụ để cấu hình Dynamic Proxy ví dụ trên ta cấu hình như sau:

Xem thêm tại : https://github.com/oracle/graal/blob/master/substratevm/DYNAMIC_PROXY.md

Chúng ta có thể thấy GraalVM AOT có khá nhiều hạn chế, cho nên việc migrate các dự án cũ để build native image khá là khó khăn, nhưng các dự án Java micro-service mới ta có thể mạnh dạn sử dụng GraalVM native images được. Và gần đây có khá nhiều các Frame-work đã được phát triển để ta có thể phát triển các ứng dụng Java dựa trên nền tảng của GraalVM như Quarkus hay Micronaut.

Trong một bài viết tới tôi sẽ giới thiệu Quarkus một bộ framework giúp chúng ta phát triển các ứng dụng Java trên nền GraalVM nhanh chóng và tiện lợi.

Hết và hẹn gặp lại.

Nguồn tham khảo:
Devoxx Tech Talk — https://www.youtube.com/watch?v=iJBh2NoSCKM
GraalVM document: https://github.com/oracle/graal

--

--