上一篇文章講述了 Clojure 編譯器工做的總體流程,主要涉及 LispReader 與 Compiler 這兩個類,並且指出編譯器並無把 Clojure 轉爲相應的 Java 代碼,而是直接使用 ASM 生成可運行在 JVM 中的 bytecode。本文將主要討論 Clojure 編譯成的 bytecode 如何實現動態運行時以及爲何 Clojure 程序啓動慢,這會涉及到 JVM 的類加載機制。javascript
JVM 設計之初只是爲 Java 語言考慮,因此最基本的概念是 class,除了八種基本類型,其餘都是對象。Clojure 做爲一本函數式編程語言,最基本的概念是函數,沒有類的概念,那麼 Clojure 代碼生成以類爲主的 bytecode 呢?html
一種直觀的想法是,每一個命名空間(namespace)是一個類,命名空間裏的函數至關於類的成員函數。但仔細想一想會有以下問題:java
上述問題 2 就要求必須將函數編譯成一個類。根據 Clojure 官方文檔,對應關係是這樣的:git
<filename>__init
的加載類gen-class
生成固定名字的類,方便與 Java 交互defrecord
、deftype
生成同名的類,proxy
、reify
生成匿名的類須要明確一點,只有在 AOT 編譯時,Clojure 纔會在本地生成 .class
文件,其餘狀況下生成的類均在內存中。github
明確了 Clojure 類生成規則後,下面介紹 Clojure 是如何實現動態運行時。這一問題將分爲 AOT 編譯與 DynamicClassLoader 類的實現兩部分。編程
$ cat src/how_clojure_work/core.clj
(ns how-clojure-work.core)
(defn -main [& _]
(println "Hello, World!"))複製代碼
使用 lein compile
編譯這個文件,會在*compile-path*
指定的文件夾(通常是項目的target
)下生成以下文件:緩存
$ ls target/classes/how_clojure_work/
core$fn__38.class
core$loading__5569__auto____36.class
core$main.class
core__init.class複製代碼
core$main.class
與core__init.class
分別表示原文件的main
函數與命名空間加載類,那麼剩下兩個類是從那裏來的呢?微信
咱們知道 Clojure 裏面不少「函數」實際上是用宏實現的,宏在編譯時會進行展開,生成新代碼,上面代碼中的ns
、defn
都是宏,展開後(在 Cider + Emacs 開發環境下,C-c M-m
)可得oracle
(do
(in-ns 'how-clojure-work.core)
((fn*
loading__5569__auto__
([]
(. clojure.lang.Var
(clojure.core/pushThreadBindings
{clojure.lang.Compiler/LOADER
(. (. loading__5569__auto__ getClass) getClassLoader)}))
(try
(refer 'clojure.core)
(finally
(. clojure.lang.Var (clojure.core/popThreadBindings)))))))
(if (. 'how-clojure-work.core equals 'clojure.core)
nil
(do
(. clojure.lang.LockingTransaction
(clojure.core/runInTransaction
(fn*
([]
(commute
(deref #'clojure.core/*loaded-libs*)
conj
'how-clojure-work.core)))))
nil)))
(def main (fn* ([& _] (println "Hello, World!"))))複製代碼
能夠看到,ns
展開後的代碼裏面包含了兩個匿名函數,對應本地上剩餘的兩個文件。下面依次分析這四個class
文件jvm
$ javap core__init.class
public class how_clojure_work.core__init {
public static final clojure.lang.Var const__0;
public static final clojure.lang.AFn const__1;
public static final clojure.lang.AFn const__2;
public static final clojure.lang.Var const__3;
public static final clojure.lang.AFn const__11;
public static void load();
public static void __init0();
public static {};
}複製代碼
能夠看到,命名空間加載類裏面有一些Var
與AFn
變量,能夠認爲一個Var
對應一個AFn
。使用 Intellj 或 JD 打開這個類文件,首先查看靜態代碼快
static {
__init0();
Compiler.pushNSandLoader(RT.classForName("how_clojure_work.core__init").getClassLoader());
try {
load();
} catch (Throwable var1) {
Var.popThreadBindings();
throw var1;
}
Var.popThreadBindings();
}複製代碼
這裏面會先調用__init0
:
public static void __init0() {
const__0 = (Var)RT.var("clojure.core", "in-ns");
const__1 = (AFn)Symbol.intern((String)null, "how-clojure-work.core");
const__2 = (AFn)Symbol.intern((String)null, "clojure.core");
const__3 = (Var)RT.var("how-clojure-work.core", "main");
const__11 = (AFn)RT.map(new Object[] {
RT.keyword((String)null, "arglists"), PersistentList.create(Arrays.asList(new Object[] {
Tuple.create(Symbol.intern((String)null, "&"),
Symbol.intern((String)null, "_"))
})),
RT.keyword((String)null, "line"), Integer.valueOf(3),
RT.keyword((String)null, "column"), Integer.valueOf(1),
RT.keyword((String)null, "file"), "how_clojure_work/core.clj"
});
}複製代碼
RT
是 Clojure runtime 的實現,在__init0
裏面會對命名空間裏面出現的 var 進行賦值。
接下來是pushNSandLoader
(內部用pushThreadBindings
實現),它與後面的 popThreadBindings
造成一個 binding,功能等價下面的代碼:
(binding [clojure.core/*ns* nil clojure.core/*fn-loader* RT.classForName("how_clojure_work.core__init").getClassLoader() clojure.core/*read-eval true] (load))複製代碼
接着查看load
的實現:
public static void load() {
// 調用 in-ns,傳入參數 how-clojure-work.core
((IFn)const__0.getRawRoot()).invoke(const__1);
// 執行 loading__5569__auto____36,功能等價於 (refer clojure.core)
((IFn)(new loading__5569__auto____36())).invoke();
Object var10002;
// 若是當前的命名空間不是 clojure.core 那麼會在一個 LockingTransaction 裏執行 fn__38
// 功能等價與(commute (deref #'clojure.core/*loaded-libs*) conj 'how-clojure-work.core)
if(((Symbol)const__1).equals(const__2)) {
var10002 = null;
} else {
LockingTransaction.runInTransaction((Callable)(new fn__38()));
var10002 = null;
}
Var var10003 = const__3;
// 爲 main 設置元信息,包括行號、列號等
const__3.setMeta((IPersistentMap)const__11);
var10003.bindRoot(new main());
}複製代碼
至此,命名空間加載類就分析完了。
$ javap core\$loading__5569__auto____36.class
Compiled from "core.clj"
public final class how_clojure_work.core$loading__5569__auto____36 extends clojure.lang.AFunction {
public static final clojure.lang.Var const__0;
public static final clojure.lang.AFn const__1;
public how_clojure_work.core$loading__5569__auto____36(); // 構造函數
public java.lang.Object invoke();
public static {};
}複製代碼
與 core__init
類結構,包含一些 var 賦值與初始化函數,同時它還繼承了AFunction
,從名字就能夠看出這是一個函數的實現。
// 首先是 var 賦值
public static final Var const__0 = (Var)RT.var("clojure.core", "refer");
public static final AFn const__1 = (AFn)Symbol.intern((String)null, "clojure.core");
// invoke 是方法調用時的入口函數
public Object invoke() {
Var.pushThreadBindings((Associative)RT.mapUniqueKeys(new Object[]{Compiler.LOADER, ((Class)this.getClass()).getClassLoader()}));
Object var1;
try {
var1 = ((IFn)const__0.getRawRoot()).invoke(const__1);
} finally {
Var.popThreadBindings();
}
return var1;
}複製代碼
上面的invoke
方法等價於
(binding [Compiler.LOADER (Class)this.getClass()).getClassLoader()]
(refer 'clojure.core))複製代碼
fn__38
與loading__5569__auto____36
相似, 這裏不在贅述。
$ javap core\$main.class
Compiled from "core.clj"
public final class how_clojure_work.core$main extends clojure.lang.RestFn {
public static final clojure.lang.Var const__0;
public how_clojure_work.core$main();
public static java.lang.Object invokeStatic(clojure.lang.ISeq);
public java.lang.Object doInvoke(java.lang.Object);
public int getRequiredArity();
public static {};
}複製代碼
因爲main
函數的參數數量是可變的,因此它繼承了RestFn
,除了 var 賦值外,重要的是如下兩個函數:
public static Object invokeStatic(ISeq _) {
// const__0 = (Var)RT.var("clojure.core", "println");
return ((IFn)const__0.getRawRoot()).invoke("Hello, World!");
}
public Object doInvoke(Object var1) {
ISeq var10000 = (ISeq)var1;
var1 = null;
return invokeStatic(var10000);
}複製代碼
經過上面的分析,咱們能夠發現,每一個函數在被調用時,會去調用getRawRoot
函數獲得該函數的實現,這種重定向是 Clojure 實現動態運行時很是重要一措施。這種重定向在開發時很是方便,能夠用 nrepl 鏈接到正在運行的 Clojure 程序,動態修改程序的行爲,無需重啓。
可是在正式的生產環境,這種重定向對性能有影響,並且也沒有重複定義函數的必要,因此能夠在服務啓動時指定-Dclojure.compiler.direct-linking=true
來避免這類重定向,官方稱爲 Direct linking。能夠在定義 var 時指定^:redef
表示必須重定向。^:dynamic
的 var 永遠採用重定向的方式肯定最終值。
須要注意的是,var 重定義對那些已經 direct linking 的代碼是透明的。
熟悉 JVM 類加載機制(不清楚的推薦我另外一篇文章《JVM 的類初始化機制》)的都會知道,
一個類只會被一個 ClassLoader 加載一次。
僅僅有上面介紹的重定向機制是沒法實現動態運行時的,還須要一個靈活的 ClassLoader,能夠在 REPL 作以下實驗:
user> (defn foo [] 1)
#'user/foo
user> (.. foo getClass getClassLoader)
#object[clojure.lang.DynamicClassLoader 0x72d256 "clojure.lang.DynamicClassLoader@72d256"]
user> (defn foo [] 1)
#'user/foo
user> (.. foo getClass getClassLoader)
#object[clojure.lang.DynamicClassLoader 0x57e2068e "clojure.lang.DynamicClassLoader@57e2068e"]複製代碼
能夠看到,只要對一個函數進行了重定義,與之相關的 ClassLoader 隨之也改變了。下面來看看 DynamicClassLoader 的核心實現:
// 用於存放已經加載的類
static ConcurrentHashMap<String, Reference<Class>>classCache =
new ConcurrentHashMap<String, Reference<Class> >();
// loadClass 會在一個類第一次主動使用時被 JVM 調用
Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class c = findLoadedClass(name);
if (c == null) {
c = findInMemoryClass(name);
if (c == null)
c = super.loadClass(name, false);
}
if (resolve)
resolveClass(c);
return c;
}
// 用戶能夠調用 defineClass 來動態生成類
// 每次調用時會先清空緩存裏已加載的類
public Class defineClass(String name, byte[] bytes, Object srcForm){
Util.clearCache(rq, classCache);
Class c = defineClass(name, bytes, 0, bytes.length);
classCache.put(name, new SoftReference(c,rq));
return c;
}複製代碼
經過搜索 Clojure 源碼,只有在 RT.java 的 makeClassLoader 函數 裏面有new DynamicClassLoader
語句,繼續經過 Intellj 的 Find Usages 發現有以下三處調用makeClassLoader
:Compiler/compile1、Compiler/eval、Compiler/load。
正如上一篇文章的介紹,這三個方法正是 Compiler 的入口函數,這也就解釋了上面 REPL 中的實驗:
每次重定義一個函數,都會生成一個新 DynamicClassLoader 實例去加載其實現。
明白了 Clojure 是如何實現動態運行時,下面分析 Clojure 程序爲何啓動慢。
首先須要明確一點,JVM 並不慢,咱們能夠將以前的 Hello World 打成 uberjar,運行測試下時間。
;; (:gen-class) 指令可以生成與命名空間同名的類
(ns how-clojure-work.core
(:gen-class))
(defn -main [& _]
(println "Hello, World!"))
# 爲了能用 java -jar 方式運行,須要在 project.clj 中添加
# :main how-clojure-work.core
$ lein uberjar
$ time java -jar target/how-clojure-work-0.1.0-SNAPSHOT-standalone.jar
Hello, World!
real 0m0.900s
user 0m1.422s
sys 0m0.087s複製代碼
在啓動時加入-verbose:class
參數,能夠看到不少 clojure.core 開頭的類
...
[Loaded clojure.core$cond__GT__GT_ from file:/Users/liujiacai/codes/clojure/how-clojure-work/target/how-clojure-work-0.1.0-SNAPSHOT-standalone.jar]
[Loaded clojure.core$as__GT_ from file:/Users/liujiacai/codes/clojure/how-clojure-work/target/how-clojure-work-0.1.0-SNAPSHOT-standalone.jar]
[Loaded clojure.core$some__GT_ from file:/Users/liujiacai/codes/clojure/how-clojure-work/target/how-clojure-work-0.1.0-SNAPSHOT-standalone.jar]
[Loaded clojure.core$some__GT__GT_ from file:/Users/liujiacai/codes/clojure/how-clojure-work/target/how-clojure-work-0.1.0-SNAPSHOT-standalone.jar]
...複製代碼
把生成的 uberjar 解壓打開,能夠發現 clojure.core 裏面的函數都在,這些函數在程序啓動時都會被加載。
這就是 Clojure 啓動慢的緣由:加載大量用不到的類。
Clojure 做爲一門 host 在 JVM 上的語言,其獨特的實現方式讓其擁動態的運行時的同時,方便與 Java 進行交互。固然,Clojure 還有不少能夠提升的地方,好比上面的慢啓動問題。另外,JVM 7 中增長了 invokedynamic 指令,可讓運行在 JVM 上的動態語言經過實現一個 CallSite (能夠認爲是函數調用)的 MethodHandle
函數來幫助編譯器找到正確的實現,這無異會提高程序的執行速度。
PS: 微信公衆號,頭條,掘金等平臺均有我文章的分享,但個人文章會隨着我理解的加深不按期更新,建議你們最好去個人博客 liujiacai.net 閱讀最新版。