A. 編譯器的工作分為哪幾個階段
編譯器就是一個普通程序,沒什麼大不了的
什麼是編譯器?
編譯器是一個將高級語言翻譯為低級語言的程序。
首先我們一定要意識到編譯器就是一個普通程序,沒什麼大不了的。
在沒有弄明白編譯器如何工作之前你可以簡單的把編譯器當做一個黑盒子,其作用就是輸入一個文本文件輸出一個二進制文件。
基本上編譯器經過了以下幾個階段,等等,這句話教科書上也有,但是我相信很多同學其實並沒有真正理解這幾個步驟到底在說些什麼,為了讓你徹底理解這幾個步驟,我們用一個簡單的例子來講解。
假定我們有一段程序:
while (y < z) {
int x = a + b;
y += x;
}
那麼編譯器是怎樣把這一段程序人類認識的程序轉換為CPU認識的二進制機器指令呢?
提取出每一個單詞:詞法分析
首先編譯器要把源代碼中的每個「單詞」提取出來,在編譯技術中「單詞」被稱為token。其實不只是每個單詞被稱為一個token,除去單詞之外的比如左括弧、右括弧、賦值操作符等都被稱為token。
從源代碼中提取出token的過程就被稱為詞法分析,Lexical Analysis。
經過一遍詞法分析,編譯器得到了以下token:
T_While while
T_LeftParen (
T_Identifier y
T_Less <
T_Identifier z
T_RightParen )
T_OpenBrace {
T_Int int
T_Identifier x
T_Assign =
T_Identifier a
T_Plus +
T_Identifier b
T_Semicolon ;
T_Identifier y
T_PlusAssign +=
T_Identifier x
T_Semicolon ;
T_CloseBrace }
就這樣一個磁碟中保存的字元串源代碼文件就轉換為了一個個的token。
這些token想表達什麼意思:語法分析
有了這些token之後編譯器就可以根據語言定義的語法恢復其原本的結構,怎麼恢復呢?
原來,編譯器在掃描出各個token後根據規則將其用樹的形式表示出來,這顆樹就被稱為語法樹。
語法樹是不是合理的:語義分析
有了語法樹後我們還要檢查這棵樹是不是合法的,比如我們不能把一個整數和一個字元串相加、比較符左右兩邊的數據類型要相同,等等。
這一步通過後就證明了程序合法,不會有編譯錯誤。
B. java 異常at org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:20)
先了解SOF的生成原因,以下為轉載,可以自己網路。
Hotspot虛擬機並不區分VM棧和本地方法棧,因此-Xoss參數實際上是無效的,棧容量只由-Xss參數設定。關於VM棧和本地方法棧在VM Spec描述了兩種異常:StackOverflowError與OutOfMemoryError,當棧空間無法繼續分配分配時,到底是內存太小還是棧太大其實某種意義上是對同一件事情的兩種描述而已,在筆者的實驗中,對於單線程應用嘗試下面3種方法均無法讓虛擬機產生OOM,全部嘗試結果都是獲得SOF異常。
1.使用-Xss參數削減棧內存容量。結果:拋出SOF異常時的堆棧深度相應縮小。
2.定義大量的本地變數,增大此方法對應幀的長度。結果:拋出SOF異常時的堆棧深度相應縮小。
3.創建幾個定義很多本地變數的復雜對象,打開逃逸分析和標量替換選項,使得JIT編譯器允許對象拆分後在棧中分配。結果:實際效果同第二點。
VM棧和本地方法棧OOM測試(僅作為第1點測試程序)
/**
*VMArgs:-Xss128k
*@authorzzm
*/
publicclassJavaVMStackSOF{
privateintstackLength=1;
publicvoidstackLeak(){
stackLength++;
stackLeak();
}
publicstaticvoidmain(String[]args)throwsThrowable{
JavaVMStackSOFoom=newJavaVMStackSOF();
try{
oom.stackLeak();
}catch(Throwablee){
System.out.println("stacklength:"+oom.stackLength);
throwe;
}
}
}
運行結果:
stacklength:2402Exceptioninthread"main"java.lang.StackOverflowErroratorg.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:20)atorg.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)atorg.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21)這個應該和你遇到的問題一樣,看看是不是存在以上3點的問題。
C. Java中有類似於NGen的工具嗎
Java世界裡有很多AOT編譯的解決方案,雖然Oracle/Sun JDK到JDK8為止都還沒有提供這樣的功能。
先來幾個傳送門:
HotSpot VM JIT的編譯產出,理論上能否被復用? - RednaxelaFX 的回答
逃逸分析為何不能在編譯期進行? - RednaxelaFX 的回答
為什麼Java不能由JVM產生針對特定操作系統的機器碼從而提高效率? - RednaxelaFX 的回答
請教:對Java類庫jar文件,有什麼好的防止反編譯辦法,最好是加密/解密方案,而不是代碼混淆方案。 - RednaxelaFX 的回答
如何將Java打包成exe文件在沒有JRE環境的電腦上執行? - RednaxelaFX 的回答
各個操作系統下的 JVM 是誰開發出來的? - RednaxelaFX 的回答
ios設備如何java編譯? - RednaxelaFX 的回答
java現在能直接編譯成機器碼? - Java
在深入到具體實現前,要先強調一點:
AOT編譯(Ahead-of-Time Compilation)不但涉及一個編譯器,還要涉及配套的運行時支持系統(runtime system)。兩者通常是緊密耦合的。
換
句話說,一個AOT編譯器只能跟自己的runtime搭配使用,這個runtime可以是一個完整的VM(如NGen與CLR的搭配),也可以是一個比較
小的runtime(如.NET Native里的MRT(Minimal Runtime),只提供基礎的GC、多線程支持等功能)。
所以不要想像說下面列舉的工具能夠對Java做AOT編譯,然後運行時還搭配Oracle JDK / OpenJDK來使用。
來看看一些具體實現。
IBM JDK6+ - Enhance performance with class sharing
IBM JDK是主流JDK之一,並且提供了AOT編譯的功能。
這個AOT編譯的主要目的是提高啟動速度,以及在多個進程之間共享AOT編譯出來的機器碼。
被AOT編譯的代碼在運行時還可以再次被JIT編譯,這樣既能提高啟動速度,又不會影響最高速度(peak performance)。
這個AOT編譯用的編譯器就是IBM J9 VM里的JIT編譯器,只是讓它以AOT模式來工作。這點跟NGen有點類似(NGen也是直接用CLR里的JIT編譯器來生成native image)。跟它搭配使用的runtime自然就是完整的J9 VM。
不
過跟NGen不同的是,NGen是真的在程序執行前就做了編譯,而IBM
J9提供的AOT編譯其實是在用戶第一次運行程序時把特殊模式的JIT編譯生成的代碼緩存到磁碟上,後續執行的時候就可以直接使用該緩存里的機器碼。所以
IBM把這個功能叫做「dynamic AOT」。
Excelsior JET
一個比較成熟的商業的Java AOT編譯解決方案。僅支持x86上的若干操作系統。
這個AOT編譯器是自己寫的一個私有的編譯器,其搭配使用的runtime也是私有的。它支持幾種不同的編譯模式,搭配使用的runtime可以完全不帶解釋器/JIT編譯器,只帶有GC、線程支持等功能,也可以帶有更完整的JVM功能。
現在可能很多人都知道Android的ART,而對Java世界裡的老前輩們沒啥認知。其實ART的AOT編譯與解釋器/JIT混合的方式,跟Excelsior JET(以及下面提到的GCJ)是相當相似的。
GCJ
一個開源的Java運行時系統,支持AOT編譯、解釋執行與JIT編譯。GCJ是GCC的一部分。在OpenJDK流行起來之前,通常各種Linux發行版帶的Java實現會是GCJ。
RoboVM
一個讓Java程序可以運行在iOS上的開源解決方案。
iOS不允許第三方程序做運行時代碼生成(也就是不允許JIT編譯),所以在iOS上運行程序要麼得AOT編譯,要麼只能解釋執行。Oracle ADF選擇使用一個只能解釋執行的JVM來支持Java程序,而RoboVM選擇使用AOT編譯。
RoboVM的AOT編譯器藉助了不少現成的框架來實現。其中最重要的兩個是Soot與LLVM,前者解決編譯器前端、後者解決編譯器後端,RoboVM自己只要解決一些跟runtime搭配的地方就好了。
RoboVM配套的runtime是自己寫的一個比較小的runtime。
VMKit
VMKit是一個基於許多現成的庫組合起來實現的VM,主要可以用作JVM,也可配置為一個CLI。
VMKit支持AOT編譯。它的JIT與AOT編譯器都是基於LLVM實現的。不過實現得比較粗糙嗯。
Avian
Avian不是一個完整的JVM,只支持Java的一個比較有用的子集。很多時候也夠用了。
它可以支持AOT編譯。
ART (Android Runtime)
ART和Dalvik VM雖然不直接實現Java位元組碼,但從整個系統的角度看它們倆都是不折不扣的Java系統。
ART支持AOT編譯與解釋執行。Java程序的啟動速度在ART上是比在Dalvik VM上快多了。
只想強調一點:ART的AOT編譯並不依賴LLVM。詳情請參考另外幾個回答:
Android 中的 LLVM 主要做什麼? - RednaxelaFX 的回答
如何看待微軟新出的LLILC,一個新的基於LLVM的CoreCLR JIT/AOT編譯器? - RednaxelaFX 的回答
Jikes RVM、Maxine VM、Joeq
這三個是元循環Java虛擬機的代表。關於元循環虛擬機(metacircular VM),請參考另一個回答:用 JavaScript 寫成的 JavaScript 解釋器,意義是什麼? - RednaxelaFX 的回答
它們都是用純Java實現的Java虛擬機,而且都能獨立運行,也就是說可以自舉(bootstrap)。要實現bootstrap,它們就必然需要能支持AOT編譯的編譯器。
所以說在這類實現里,AOT編譯不是為了提高啟動速度,而是為了實現bootstrap的根本需求。有趣的是,它們可以(在一定范圍內)支持定製boot image的內容,也就是說可以讓Java應用程序與JVM一起AOT編譯構成boot image。
JNode - Java New Operating System Design Effort
JNode是一個用純Java實現的操作系統。這比上面三個元循環Java虛擬機還要更進一步,可以在裸硬體上bootstrap。自然,它也需要一個支持AOT編譯的編譯器,同樣是出於實現的根本需求。
Oracle Labs: Substrate VM
Substrate VM是Oracle Labs的一個研究項目,跟Graal編譯器與Truffle框架搭配使用。它實現了一個很小型的、可定製的runtime,可以讓基於Truffle實現的編程語言可以脫離標准JVM獨立運行。
這里,Substrate VM提供的是runtime的功能,真正的AOT編譯器是Graal。
Oracle/Sun JDK
其
實Sun以前在JDK6時期研究過實現AOT編譯,但是當時選擇的實現方式比較取巧。後來發現效果並不理想,而且在有了多層編譯系統(tiered
compilation system)之後這個AOT編譯的原型實現在啟動速度上根本沒有優勢,就把這個項目擱置了。具體細節抱歉我無法多說。
但是Oracle JDK在計劃提供新的AOT編譯支持。或許會在未來版本的Oracle JDK里出現。請拭目以待。
目前Oracle在公開場合介紹這個AOT編譯器的主要資料是JVMLS 2015上的一個演講,Java Goes AOT - JVMLS 2015 (打不開請自備工具…),有興趣的同學可以參考下。它是一個基於Graal編譯器的實現。
IKVM.NET
前面說的AOT編譯解決方案都是把Java(包含Java位元組碼的Class文件)編譯到native code。http://IKVM.NET是一種比較特殊的方案,把Java Class文件編譯到.NET Assembly,然後可以用任意CLI(Common Language Infrastructure)實現上運行,例如微軟的CLR和Xamarin的Mono。
藉助CLR的NGen和Mono的AOT編譯,http://IKVM.NET生成的.NET Assembly還可以進一步被編譯為native code。這樣也算間接達到了AOT編譯的目的。
這不是拿來搞笑的。http://IKVM.NET的作者做過一個演示,在Windows上基於IKVM+NGen來運行Eclipse,啟動速度比當時的Oracle JDK6快得多…
跟http://IKVM.NET類似的項目以前還有幾個,例如Ja.NET(官網掛了,介紹可以看InfoQ的新聞稿)。但活到現在的恐怕就IKVM.NET一家了。
D. Go 語言內存管理(三):逃逸分析
Go 語言較之 C 語言一個很大的優勢就是自帶 GC 功能,可 GC 並不是沒有代價的。寫 C 語言的時候,在一個函數內聲明的變數,在函數退出後會自動釋放掉,因為這些變數分配在棧上。如果你期望變數的數據可以在函數退出後仍然能被訪問,就需要調用 malloc 方法在堆上申請內存,如果程序不再需要這塊內存了,再調用 free 方法釋放掉。Go 語言不需要你主動調用 malloc 來分配堆空間,編譯器會自動分析,找出需要 malloc 的變數,使用堆內存。編譯器的這個分析過程就叫做逃逸分析。
所以你在一個函數中通過 dict := make(map[string]int) 創建一個 map 變數,其背後的數據是放在棧空間上還是堆空間上,是不一定的。這要看編譯器分析的結果。
可逃逸分析並不是百分百准確的,它有缺陷。有的時候你會發現有些變數其實在棧空間上分配完全沒問題的,但編譯後程序還是把這些數據放在了堆上。如果你了解 Go 語言編譯器逃逸分析的機制,在寫代碼的時候就可以有意識地繞開這些缺陷,使你的程序更高效。
Go 語言雖然在內存管理方面降低了編程門檻,即使你不了解堆棧也能正常開發,但如果你要在性能上較真的話,還是要掌握這些基礎知識。
這里不對堆內存和棧內存的區別做太多闡述。簡單來說就是, 棧分配廉價,堆分配昂貴。 棧空間會隨著一個函數的結束自動釋放,堆空間需要時間 GC 模塊不斷地跟蹤掃描回收。如果對這兩個概念有些迷糊,建議閱讀下面 2 個文章:
這里舉一個小例子,來對比下堆棧的差別:
stack 函數中的變數 i 在函數退出會自動釋放;而 heap 函數返回的是對變數 i 的引用,也就是說 heap() 退出後,表示變數 i 還要能被訪問,它會自動被分配到堆空間上。
他們編譯出來的代碼如下:
邏輯的復雜度不言而喻,從上面的匯編中可看到, heap() 函數調用了 runtime.newobject() 方法,它會調用 mallocgc 方法從 mcache 上申請內存,申請的內部邏輯前面文章已經講述過。堆內存分配不僅分配上邏輯比棧空間分配復雜,它最致命的是會帶來很大的管理成本,Go 語言要消耗很多的計算資源對其進行標記回收(也就是 GC 成本)。
Go 編輯器會自動幫我們找出需要進行動態分配的變數,它是在編譯時追蹤一個變數的生命周期,如果能確認一個數據只在函數空間內訪問,不會被外部使用,則使用棧空間,否則就要使用堆空間。
我們在 go build 編譯代碼時,可使用 -gcflags '-m' 參數來查看逃逸分析日誌。
以上面的兩個函數為例,編譯的日誌輸出是:
日誌中的 &i escapes to heap 表示該變數數據逃逸到了堆上。
需要使用堆空間,所以逃逸,這沒什麼可爭議的。但編譯器有時會將 不需要 使用堆空間的變數,也逃逸掉。這里是容易出現性能問題的大坑。網上有很多相關文章,列舉了一些導致逃逸情況,其實總結起來就一句話:
多級間接賦值容易導致逃逸 。
這里的多級間接指的是,對某個引用類對象中的引用類成員進行賦值。Go 語言中的引用類數據類型有 func , interface , slice , map , chan , *Type(指針) 。
記住公式 Data.Field = Value ,如果 Data , Field 都是引用類的數據類型,則會導致 Value 逃逸。這里的等號 = 不單單只賦值,也表示參數傳遞。
根據公式,我們假設一個變數 data 是以下幾種類型,相應的可以得出結論:
下面給出一些實際的例子:
如果變數值是一個函數,函數的參數又是引用類型,則傳遞給它的參數都會逃逸。
上例中 te 的類型是 func(*int) ,屬於引用類型,參數 *int 也是引用類型,則調用 te(&j) 形成了為 te 的參數(成員) *int 賦值的現象,即 te.i = &j 會導致逃逸。代碼中其他幾種調用都沒有形成 多級間接賦值 情況。
同理,如果函數的參數類型是 slice , map 或 interface{} 都會導致參數逃逸。
匿名函數的調用也是一樣的,它本質上也是一個函數變數。有興趣的可以自己測試一下。
只要使用了 Interface 類型(不是 interafce{} ),那麼賦值給它的變數一定會逃逸。因為 interfaceVariable.Method() 先是間接的定位到它的實際值,再調用實際值的同名方法,執行時實際值作為參數傳遞給方法。相當於 interfaceVariable.Method.this = realValue
向 channel 中發送數據,本質上就是為 channel 內部的成員賦值,就像給一個 slice 中的某一項賦值一樣。所以 chan *Type , chan map[Type]Type , chan []Type , chan interface{} 類型都會導致發送到 channel 中的數據逃逸。
這本來也是情理之中的,發送給 channel 的數據是要與其他函數分享的,為了保證發送過去的指針依然可用,只能使用堆分配。
可變參數如 func(arg ...string) 實際與 func(arg []string) 是一樣的,會增加一層訪問路徑。這也是 fmt.Sprintf 總是會使參數逃逸的原因。
例子非常多,這里不能一一列舉,我們只需要記住分析方法就好,即,2 級或更多級的訪問賦值會 容易 導致數據逃逸。這里加上 容易 二字是因為隨著語言的發展,相信這些問題會被慢慢解決,但現階段,這個可以作為我們分析逃逸現象的依據。
下面代碼中包含 2 種很常規的寫法,但他們卻有著很大的性能差距,建議自己想下為什麼。
Benchmark 和 pprof 給出的結果:
熟悉堆棧概念可以讓我們更容易看透 Go 程序的性能問題,並進行優化。
多級間接賦值會導致 Go 編譯器出現不必要的逃逸,在一些情況下可能我們只需要修改一下數據結構就會使性能有大幅提升。這也是很多人不推薦在 Go 中使用指針的原因,因為它會增加一級訪問路徑,而 map , slice , interface{} 等類型是不可避免要用到的,為了減少不必要的逃逸,只能拿指針開刀了。
大多數情況下,性能優化都會為程序帶來一定的復雜度。建議實際項目中還是怎麼方便怎麼寫,功能完成後通過性能分析找到瓶頸所在,再對局部進行優化。
E. Go切片數組深度解析
Go 中的分片數組,實際上有點類似於Java中的ArrayList,是一個可以擴展的數組,但是Go中的切片由比較靈活,它和數組很像,也是基於數組,所以在了解Go切片前我們先了解下數組。
數組簡單描述就由相同類型元素組成的數據結構, 在創建初期就確定了長度,是不可變的。
但是Go的數組類型又和C與Java的數組類型不一樣, NewArray 用於創建一個數組,從源碼中可以看出最後返回的是 &Array{}的指針,並不是第一個元素的指針,在Go中數組屬於值類型,在進行傳遞時,採取的是值傳遞,通過拷貝整個數組。Go語言的數組是一種有序的struct。
Go 語言的數組有兩種不同的創建方式,一種是顯示的初始化,一種是隱式的初始化。
注意一定是使用 [...]T 進行創建,使用三個點的隱式創建,編譯器會對數組的大小進行推導,只是Go提供的一種語法糖。
其次,Go中數組的類型,是由數值類型和長度兩個一起確定的。[2]int 和 [3]int 不是同一個類型,不能進行傳參和比較,把數組理解為類型和長度兩個屬性的結構體,其實就一目瞭然了。
Go中的數組屬於值類型,通常應該存儲於棧中,局部變數依然會根據逃逸分析確定存儲棧還是堆中。
編譯器對數組函數中做兩種不同的優化:
在靜態區完成賦值後復制到棧中。
總結起來,在不考慮逃逸分析的情況下,如果數組中元素的個數小於或者等於 4 個,那麼所有的變數會直接在棧上初始化,如果數組元素大於 4 個,變數就會在靜態存儲區初始化然後拷貝到棧上。
由於數組是值類型,那麼賦值和函數傳參操作都會復制整個數組數據。
不管是賦值或函數傳參,地址都不一致,發生了拷貝。如果數組的數據較大,則會消耗掉大量內存。那麼為了減少拷貝我們可以主動的傳遞指針呀。
地址是一樣的,不過傳指針會有一個弊端,從列印結果可以看到,指針地址都是同一個,萬一原數組的指針指向更改了,那麼函數裡面的指針指向都會跟著更改。
同樣的我們將數組轉換為切片,通過傳遞切片,地址是不一樣的,數組值相同。
切片是引用傳遞,所以它們不需要使用額外的內存並且比使用數組更有效率。
所以,切片屬於引用類型。
通過這種方式可以將數組轉換為切片。
中間不加三個點就是切片,使用這種方式創建切片,實際上是先創建數組,然後再通過第一種方式創建。
使用make創建切片,就不光編譯期了,make創建切片會涉及到運行期。1. 切片的大小和容量是否足夠小;
切片是否發生了逃逸,最終在堆上初始化。如果切片小的話會先在棧或靜態區進行創建。
切片有一個數組的指針,len是指切片的長度, cap指的是切片的容量。
cap是在初始化切片是生成的容量。
發現切片的結構體是數組的地址指針array unsafe.Pointer,而Go中數組的地址代表數組結構體的地址。
slice 中得到一塊內存地址,&array[0]或者unsafe.Pointer(&array[0])。
也可以通過地址構造切片
nil切片:指的unsafe.Pointer 為nil
空切片:
創建的指針不為空,len和cap為空
當一個切片的容量滿了,就需要擴容了。怎麼擴,策略是什麼?
如果原來數組切片的容量已經達到了最大值,再想擴容, Go 默認會先開一片內存區域,把原來的值拷貝過來,然後再執行 append() 操作。這種情況對現數組的地址和原數組地址不相同。
從上面結果我們可以看到,如果用 range 的方式去遍歷一個切片,拿到的 Value 其實是切片裡面的值拷貝,即淺拷貝。所以每次列印 Value 的地址都不變。
由於 Value 是值拷貝的,並非引用傳遞,所以直接改 Value 是達不到更改原切片值的目的的,需要通過 &slice[index] 獲取真實的地址。
F. 反射的性能開銷都在哪
1.反射調用過程中會產生大量的臨時對象,這些對象會佔用內存,可能會導致頻繁 gc,從而影響性能。
2.反射調用方法時會從方法數組中遍歷查找,並且會檢查可見性等操作會耗時。
3.反射在達到一定次數時,會動態編寫位元組碼並載入到內存中,這個位元組碼沒有經過編譯器優化,也不能享受JIT優化。
4.Method#invoke 方法會對參數做封裝和解封操作,會涉及自動裝箱/拆箱和類型轉換,都會帶來一定的資源開銷。
5.反射方法難以內聯
6.需要檢查方法可見性和校驗參數
invoke 方法的參數是一個可變長參數,也就是構建一個 Object 數組存參數,這也同時帶來了基本數據類型的裝箱操作,在 invoke 內部會進行運行時許可權檢查,這也是一個損耗點。普通方法調用可能有一系列優化手段,比如方法內聯、逃逸分析,而這又是反射調用所不能做的,性能差距再一次被放大。
優化反射調用,可以盡量避免反射調用虛方法、關閉運行時許可權檢查、可能需要增大基本數據類型對應的包裝類緩存、如果調用次數可知可以關閉 Inflation 機制,以及增加內聯緩存記錄的類型數目。
G. 菜鳥:剛學java,堆區,棧區,靜態區,代碼區,暈了!!!!!
你問題太多了。簡單為你解答一下吧,JAVA語言的內存管理分為棧內存,堆內存和方法區,棧內存用來存儲基本數據類型和對象的引用(對象的實體和引用這兩個概念你要搞明白),堆內存用來存儲對象的實體。。你記住,JAVA是一門面向對象的語言,在JAVA理萬事萬物都是對象,除了兩個東西:1,8個基本數據類型(對應的,還有8個相關的包裝類,但是為了JAVA運行速度的考慮,SUN公司保留了這8個基本數據類型);2,就是你所謂的入口方法,即main方法;這兩點是JAVA不是純粹的面向對象語言的表現,也就是他比較特殊的地方,你記住就行了;接下來,我們來看棧內存和堆內存,JAVA裡面所有東西都是對象,那麼對象保存在哪呢?其實,對象里的所有東西保存在堆內存里,裡麵包括了這個對象的成員變數和方法等東西,而棧內存里,保存的是這個對象所屬的這塊堆內存的首地址?也就是一個16進制的數字,明白了?因為你要告訴JAVA虛擬機從哪裡去開始讀取這塊堆內存啊。所以,你明白棧內存用來存儲基本數據類型和對象的引用,堆內存用來存儲對象的實體了。。而內存管理裡面還有一塊叫方法區,這是JAVA虛擬機用來在執行一個JAVA程序之前保存這個程序的結構等級的地方,虛擬機按照這個結構等級來調用程序里德對象方法等,而靜態變數和靜態方法正是保存在方法區里,所以靜態方法可以在不創建對象的時候就調用,因為創建對象就是為對象分配堆內存,只有創建了對象之後才能調用對象的非靜態方法和非靜態變數。。你的第一個問題就能解答了,這個情況就是zhangsan的堆內存里保存的car對象的引用,而這個引用又指向car對象的堆內存;對象的成員變數是保存在自己的堆內存里的;而入口類是一個特殊的東西,你特殊對待就行了。
H. 英語Max Non Heap Memory怎麼翻譯
為什麼要學習JVM?
深入理解JVM可以幫助我們從平台角度提高解決問題的能力,例如,有效防止內存泄漏(Memory leak),優化線程鎖的使用 (Thread Lock),更加高效的進行垃圾回收 (Garbage collection),提高系統吞吐量 (throughput),降低延遲(Delay),提高其性能(performance)等。
你是如何理解JVM的?
JVM 是 Java Virtual Machine的縮寫,顧名思義,它是一個虛擬計算機,是硬體計算機的抽象(虛構)實現,是JAVA平台的一部分,如圖所示(見圖中的最底端):
JVM是 Java 程序能夠實現跨平台的基礎(Java的跨平台本質上是通過不同平台的JVM實現的),它的作用是載入 Java 程序,把位元組碼(bytecode)翻譯成機器碼再交由 CPU 執行。如圖所示:
程序在執行之前先要把 Java 代碼(.java)轉換成位元組碼(.class),JVM 通過類載入器(ClassLoader)把位元組碼載入到內存中,【關注尚矽谷,輕松學IT】但位元組碼文件是 JVM 的一套指令集規范,並不能直接交給底層操作系統去執行,因此需要特定的命令解析器執行引擎(Execution Engine) 將位元組碼翻譯成底層機器碼,再交由 CPU 去執行。
市場上有哪些主流的JVM呢?
JVM是一種規范,基於這種規范,不同公司做了具體實現,BEA公司研發JRockit VM,後在2008年由Oracle公司收購;IBM公司研發了J9 VM,只應用於IBM 內部。Sun公司研發了HotSpot VM,後在2010年由Oracle公司收購。目前是甲骨文公司最主流的一款JVM虛擬機,也是我們現在最常用的一種。
JVM的體系結構是怎樣的?
JVM 的體系結構,如圖所示:
類載入系統 (ClassLoader System)負責載入類到內存;運行時數據區 (Runtime Data Area)負責存儲對象數據信息;執行引擎(Execution Engine)負責調用對象執行業務;本地庫介面(Native Interface)負責融合不同的編程語言為 Java 所用。
JVM有哪些運行模式嗎?
JVM有兩種運行模式Server與Client。兩種模式的區別在於,Client模式啟動速度較快,Server模式啟動較慢;但是啟動進入穩定期之後Server模式的程序運行速度比Client要快很多。這是因為Server模式啟動的JVM採用的是重量級的虛擬機,對程序採用了更多的優化;而Client模式啟動的JVM採用的是輕量級的虛擬機。所以Server啟動慢,但穩定後速度比Client遠遠要快。
現在64位的jdk中默認都是server模式(可通過 java -version進行查看)。當虛擬機運行在-client模式的時候,使用的是一個代號為C1的輕量級編譯器, 而server模式啟動的虛擬機採用相對重量級,代號為C2的編譯器.c1、c2都是JIT編譯器, C2比C1編譯器編譯的相對徹底,服務起來之後,性能更高。
JVM 運行時內存結構是怎樣的?
不同虛擬機實現可能略微有所不同,但都會遵從 Java 虛擬機規范,Java 8 虛
擬機規范規定,Java 虛擬機所管理的內存將會包括以下幾個區域,如圖所示:
Java 堆(Heap)
Java堆(Java Heap)是 JVM 中內存最大的一塊,被所有線程共享的,在虛擬機啟動時創建,主要用於存放對象實例,大部分對象實例也都是在這里分配。隨著JIT編譯器的發展和逃逸分析技術的逐漸成熟,棧上分配、標量替換優化的技術將會導致一些微妙的變化,所有的對象都分配在堆上漸漸變得不那麼絕對了。小對象未逃逸還可以在直接在棧上分配。如果在堆中沒有內存完成實例分配,並且堆已不可以再進行擴展時,系統底層運行時將會拋出 OutOfMemoryError。Java 虛擬機規范規定,Java 堆可以處在物理上不連續的內存空間中,只要邏輯上連續即可,就像我們的磁碟空間一樣。在實現上也可以是固定大小的,也可以是可擴展的,不過當前主流的虛擬機都是可擴展的,通過 -Xmx 和 -Xms 參數定義堆內存大小。
方法區(Method Area)
方法區(Methed Area)是一種規范,用於存儲已被虛擬機載入的類信息、常量、靜態變數、即時編譯後的代碼等數據。不同jdk,方法區的實現不同,HotSpot 虛擬機在 JDK 8 中使用 Native Memory 來實現方法區。當方法無法滿足內存分配需求時會拋出 OutOfMemoryError 異常。
Java 虛擬機棧(VM Stack)
Java 虛擬機棧(Java Virtual Machine Stacks)描述的是 Java 方法執行時的內存模型,每個方法在被線程調用時都會創建一個棧幀(Stack Frame)用於存儲局部變數表、操作數棧、動態鏈接、方法出口等信息,每個方法從調用直至執行完成的過程,【關注尚矽谷,輕松學IT】都對應著一個棧幀在虛擬機棧中入棧到出棧的過程。如果線程請求的棧深度大於虛擬機所允許的棧深度就會拋出 StackOverflowError 異常。如果虛擬機是可以動態擴展的,如果擴展時無法申請到足夠的內存就會拋出 OutOfMemoryError 異常。
JVM本地方法棧 (Native Method Stack)
本地方法棧(Native Method Stack)與虛擬機棧的作用類似,只不過虛擬機棧是服務 Java 方法的,而本地方法棧是為虛擬機調用 Native 方法服務的。在 Java 虛擬機規范中對於本地方法棧沒有特殊的要求,虛擬機可以自由的實現它,因此在 Sun HotSpot 虛擬機直接把本地方法棧和虛擬機棧合二為一了。
JVM程序計數器(Program Counter Register)
程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看作是當前線程執行的位元組碼的行號指示器。在虛擬機的概念模型里,位元組碼解析器的工作是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
由於 JVM 的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,也就是任何時刻,一個處理器(或者說一個內核)都只會執行一條線程中的指令。因此為了線程切換後能恢復到正確的執行位置,每個線程都有獨立的程序計數器。
如果線程正在執行 Java 中的方法,程序計數器記錄的就是正在執行虛擬機位元組碼指令的地址,如果是 Native 方法,這個計數器就為空(undefined),因此該內存區域是唯一一個在 Java 虛擬機規范中沒有規定 OutOfMemoryError 的區域。
如何理解JVM中的GC系統?
追蹤仍然使用的所有對象,並將其餘對象標記為垃圾,然後進行回收,這個過程稱之為GC(垃圾回收).所有的GC系統可從GC判斷策略(例如引用計數,對象可達性分析),GC收集演算法(標記-清除,標記-清除-整理,標記-復制-清除,分代),GC收集器(例如Serial,Parallel,CMS,G1)等方面進行學習
JVM引用鏈中可以作為 Root 的對象?
Java 虛擬機棧中的引用對象;
本地方法棧中 JNI(既一般說的 Native 方法)引用的對象;
方法區中類靜態常量的引用對象;
方法區中常量的引用對象。
JVM中常見垃圾回收演算法有哪些?
引用計數器演算法
這個演算法是給每一個對象設置一個引用計數器,每當有一個地方引用這個對象的時候,計數器就加 1,與之相反,每當引用失效的時候就減 1。也就是以計數來判斷對象是否為垃圾。例如:
引用計數法,有一個很大的缺陷就是循環引用,例如:
可達性分析演算法
這個演算法的核心思路就是通過一系列的「GC Roots」對象作為起始點,從這些對象開始往下搜索,搜索所經過的路徑稱之為「引用鏈」。當一個對象到 GC Roots 沒有任何引用鏈相連的時候,證明此對象是可以被回收的。例如:
復制演算法
這個演算法是將內存分為大小相同的兩塊,當這一塊使用完了,就把當前存活的對象復制到另一塊,然後一次性清空當前區塊。此演算法的缺點是只能利用一半的內存空間。例如:
標記-清除演算法
這個演算法執行分兩階段,第一階段從引用根節點開始標記所有被引用的對象,第二階段遍歷整個堆,把未標記的對象清除。此演算法需要暫停整個應用,同時,會產生內存碎片。例如:
標記-整理演算法
這個演算法結合了「標記-清除」和「復制」兩個演算法的優點。第一階段從根節點開始標記所有被引用對象,第二階段遍歷整個堆,把存活對象「壓縮」復制到堆的其中一塊空間中,按順序排放。此演算法避免了「標記-清除」的碎片問題,同時也避免了「復制」演算法的空間問題,例如:
JVM對象引用都有哪些類型?
不管是引用計數法還是可達性分析演算法都與對象的「引用」有關[說說Java中的四大引用類型。],這說明對象的引用決定了對象的生死,對象的引用關系如下。
強引用
在代碼中普遍存在的,類似 Object obj = new Object() 這類引用,只要強引用還在,垃圾收集器永遠不會回收掉被引用的對象。
軟引用
是一種相對強引用弱化一些的引用,可以讓對象豁免一些垃圾收集,只有當JVM 認為內存不足時,才會去試圖回收軟引用指向的對象,JVM 會確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對象。
弱引用
非必需對象,但它的強度比軟引用更弱,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。
虛引用
也稱為幽靈引用或幻影引用,是最弱的一種引用關系,無法通過虛引用來獲取一個對象實例,為對象設置虛引用的目的只有一個,就是當這個對象被收集器回收時收到一條系統通知。
JVM垃圾回收器的分類都有哪些?
新生代回收器
Serial、ParNew、Parallel Scavenge
老年代回收器
Serial Old、Parallel Old、CMS
整堆回收器
G1垃圾回收器
分代垃圾回收器的組成部分有哪些?
分代垃圾回收器是由新生代(Young Generation)和老生代(Tenured Generation)組成的,默認情況下新生代和老生代的內存比例是 1:2。
新生代的組成部分有哪些?:
新生代是由:Eden、Form Survivor、To Survivor 三個區域組成的,它們內存默認佔比是 8:1:1,如圖所示:
新生代垃圾回收是怎麼執行的?
第一步將Eden和From Survivor 活著的對象復制到 To Survivor 區;第二步將清空 Eden 和 From Survivor 分區;第三步將From Survivor 和 To Survivor 分區交換(From 變 To,To 變 From)。當新生代的 Survivor 分區為 2 個的時候,不論是空間利用率還是程序運行的效率都是最優的。
談談JVM中的CMS 垃圾回收器?
CMS(Concurrent Mark and Sweep)是並發標記和清除垃圾收集器。它會使用空閑列表(free-lists)管理內存空間的回收,不對老年代進行整理。其優點是在標記、清除階段的大部分工作和應用線程一起並發執行。可以降低延遲,縮短停頓時間,www.atguigu.com提高服務的響應時間。當然也有缺陷,主要表現在,對 CPU 資源要求敏感,無法清除浮動垃圾(浮動垃圾指的是 CMS 清除垃圾的時候,還有用戶線程產生新的垃圾,這部分未被標記的垃圾叫做「浮動垃圾」,只能在下次 GC 的時候進行清除),還會產生大量空間碎片。
談談JVM中的是 G1 垃圾回收器?
G1(Garbage-First GC)是一款實時收集器,其設計目標是將STW停頓時間和分布變成可預期以及可配置的。可以說是一種兼顧吞吐量和停頓時間的 GC 實現。G1 可以直觀的設定停頓時間的目標,相比於 CMS ,G1 未必能做到 CMS 在最好情況下的延時停頓,但是最差情況要好很多。
使用G1收集器時,Java堆的內存布局與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔閡了,它們都是一部分(可以不連續)Region的集合,例如:
這樣的劃分使得 GC不必每次都去收集整個堆空間, 而是以增量的方式來處理,每次只處理一部分小堆區,稱為此次的回收集(collection set). 每次暫停都會收集所有年輕代的小堆區, 同時也可能只包含一部分老年代小堆區。
G1的另一項創新, 是在並發階段估算每個小堆區存活對象的總數。用來構建回收集(collection set)的原則是: 垃圾最多的小堆區會被優先收集。這也是G1名稱的由來:garbage-first。
G1 解決了 CMS 中的各種疑難問題, 包括暫停時間的可預測性, 並終結了堆內存的碎片化。對單業務延遲非常敏感的系統來說, 如果CPU資源不受限制,那麼G1可以說是 HotSpot 中最好的選擇, 特別是在最新版本的Java虛擬機中。當然,這種降低延遲的優化也不是沒有代價的: 由於額外的寫屏障(write barriers)和更積極的守護線程, G1的開銷會更大。所以, 如果系統屬於吞吐量優先型的,又或者CPU持續佔用100%, 而又不在乎單次GC的暫停時間, 那麼CMS是更好的選擇。
JVM垃圾回收的調優參數有哪些?
-Xmx:512 設置最大堆內存為 512 M;
-Xms:256 初始堆內存(最小堆)為 256 M;
-XX:MaxNewSize 設置最大年輕代內存;
-XX:MaxTenuringThreshold=6 設置新生代對象經過6次GC晉升到老年代;
-XX:PretrnureSizeThreshold 設置大對象的值,超過這個值的大對象直接進入老生代;
-XX:NewRatio 設置分代垃圾回收器新生代和老生代內存佔比;
-XX:SurvivorRatio 設置新生代 Eden、Form Survivor、To Survivor 佔比。
JVM現代並發GC有什麼調優原則
第一要空間換時間與效率,針對G1 & ZGC 加大堆內存(更多的空餘空間)的配置往往更有利 於GC達到目標暫停時間。第二要知道低暫停不代表高吞吐量,並發GC是保證並發階段GC的同時業務線程依然有幾率獲得CPU時間片,但同時也意味著GC會與業務線程搶占計算資源,且往往更多的並發階段為了處理更多的同步問題,也會佔用更多的計算資源。第三是GC調優永遠要考慮機器資源,對應系統應用場景等等,至少目前沒有銀彈。
文章來源於jason
I. 編譯器的工作原理
編譯 是從源代碼(通常為高級語言)到能直接被計算機或虛擬機執行的目標代碼(通常為低級語言或機器語言)的翻譯過程。然而,也存在從低級語言到高級語言的編譯器,這類編譯器中用來從由高級語言生成的低級語言代碼重新生成高級語言代碼的又被叫做反編譯器。也有從一種高級語言生成另一種高級語言的編譯器,或者生成一種需要進一步處理的的中間代碼的編譯器(又叫級聯)。
典型的編譯器輸出是由包含入口點的名字和地址, 以及外部調用(到不在這個目標文件中的函數調用)的機器代碼所組成的目標文件。一組目標文件,不必是同一編譯器產生,但使用的編譯器必需採用同樣的輸出格式,可以鏈接在一起並生成可以由用戶直接執行的EXE,
所以我們電腦上的文件都是經過編譯後的文件。
J. 編譯器錯誤怎麼解決
1、分析原因,這樣的錯誤出現一般是由於伺服器拒絕了某一項請求,常見的是寫入,所以問題在有表單輸入的網頁中更容易出現。