問:String,StringBuffer,StringBuilder的區別
答:
- String是java中的字符串類型,被final關鍵字修飾,內部使用final修飾的char[] value存放字符,和final修飾的count來存儲數組長度。因為是final修飾的類,所以不能被繼承。因為final修飾的 value和count,所以不可被改變,所以String是不可變數據,可以被線程安全的共享。所以總結下來,String本質是字符數組,不可繼承切不可改變。對String進行操作時,每次都是生成新的String對象。
- 因為String的特性,java的字符串運算比較耗性能,所以提供了可變的字符串實現,也就是StringBuilder。StringBuilder繼承了AbstractStringBuilder,AbstractStringBuilder也是通過一個char類型的數組value和count實現的。value和count是可變的,所以append方法中會把新的字符添加到value中并且改變count的值。當value放不下的時候,會進行擴容操作。擴容是將value容量變成count+1再乘以2,如果還小于所需的最小長度,就擴容的所需的最小長度。擴容之后用Arrays.copyOf復制字符。
- StringBuffer跟StringBuilder的實現原理一樣,只是所有的方法都加了同步操作,所以效率會低,但是線程安全。
問: 下面一行代碼到底做了什么呢?
String s = new String("ddd")
答:
- 在編譯期,編譯器到常量池里面尋找有沒有"ddd"這個字符串,如果沒有則在常量池中開辟空間存放"ddd";在運行時,在堆內存中開辟空間存放String類型的對象,然后在棧中開辟空間存放變量s。
問:既然講到常量池,堆內存,棧,那就講一下這些都是什么
答:
堆內存,棧內存這些都是jvm的邏輯內存模型。java的運行期數據區分為5部分,分別是方法區,java堆,java棧,程序計數器和本地方法棧。其中方法區和java堆是所有線程共享的,其他的都是線程私有的。
程序計數器,是一塊很小的內存空間,可以看作是當前線程執行的字節碼的行號。存儲的是正在執行的java方法的虛擬機字節碼指令地址。如果線程在執行native方法,則存儲0.此內存區域是唯一沒有OutOfMemoryError的區域。
-
方法區,jvm內部存儲類型信息的地方。類型信息是由類加載器加載類的時候生成的。一個類要被使用會由java虛擬機對.class文件進行裝載,連接(驗證,準備,解析)和初始化。方法區會存儲類型信息,字段信息,方法信息和其他信息。其他信息中就包含上一個問題說到的常量池。jvm為每一個已加載的類分配一個常量池,包含實際的常量(String,Integer和floating pointer常量)和對類型,字段和方法的符號引用。
- 方法區是所有線程共享的,所以數據必須被設計成線程安全的。例如,假如同時有兩個線程都企圖訪問方法區中的同一個類,而這個類還沒有被裝入JVM,那么只允許一個線程去裝載它,而其它線程必須等待
- 方法區大小可以動態調整,可以通過-XX:PermSize 和 -XX:MaxPermSize 參數限制方法區的大小。
- 方法區也會發生gc,主要是針對常量池的回收和對類型的卸載
- HotSpot會用永久代來實現方法區
-
java棧,也叫虛擬機棧,是描述java方法執行的內存模型的。每個方法被執行的時候,都會生成一個棧幀用于存儲局部變量表,操作數棧,動態鏈接,方法出口等信息。java棧是線程私有的,隨線程的生命周期。每一個方法被調用到執行完成,就對應著一個棧幀從入棧到出棧。對于執行引擎來說,只有棧頂的棧幀是有效的,被稱作當前棧幀。
- 局部變量表,用于存放方法參數和方法內部定義的局部變量。局部變量表以變量槽(slot)為單位,32位系統中,一個slot可以存放32位以內的數據類型,包括(boolean,byte,short,int,float,reference和returnAddress)。returnAddress類型是為字節碼指令jsr、jsr_w和ret服務的,它指向了一條字節碼指令的地址。
- 操作數棧,和局部變量表一樣被設計成以字長為單位的數組。但操作只能按照棧的方式來操作。也是操作一樣的數據類型。byte,short和char在入棧之前會轉換成int類型的。虛擬機的大部分工作都是基于操作數棧。比如加法操作,就是先往棧里壓入兩個int類型的數據,再彈出兩個變量執行iadd操作,然后將計算的值存的局部變量表里。偽指令如下:
<pre>
begin
iload_0 // push the int in local variable 0 ontothe stack
iload_1 //push the int in local variable 1 onto the stack
iadd // pop two ints, add them, push result
istore_2 // pop int, store into local variable 2
end
</pre>- 動態鏈接,虛擬機運行的時候,運行時常量池會保存大量的符號引用,這些符號引用可以看成是每個方法的間接引用。如果代表棧幀A的方法想調用代表棧幀B的方法,那么這個虛擬機的方法調用指令就會以B方法的符號引用作為參數,但是因為符號引用并不是直接指向代表B方法的內存位置,所以在調用之前還必須要將符號引用轉換為直接引用,然后通過直接引用才可以訪問到真正的方法。如果符號引用是在類加載階段或者第一次使用的時候轉化為直接應用,那么這種轉換成為靜態解析,如果是在運行期間轉換為直接引用,那么這種轉換就成為動態連接。
- 返回地址,方法返回有兩種情況,1是正常退出,2是異常退出。正常退出要根據方法定義看是否返回數據給上層調用者,調用者的pc計數器的值就可以作為返回地址;異常退出要根據異常處理表來確定返回地址。方法退出時要恢復上層方法的局部變量表和操作數棧,如果有返回值的話,就把返回值壓入調用者棧幀的操作數棧,并把pc計數器的值調整為調用入口的下一條指令
本地方法棧,與虛擬機棧類似。只不過虛擬機棧為執行java方法服務,本地方法棧為執行native方法服務。
-
java堆,堆是jvm管理的最大的內存塊,所有的線程共享,用于存放對象實例。垃圾回收也主要發生在這塊區域中。
- 堆的大小可以通過 -Xms和-Xmx配置
- 從內存回收的角度看,現在收集器都采用分代收集算法。將內存分為新生代和老年代。新生代又分為Eden區和Survivor區,Survivor區又分為from區和to區
- 老年代用于存放經過新生代GC后還存活的對象和大對象
- 通過-Xmn指定新生代大小,通過-XX:PretenureSizeThreshold設置多大的對象直接分配在老年代中
- 內存分配過程
- JVM 會試圖為相關Java對象在Eden Space中初始化一塊內存區域。
- 當Eden空間足夠時,內存申請結束;否則到下一步。
- JVM 試圖釋放在Eden中所有不活躍的對象(這屬于1或更高級的垃圾回收)。釋放后若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區。
- Survivor區被用來作為Eden及Old的中間交換區域,當Old區空間足夠時,Survivor區的對象會被移到Old區,否則會被保留在Survivor區。
- 當Old區空間不夠時,JVM 會在Old區進行完全的垃圾收集(0級)。
- 完全垃圾收集后,若Survivor及Old區仍然無法存放從Eden復制過來的部分對象,導致JVM無法在Eden區為新對象創建內存區域,則出現“outofmemory”錯誤。
問:剛剛講到java內存中的垃圾回收,可以說一下你理解的垃圾回收嗎
答:
- 垃圾回收機制最早出現在lisp語言中,后來java借鑒了過來。垃圾回收主要是讓程序自己管理內存的回收,而不用程序員手動去回收內存。一般來說,java程序員可以不重視java內存分配和垃圾回收,但充分了解jvm的GC機制可以讓程序員更好的利用計算機資源。
- gc要為程序員管理內存,就要確定哪些內存需要回收,什么時候回收和怎么回收。
- 確定哪些內存需要回收,有兩種方法:
- 引用計數法,創建對象的時候,為這個對象在堆棧中分配內存,同時產生一個引用計數器并加1,對象的一個引用銷毀的時候,引用計數器減1,引用計數器為0的時候,表示這個對象可以回收了。jdk1.2之前用的這種方法。但是這樣會有循環引用問題,就是A保留B的引用,B保留A的引用,雖然都沒有外部引用了,但引用計數器不為0,所以不能回收。
- 根搜索算法,利用圖論中的理論,從一個節點GC ROOT根開始搜索,所有子引用節點搜索完成后,剩余未被搜索到的節點即無用節點,可以被回收。
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區中靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中引用的對象
- 兩種判定算法都用到了引用,去判斷對象是否可以回收。一般引用分為4種類型,強引用,軟引用,弱引用和虛引用。
- 強引用就是我們平時代碼中,指向新建對象的變量。只要強引用在,對象就不會被回收。
- 軟引用描述一些還有用但非必要存在的對象,內存不足時才會被回收,一般和引用隊列(ReferenceQueue)共同使用做內存敏感的高速緩存
- 弱引用也是描述非必要對象,不管內存足不足都會回收
- 虛引用不影響對象的生命周期,虛引用必須和引用隊列關聯使用,程序可以判斷引用隊列中是否加入虛引用來判斷對象是否要被回收
- 解決了哪些內存要回收的問題之后,再就是怎么回收的問題了。垃圾回收的策略有四種,分別是標記-清除算法,復制算法,標記-整理算法和分代收集算法
- 標記-清除算法分兩個階段,先標識哪些內存需要清除,然后統一回收要清除的對象。這樣有兩個問題,標記和清除的效率都不高,而且會產生內存碎片問題
- 復制算法就是把內存平均分配成兩塊,每次使用其中一塊,內存回收時,把存活的對象復制到另一塊上。特點是實現簡單效率高,適用于短生命周前對象,但可用內存少了一半
- 標記-整理算法,對于存活率較高的對象,如果采用復制算法就會導致多次復制,效率十分低。老年代對象就有這種特點。標記-整理算法是一種老年代回收算法,過程跟標記-清除算法類似。不同的是在第二階段不會直接清除,而是將對象向一端移動,然后直接清理掉端邊界以外的內存
- 分代算法就是將內存分為不同的塊,根據每塊內存的特點選擇使用合適的回收算法。一般新生代采用復制算法,老年代采用標記整理算法
- 回收算法是理論基礎,jvm提供了多種算法的實現,也就是我們說的垃圾回收器
- Serial回收器,采用復制算法,回收時暫停所有用戶線程。適合單核cpu。是新生代垃圾回收器
- ParNew,Serial的多線程版本。適合多cpu場景。出了Serial,只有它可以和CMS配合使用
- Parallel Scavenge,也是新生代收集器,目標是提高吞吐量,適應主要適合在后臺運算而不需要太多交互的任務。
- Serial Old,是老年代的單線程收集器,作為CMS收集器的后備預案
- Parallel Old,是ParallelScavenge收集器的老年代版本,使用多線程和“標記-整理”算法。
- CMS是一種以獲取最短回收停頓時間為目標的收集器,優點是并發收集,停頓低。缺點是對cpu敏感,沒法處理浮動垃圾,會產生內存碎片問題
- G1,可以參考深入理解G1
- G1對空間壓縮有優勢
- 通過將內存分成區域(region)的方式避免內存碎片
- Eden, Survivor, Old區不再固定、在內存使用效率上來說更靈活
- G1可以通過設置預期停頓時間(Pause Time)來控制垃圾收集時間避免應用雪崩現象
- G1在回收內存后會馬上同時做合并空閑內存的工作、而CMS默認是在STW(stop the world)的時候做
- G1會在Young GC中使用、而CMS只能在O區使用
- 觸發GC
- Minor GC(新生代回收)的觸發條件比較簡單,Eden空間不足就開始進行Minor GC回收新生代。
- 而Full GC(老年代回收,一般伴隨一次Minor GC)則有幾種觸發條件:
- 老年代空間不足
- PermSpace空間不足
- 統計得到的Minor GC晉升到老年代的平均大小大于老年代的剩余空間
- 確定哪些內存需要回收,有兩種方法: