1. 程式人生 > >Java基礎總結(內部版)

Java基礎總結(內部版)

Java基礎總結

琥魄 琥魄 瀏覽 4 2016-07-28 10:45:38 發表於: 網商銀行技術部落格 >> Java技術

編輯 刪除

Java核心技術Java  修改標籤  標籤歷史

阿里實習在內部部落格發的部落格, 排版較CSDN明顯好看很多

轉載留作紀念

一、JVM

1、記憶體模型

1.1.1 記憶體分幾部分

這裡寫圖片描述

(1)程式計數器

可看作當前執行緒所執行的位元組碼的**行號指示器**。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。

線上程建立時建立。執行本地方法時,PC的值為null。為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,執行緒私有

(2)Java虛擬機器棧

執行緒私有,生命週期同線程。**每個方法在執行同時,建立棧幀**。用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。棧中的區域性變量表主要存放一些基本型別的變數(int, short, long, byte, float,double, boolean, char)和物件控制代碼。

棧中有區域性變量表,包含引數和區域性變數。

這裡寫圖片描述

此外,java中沒有暫存器,因此所有的引數傳遞依靠運算元棧。

棧上分配,小物件(一般幾十個bytes),在沒有逃逸的情況下,可以直接分配在棧上。(沒有逃逸是指,物件只能給當前執行緒使用,如果多個執行緒都要用,則不可以,因為棧是執行緒私有的。)直接分配在棧上,可以自動回收,減輕GC壓力。因為棧本身比較小,大物件也不可以分配,會影響效能。

-XX:+DoEscapeAnalysis 啟用逃逸分析,若非逃逸則可棧上分配。

(3)本地方法棧

執行緒私有,與Java虛擬機器棧非常相似,區別不過是虛擬機器棧為虛擬機器執行Java 方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器使用到的 Native 方法(非java語言實現,比如C)服務。Hotspot 直接把本地方法棧和虛擬機器棧合二為一。

棧&本地方法棧:**執行緒建立時產生,方法執行是生成棧幀。**

(4)Java堆

執行緒共有(可能劃分出多個執行緒私有的分配緩衝區,Thread Local Allow),Java虛擬機器管理記憶體中最大的一塊,此區域唯一目的就是存放物件例項,幾乎所有物件例項在此區分配,執行緒共享記憶體。可細分為新生代和老年代,方便GC。主流虛擬機器都是按可擴充套件實現(通過-Xmx 和 -Xms 控制)。

注意:Java堆是Java程式碼可及的記憶體,是留給開發人員使用的;**非堆(Non-Heap)**就是JVM留給 自己用的,所以方法區、JVM內部處理或優化所需的記憶體(如JIT編譯後的程式碼快取)、每個類結構(如執行時常數池、欄位和方法資料)以及方法和構造方法的程式碼都在非堆記憶體中。

關於TLAB

Sun Hotspot JVM為了提升物件記憶體分配的效率,對於所建立的執行緒都會分配一塊獨立的空間TLAB(Thread Local Allocation Buffer),其大小由JVM根據執行的情況計算而得,在TLAB上分配物件時不需要加鎖,因此JVM在給執行緒的物件分配記憶體時會盡量的在TLAB上分配,在這種情況下JVM中分配物件記憶體的效能和C基本是一樣高效的,但如果物件過大的話則仍然是直接使用堆空間分配

Java堆:**在虛擬機器啟動時建立**

(5)方法區

執行緒共有,用於儲存已被虛擬機器載入的類資訊、常量池、靜態變數、即時編譯器編譯後的程式碼等資料。雖然Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但它卻有一個別名Non-Heap(非堆),目的是與Java堆區分開。

注意,通常和永久區(Perm)關聯在一起。但也不一定,JDK6時,String等常量資訊保存於方法區,JDK7時,移動到了堆。永久代和方法區不是一個概念,但是有的虛擬機器用永久代來實現方法區,可以用永久代GC來管理方法區,省去專門寫的功夫。

(6)執行時常量池

方法區的一部分,存放編譯期生成的各種字面量和符號引用。

(7)直接記憶體

並不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域,也可能導致 OOM 異常(記憶體區域綜合>實體記憶體時)。NIO類,可以使用Native 函式庫直接分配堆外記憶體,然後通過一個儲存在Java 堆裡面的 DirectByteBuffer 物件作為這塊記憶體的引用進行操作。

這裡寫圖片描述

類載入時 方法資訊儲存在一塊稱為方法區的記憶體中, 並不隨你建立物件而隨物件保存於堆中。可參考《深入java虛擬機器》前幾章。
另參考(他人文章):
如果instance method也隨著instance增加而增加的話,那記憶體消耗也太大了,為了做到共用一小段記憶體,Java 是根據this關鍵字做到的,比如:instance1.instanceMethod(); instance2.instanceMethod(); 在傳遞給物件引數的時候,Java 編譯器自動先加上了一個this引數,它表示傳遞的是這個物件引用,雖然他們兩個物件共用一個方法,但是他們的方法中所產生的資料是私有的,這是因為引數被傳進來變成call stack內的entry,而各個物件都有不同call stack,所以不會混淆。其實呼叫每個非static方法時,Java 編譯器都會自動的先加上當前呼叫此方法物件的引數,有時候在一個方法呼叫另一個方法,這時可以不用在前面加上this的,因為要傳遞的物件引數就是當前執行這個方法的物件。

1.1.2 堆溢位、棧溢位原因及例項,線上如何排查

(1)棧溢位

遞迴,容易引起棧溢位stackoverflow;因為方法迴圈呼叫,方法呼叫會不斷建立棧幀。
造成棧溢位的幾種情況:
1)遞迴過深
2)陣列、List、map資料過大
3 ) 建立過多執行緒

對於Java虛擬機器棧和本地方法棧,Java虛擬機器規範規定了**兩種異常**狀況:

① 執行緒請求深度>虛擬機器所允許的深度,將丟擲StackOverFlowError(SOF)異常;

② 如果虛擬機器可動態擴充套件,且擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemoryError(OOM)異常。

(2)堆溢位

如果在堆中沒有記憶體完成例項分配,且堆無法擴充套件時,將丟擲OOM異常。

在方法區也會丟擲 OOM 異常。

例項

可使用以下程式碼造成堆疊溢位:

package overflow;

import java.util.ArrayList;

/**
 * Created by hupo.wh on 2016/7/7.
 */

public class MyTest {

    public void testHeap(){
        for(;;){
            ArrayList list = new ArrayList (2000);
        }
    }
    int num=1;
    public void testStack(){
        num++;
        this.testStack();
    }

    public static void main(String[] args){

        MyTest t  = new MyTest();
        t.testHeap();
        //t.testStack();
    }
}

如下程式碼會造成OOM堆溢位:

package OOM;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by hupo.wh on 2016/7/15.
 */
public class App1 {

    static class OOMClass {
        long[] num = new long[10240];
    }

    public static void main(String[] args) {
        List<OOMClass> list = new ArrayList<>();
        while (true) {
            list.add(new OOMClass());
        }
    }


}

另外,Java虛擬機器的堆大小如何設定:命令列
 java –Xms128m //JVM佔用最小記憶體
–Xmx512m //JVM佔用最大記憶體
–XX:PermSize=64m //最小堆大小
–XX:MaxPermSize=128m //最大堆大小

2、類載入機制

基本上所有的類載入器都是 java.lang.ClassLoader類的一個例項。下面詳細介紹這個 Java 類。

1.2.1 java.lang.ClassLoader類介紹

java.lang.ClassLoader類的基本職責就是根據一個**指定的類的名稱**,找到或者生成其對應的位元組程式碼,然後從這些位元組程式碼中定義出一個 Java 類,即 java.lang.Class類的一個例項。除此之外,ClassLoader還負責載入 Java 應用所需的資源,如影象檔案和配置檔案等。

1.2.2 類載入器的樹狀組織結構

Java 中的類載入器大致可以分成兩類:一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。**系統提供**的類載入器主要有下面三個:

(1)引導類載入器(bootstrap class loader):它用來載入 Java 的核心庫,是用原生程式碼來實現的,並不繼承自 java.lang.ClassLoader。

BootStrapClassLoader
負責jdk_home/jre/lib目錄下的核心 api或 -Xbootclasspath選項指定的jar包載入進來。

(2)擴充套件類載入器(extensions class loader):它用來載入 Java 的擴充套件庫。Java 虛擬機器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入 Java 類。

ExtClassLoader
負責jdk_home/jre/lib/ext目錄下的jar包或 -Djava.ext.dirs指定目錄下的jar包載入進來。

(3)系統類載入器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來載入 Java 類。一般來說,Java 應用的類都是由它來完成載入的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。

AppClassLoader
負責java -classpath/-Djava.class.path所指的目錄下的類與jar包載入進來,System.getClassLoader獲取到的就是這個類載入器。

除了系統提供的類載入器以外,開發人員可以通過繼承 java.lang.ClassLoader類的方式實現自己的類載入器,以滿足一些特殊的需求。

除了引導類載入器之外,所有的類載入器都有一個父類載入器。getParent()方法可以得到。對於系統提供的類載入器來說,系統類載入器的父類載入器是擴充套件類載入器,而擴充套件類載入器的父類載入器是引導類載入器;對於開發人員編寫的類載入器來說,其父類載入器是載入此類載入器 Java 類的類載入器。因為類載入器 Java 類如同其它的 Java 類一樣,也是要由類載入器來載入的。一般來說,開發人員編寫的類載入器的父類載入器是系統類載入器。類載入器通過這種方式組織起來,形成樹狀結構。樹的根節點就是引導類載入器。

這裡寫圖片描述

1.2.3 雙親委派模型

類載入器在嘗試自己去查詢某個類的位元組程式碼並定義它時,會先代理給其父類載入器,由父類載入器先去嘗試載入這個類,依次類推。

在介紹代理模式背後的動機之前,首先需要說明一下 Java 虛擬機器是如何判定兩個 Java 類是相同的。**Java 虛擬機器不僅要看類的全名是否相同,還要看載入此類的類載入器是否一樣。**只有兩者都相同的情況,才認為兩個類是相同的。即便是同樣的位元組程式碼,被不同的類載入器載入之後所得到的類,也是不同的。比如一個 Java 類 com.example.Sample,編譯之後生成了位元組程式碼檔案 Sample.class。兩個不同的類載入器 ClassLoaderA和 ClassLoaderB分別讀取了這個 Sample.class檔案,並定義出兩個 java.lang.Class類的例項來表示這個類。這兩個例項是不相同的。對於 Java 虛擬機器來說,它們是不同的類。試圖對這兩個類的物件進行相互賦值,會丟擲執行時異常 ClassCastException。

所以才有雙親委派模型,這樣的話,可保證載入的類(特別是Object和String這類基礎類)是同一個。

package classloaderstring;

/**
 * Created by hupo.wh on 2016/7/7.
 */
public class String {

    public java.lang.String toString() {
        return "這是我自定義的String類的toString方法";
    }
}

package classloaderstring;

import java.lang.*;
import java.lang.reflect.Method;

/**
 * Created by hupo.wh on 2016/7/7.
 */
public class TestString {

    public static void main(java.lang.String args[]) throws Exception {

        java.lang.String classDataRootPath = "D:\\xiaohua\\WhTest\\out\\production\\WhTest\\classloader\\Sample";
        FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);

        Class<?> class1 = fscl1.loadClass("classloaderstring.String");
        Object obj1 = class1.newInstance();

        System.out.println(java.lang.String.class.getClassLoader());
        System.out.println(class1.getClassLoader());

        System.out.println(java.lang.String.class);
        System.out.println(class1);

        Method setSampleMethod = class1.getMethod("toString");
        System.out.println(setSampleMethod.invoke(obj1));
    }
}

輸出:

null
[email protected]42a57993
class java.lang.String
class classloaderstring.String
這是我自定義的String類的toString方法

這兩個類並不是一個String類,要包名類名+loader一致是不可能的,所以雙親委派模型從外界無法破壞。

注意:這裡有

  1. 若載入的類能被系統載入器載入到(Sample類在classpath下),則無異常。因為defining class loader都是AppClassLoader
  2. 若載入的類不能被系統載入器載入到,則拋異常。此時的 defining class loader 才是自定義的 FileSystemClassLoader

1.2.4 defining loader 和 initiating loader

前面提到過類載入器會首先代理給其它類載入器來嘗試載入某個類。這就意味著真正完成類的載入工作的類載入器和啟動這個載入過程的類載入器,有可能不是同一個。真正完成類的載入工作是通過呼叫 defineClass來實現的;而啟動類的載入過程是通過呼叫 loadClass來實現的。前者稱為一個類的定義載入器(**defining loader**),後者稱為初始載入器(**initiating loader**)。在 Java 虛擬機器判斷兩個類是否相同的時候,使用的是類的定義載入器。也就是說,哪個類載入器啟動類的載入過程並不重要,重要的是最終定義這個類的載入器。兩種類載入器的關聯之處在於:**一個類的定義載入器是它引用的其它類的初始載入器。**如類 com.example.Outer引用了類 com.example.Inner,則由類 com.example.Outer的定義載入器負責啟動類 com.example.Inner的載入過程。

方法 loadClass()丟擲的是 java.lang.ClassNotFoundException異常;方法 defineClass()丟擲的是 java.lang.NoClassDefFoundError異常。

類載入器在成功載入某個類之後,會把得到的 java.lang.Class類的例項快取起來。下次再請求載入該類的時候,類載入器會直接使用快取的類的例項,而不會嘗試再次載入。也就是說,對於一個類載入器例項來說,相同全名的類只加載一次,即 loadClass方法不會被重複呼叫。

1.2.5 Class.forName 載入

Class.forName是一個靜態方法,同樣可以用來載入類。該方法有兩種形式:

Class.forName(String name, boolean initialize, ClassLoader loader)

Class.forName(String className)

第一種形式的引數 name表示的是類的全名;initialize表示是否初始化類;loader表示載入時使用的類載入器。

第二種形式則相當於設定了引數 initialize的值為 true,loader的值為當前類的類載入器。Class.forName的一個很常見的用法是在載入資料庫驅動的時候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用來載入 Apache Derby 資料庫的驅動。

1.2.6 類載入過程

從類被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,類的生命週期包括載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)7個階段。

這裡寫圖片描述

其中載入(除了自定義載入)+連結的過程是完全由jvm負責的,什麼時候要對類進行初始化工作(載入+連結在此之前已經完成了),jvm有嚴格的規定(四種情況):

1.遇到new,getstatic,putstatic,invokestatic這4條位元組碼指令時,加入類還沒進行初始化,則馬上對其進行初始化工作。其實就是3種情況:用new例項化一個類時、讀取或者設定類的靜態欄位時(不包括被final修飾的靜態欄位,因為他們已經被塞進常量池了)、以及執行靜態方法的時候。

2.使用java.lang.reflect.*的方法對類進行反射呼叫的時候,如果類還沒有進行過初始化,馬上對其進行。

3.初始化一個類的時候,如果他的父親還沒有被初始化,則先去初始化其父親。

4.當jvm啟動時,使用者需要指定一個要執行的主類(包含static void main(String[] args)的那個類),則jvm會先去初始化這個類。

以上4種預處理稱為對一個類進行主動的引用,其餘的其他情況,稱為被動引用,都不會觸發類的初始化。

載入:
在載入階段,虛擬機器主要完成三件事:

1.通過一個類的全限定名來獲取定義此類的二進位制位元組流。
2.將這個位元組流所代表的靜態儲存結構轉化為方法區域的執行時資料結構。
3.在Java堆中生成一個代表這個類的java.lang.Class物件,作為方法區域資料的訪問入口。

驗證:

驗證階段作用是保證Class檔案的位元組流包含的資訊符合JVM規範,不會給JVM造成危害。如果驗證失敗,就會丟擲一個java.lang.VerifyError異常或其子類異常。驗證過程分為四個階段:

1.檔案格式驗證:驗證位元組流檔案是否符合Class檔案格式的規範,並且能被當前虛擬機器正確的處理。
2.元資料驗證:是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言的規範。
3.位元組碼驗證:主要是進行資料流和控制流的分析,保證被校驗類的方法在執行時不會危害虛擬機器。
4.符號引用驗證:符號引用驗證發生在虛擬機器將符號引用轉化為直接引用的時候,這個轉化動作將在解析階段中發生。

準備:

準備階段為變數分配記憶體並設定類變數的初始化。在這個階段分配的僅為類的變數(static修飾的變數),而不包括類的例項變數,例項變數將會在物件例項化時隨著物件一起分配在Java堆中。對非final的變數,JVM會將其設定成“零值”,而不是其賦值語句的值:

private static int size = 12;

那麼在這個階段,size的值為0,而不是12。 final修飾的類變數將會賦值成真實的值。

解析:

解析過程是將常量池內的符號引用替換成直接引用。主要包括四種類型引用的解析。類或介面的解析、欄位解析、方法解析、介面方法解析。

初始化:

在準備階段,類變數已經經過一次初始化了,在這個階段,則是根據程式設計師通過程式制定的計劃去初始化類的變數和其他資源。這些資源有static{}塊,建構函式,父類的初始化等。

至於使用和解除安裝階段階段,這裡不再過多說明,使用過程就是根據程式定義的行為執行,解除安裝由GC完成

3、垃圾回收 GC

1.3.1 引用計數法

目前主流的虛擬機器都沒有使用引用計數法,主要原因就是它很難解決物件之間互相迴圈引用的問題。

1.3.2 可達性分析演算法

思想:

通過一系列稱為 GC Roots 的物件作為起始點,從這些點開始向下搜尋,搜尋走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈連線(用圖論的話來說,就是從GC Roots到這個物件不可達),證明此物件不可用。

Java語言中,可作為GC Roots的物件包括:

(1)虛擬機器棧(棧幀中的本地變量表)中引用的物件

(2)方法區中類靜態屬性引用的物件

(3)方法區中常量引用的物件

(4)本地方法棧中JNI ( 即一般說的Native方法)引用的物件

1.3.3 再談引用

在JDK 1.2之後 ,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference )、軟引用(Soft Reference )、弱引用(Weak Reference )、虛引用(Phantom Reference) 4種 , 引用強度依次逐漸減弱。

強引用

指在程式程式碼之中普遍存在的,類似“Object obj=new Object ( ) ”這類的引用 ,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件。

軟引用

用來描述一些還有用但並非必需的物件。對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行**二次回收**。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。在JDK 1.2之後,提供了SoftReference類來實現軟引用。

弱引用

也是用來描述非必需物件的,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。在JDK1.2之後,提供了PhantomReference類來實現虛引用。

虛引用

也稱為幽靈引用或者幻影引用,它是最弱的一種引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。在JDK1.2之後,提供了PhantomReference類來實現虛引用。

1.3.4 物件回收過程

即使在可達性分析演算法中不可達的物件,也並非是“非死不可”的 ,這時候它們暫時處於“緩刑” 階段 ,要真正宣告一個物件死亡 ,至少要經歷**兩次標記過程**

如果這個物件被判定為有必要執行finalize() 方法,那麼這個物件將會放置在一個叫做 F-Queue的佇列之中,並在稍後由一個由虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行它。

1.3.5 對於方法區(Hotspot虛擬機器的永久代)的回收

判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是“無用的類”:

(1)該類所有的例項都已經被回收,也就是Java堆中不存在該類的任何例項

(2)載入該類的ClassLoader已經被回收

(3)該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法

1.3.6 垃圾收集演算法

1.3.6.1 標記-清除演算法

望名生意,演算法分為“標記”和“清除”兩個階段:

首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件,它的標記過程如前

它的主要不足有兩個:

(1)效率問題,標記和清除兩個過程的效率都不高;

(2)空間問題,標記清除之後會產生**大量不連續的記憶體碎片**,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

這裡寫圖片描述

1.3.6.2 複製演算法

將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。

適用於物件存活率低的場景(新生代)

這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標 ,按順序分配記憶體即可,實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小為了原來的一半,未免太高了一點。

將記憶體分為**一塊較大的Eden空間和兩塊較小的Survivor空間** ,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活著的物件一次地複製到另外一塊Survivor空間上,最 後清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機器預設Eden和Survivor的大小比例是 8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的90% ( 80%+10% ) ,只有10% 的記憶體會被 “浪費”。當然,98%的物件可回收只是一般場景下的資料,我們沒有辦法保證每次回收都只有不多於10%的物件存活,當Survivor空間不夠用時,需要依賴其他記憶體(這裡指老年代)進行分配擔保( Handle Promotion ) 。

這裡寫圖片描述

1.3.6.3 標記-整理演算法

適用於物件存活率高的場景(老年代)

複製收集演算法在物件存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是 ,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法。

標記過程類似“標記-清除”演算法,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體,類似於磁碟整理的過程

這裡寫圖片描述

總的分類如下圖:

這裡寫圖片描述

1.3.7 記憶體申請過程

記憶體由Perm和Heap組成。其中Heap = {Old + NEW = { Eden , from, to } }。perm用來存放常量等。
heap中分為年輕代(young)和年老代(old)。年輕代又分為Eden,Survivor(倖存區)。Survivor又分為from,to,也可以不只是這兩塊,切from和to沒有先後順序。其中,old和young區比例可手動分配。

這裡寫圖片描述

當OLD區空間不夠時,JVM會在OLD區進行完全的垃圾收集。完全垃圾收集後,若Survivor及OLD區仍然無法存放從Eden複製過來的部分物件,導致JVM無法在Eden區為新物件建立記憶體區域,則出現”out of memory”Error。

4、JVM啟動過程

JVM工作原理和特點主要是指作業系統裝入JVM是通過jdk中Java.exe來完成,通過下面4步來完成JVM環境.

1.建立JVM裝載環境和配置
2.裝載JVM.dll
3.初始化JVM.dll並掛界到JNIENV(JNI呼叫介面)例項
4.呼叫JNIEnv例項裝載並處理class類。

這裡寫圖片描述

5、Class檔案結構

Class檔案的總體結構如下:

Class檔案 {
    檔案描述
    常量池
    類概述
    欄位表
    方法表
    擴充套件資訊表
}

1.5.1 檔案描述

(1)magic位、class檔案版本號。Magic位很容易記住,數值是0xCAFEBABE。

(2)常量池
儲存一組常量,供class檔案中其它元素引用。常量池中順序儲存著一組常量,常量在池中的位置稱為索引。Class檔案中其它結構通過索引來引用常量。常量最主要是被指令引用。編譯器將原始碼編譯成指令和常量,圖形表示如下:

這裡寫圖片描述

(3)類概述
儲存了當前類的總體資訊,包括當前類名、所繼承的父類、所實現的介面。

(4)欄位表
儲存了一組欄位結構,類中每個欄位對應一個欄位結構。
欄位結構儲存了欄位的資訊,包括欄位名、欄位修飾符、欄位指向的型別等。

(5)方法表
儲存了一組方法結構,類中每個方法對應一個方法結構。
方法結構比較複雜,它內部最重要的結構是Code結構。每個非抽象方法的方法結構下有一個Code結構,儲存了方法的位元組碼。

(6)擴充套件資訊表
儲存了類級別的可選資訊,例如類級別的annotation。(方法、欄位級別的annotation分別儲存在方法結構、欄位結構中)

1.5.2 棧結構

我們對於站結構的內部構造,大部分則瞭解甚少。位元組碼的執行依賴棧結構,理解棧結構是理解位元組碼的基礎。

棧由幀組成,一個幀對應一個方法呼叫。一個方法被呼叫時,一個幀被建立,方法返回時,對應的幀被銷燬。

幀儲存了方法執行期間的資料,包括變數資料和計算的中間結果。幀由兩部分組成,變量表和操作棧。這兩個結構是位元組碼執行期間直接依賴的兩個結構

操作棧

顧名思義,操作棧是一個棧結構,即LIFO結構。操作棧位於幀內部,用於儲存方法執行期間的中間結果。操作棧在JVM中的角色,類似於暫存器在實體機中的角色。

位元組碼中絕大多數指令,都是圍繞著操作棧執行的。它們或是從其他地方讀資料,壓入操作棧;或是從操作棧彈資料進行處理;還有的先彈資料,再處理,最會將結果壓入操作。
在JVM中,要對資料進行處理,首先要把資料讀進操作棧。

int變數求和

要對兩個int變數求和,我們先通過iload指令量兩個變數壓入操作棧,然後執行iadd指令。iadd從操作棧彈出兩個int值,求和,然後將結果壓入操作棧。

呼叫方法物件
呼叫物件方法時,我們需要將被呼叫物件,呼叫引數依次壓入操作棧,然後執行invokevirtual指令。該指令從操作棧彈出呼叫引數,被呼叫物件,執行方法呼叫。

變量表
變量表用於儲存變數資料。
變量表由一組槽組成。一個槽能夠儲存一個除long、double外其他型別的資料。兩個槽能夠儲存一個long型或double型資料。變數所在的槽在變量表中位置稱為變數索引,對於long和double型別,變數索引是第一個槽的位置。

變數在表量表中的順序是:
this、方法引數(從左向右)、其它變數
如果是static方法,則this沒有。
示例:
有如下方法:

void test(int a,int b){
    int c=0;
    long  d=0;
}

其對應的變量表為:

這裡寫圖片描述

二、Java基礎

1、什麼是介面?什麼是抽象類?區別是什麼?

2.1.1 介面

在軟體工程中,介面泛指供別人呼叫的方法或者函式。從這裡,我們可以體會到Java語言設計者的初衷,它是**對行為的抽象**。

介面中可以含有 變數和方法。但是要注意,介面中的變數會**被隱式地指定為public static final變數**(並且**只能是public static final變數**,用private修飾會報編譯錯誤),而**方法會被隱式地指定為public abstract方法且只能是public abstract方法**(用其他關鍵字,比如private、protected、static、 final等修飾會報編譯錯誤),並且介面中所有的方法不能有具體的實現,也就是說,介面中的方法必須都是抽象方法。從這裡可以隱約看出介面和抽象類的區別,介面是一種極度抽象的型別,它比抽象類更加“抽象”,並且一般情況下不在介面中定義變數。

  可以看出,允許一個類遵循多個特定的介面。如果一個非抽象類遵循了某個介面,就必須實現該介面中的所有方法。對於遵循某個介面的抽象類,可以不實現該介面中的抽象方法。

2.1.2 抽象類

抽象方法是一種特殊的方法:它只有宣告,而沒有具體的實現。抽象方法的宣告格式為:

abstract void fun();

  抽象方法必須用abstract關鍵字進行修飾。如果一個類含有抽象方法,則稱這個類為抽象類,抽象類必須在類前用abstract關鍵字修飾。因為抽象類中含有無具體實現的方法,所以**不能用抽象類建立物件。**

  下面要注意一個問題:在《JAVA程式設計思想》一書中,將抽象類定義為“包含抽象方法的類”,但是後面發現如果一個類不包含抽象方法,只是用abstract修飾的話也是抽象類。也就是說抽象類不一定必須含有抽象方法。個人覺得這個屬於鑽牛角尖的問題吧,因為如果一個抽象類不包含任何抽象方法,為何還要設計為抽象類?所以暫且記住這個概念吧,不必去深究為什麼。

/**
 * Created by hupo.wh on 2016/7/7.
 */
public abstract class AbstractClass {

    public void ab() {
        System.out.println("Hello");
    }

}

  從這裡可以看出,抽象類就是為了繼承而存在的,如果你定義了一個抽象類,卻不去繼承它,那麼等於白白建立了這個抽象類,因為你不能用它來做任何事情。對於一個父類,如果它的某個方法在父類中實現出來沒有任何意義,必須根據子類的實際需求來進行不同的實現,那麼就可以將這個方法宣告為abstract方法,此時這個類也就成為abstract類了。

  包含抽象方法的類稱為抽象類,但並不意味著抽象類中只能有抽象方法,它和普通類一樣,同樣可以擁有成員變數和普通的成員方法。注意,抽象類和普通類的主要有三點區別:

  1)抽象方法必須為public或者protected(因為如果為private,則不能被子類繼承,子類便無法實現該方法),預設情況下預設為public。

  2)抽象類不能用來建立物件;

  3)如果一個類繼承於一個抽象類,則子類必須實現父類的抽象方法。如果子類沒有實現父類的抽象方法,則必須將子類也定義為為abstract類。

  在其他方面,抽象類和普通的類並沒有區別。

2.1.3 區別

2.1.3.1 語法層面上的區別

  1)抽象類可以提供成員方法的實現細節,而介面中只能存在public abstract 方法;

  2)抽象類中的成員變數可以是各種型別的,而介面中的成員變數只能是public static final型別的;

  3)介面中不能含有靜態程式碼塊以及靜態方法,而抽象類可以有靜態程式碼塊和靜態方法;

  4)一個類只能繼承一個抽象類,而一個類卻可以實現多個介面。

2.1.3.2 設計層面上的區別

  1)**抽象類是對一種事物的抽象,即對類抽象,而介面是對行為的抽象。**

抽象類是對整個類整體進行抽象,包括屬性、行為,但是介面卻是對類區域性(行為)進行抽象。舉個簡單的例子,飛機和鳥是不同類的事物,但是它們都有一個共性,就是都會飛。那麼在設計的時候,可以將飛機設計為一個類Airplane,將鳥設計為一個類Bird,但是不能將 飛行 這個特性也設計為類,因此它只是一個行為特性,並不是對一類事物的抽象描述。此時可以將 飛行 設計為一個介面Fly,包含方法fly( ),然後Airplane和Bird分別根據自己的需要實現Fly這個介面。然後至於有不同種類的飛機,比如戰鬥機、民用飛機等直接繼承Airplane即可,對於鳥也是類似的,不同種類的鳥直接繼承Bird類即可。從這裡可以看出,繼承是一個 "是不是"的關係,而 介面 實現則是 "有沒有"的關係。如果一個類繼承了某個抽象類,則子類必定是抽象類的種類,而介面實現則是有沒有、具備不具備的關係,比如鳥是否能飛(或者是否具備飛行這個特點),能飛行則可以實現這個介面,不能飛行就不實現這個介面。

  2)**設計層面不同,抽象類作為很多子類的父類,它是一種模板式設計。而介面是一種行為規範,它是一種輻射式設計。**

什麼是模板式設計?最簡單例子,大家都用過ppt裡面的模板,如果用模板A設計了ppt B和ppt C,ppt B和ppt C公共的部分就是模板A了,如果它們的公共部分需要改動,則只需要改動模板A就可以了,不需要重新對ppt B和ppt C進行改動。而輻射式設計,比如某個電梯都裝了某種報警器,一旦要更新報警器,就必須全部更新。也就是說對於抽象類,如果需要新增新的方法,可以直接在抽象類中新增具體的實現,子類可以不進行變更;而對於介面則不行,如果介面進行了變更,則所有實現這個介面的類都必須進行相應的改動。

2、什麼是序列化?

2.2.1 概念

序列化,序列化是可以把物件轉換成位元組流在網路上傳輸。將一個java物件變成位元組流的形式傳出去或者從一個位元組流中恢復成一個java物件。

個人認為,序列化就是一種思想,能夠完成轉換,能夠轉換回來,效率越高越好

序列化(Serialization)是將物件的狀態資訊轉換為可以儲存或傳輸的形式的過程。在序列化期間,物件將其當前狀態寫入到臨時或永續性儲存區。之後可以通過從儲存區中讀取或反序列化物件的狀態,重新建立該物件。

java中的序列化(serialization)機制能夠將一個例項物件的狀態資訊寫入到一個位元組流中,使其可以通過socket進行傳輸、或者持久化儲存到資料庫或檔案系統中;然後在需要的時候,可以根據位元組流中的資訊來重構一個相同的物件。序列化機制在java中有著廣泛的應用,EJB、RMI等技術都是以此為基礎的。

一般而言,要使得一個類可以序列化,只需簡單實現java.io.Serializable介面即可(還要實現無引數的構造方法)。該介面是一個標記式介面,它本身不包含任何內容,實現了該介面則表示這個類準備支援序列化的功能。

2.2.2 序列化與反序列化例程

序列化一般有三種形式:預設形式、xml、json格式

預設格式如下:

package serializable;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * Created by hupo.wh on 2016/7/3.
 */
public class SerializeToFlatFile {

    public static void main(String[] args) {
        SerializeToFlatFile ser = new SerializeToFlatFile();
        ser.savePerson();
        ser.restorePerson();
    }

    public void savePerson(){
        Person myPerson = new Person("Jay", 24);
        try{
            FileOutputStream fos = new FileOutputStream("d:\\person.txt");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            System.out.println("Person--Jay,24---Written");

            oos.writeObject(myPerson);
            oos.flush();
            oos.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    //@SuppressWarnings("resource")
    public void restorePerson(){
        try{
            FileInputStream fls = new FileInputStream("d:\\person.txt");
            ObjectInputStream ois = new ObjectInputStream(fls);

            Person myPerson = (Person)ois.readObject();
            System.out.println("\n---------------------\n");
            System.out.println("Person --read:");
            System.out.println("Name is:"+myPerson.getName());
            System.out.println("Age is :"+myPerson.getAge());

        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

另兩種大同小異

2.2.3 應用場景

序列化的實現:將需要被序列化的類實現Serializable介面,該介面沒有需要實現的方法,implements Serializable只是為了標註該物件是可被序列化的,然後使用一個輸出流(如:FileOutputStream)來構造一個ObjectOutputStream(物件流)物件,接著,使用ObjectOutputStream物件的writeObject(Object obj)方法就可以將引數為obj的物件寫出(即儲存其狀態),要恢復的話則用輸入流

三種情況下需要進行序列化

1、把物件持久化到檔案或資料中
2、在網路上傳輸
3、進行RMI傳輸物件時

RPC和RMI都是遠端呼叫,屬於中介軟體技術。RMI是針對於java語言的,它使用的是JRMP協議通訊,而RPC是更大眾化的,使用http協議傳輸。

其版本號id,Java的序列化機制是通過在執行時判斷類的serialVersionUID來驗證版本一致性的。在進行反序列化時,JVM會把傳來的位元組流中的serialVersionUID與本地相應實體(類)的serialVersionUID進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常。

常用序列化技術有3種:java seriaizable,hessian,hessian2,以及protobuf

工具有很多,網上有個對比:

這裡寫圖片描述

這裡寫圖片描述

3、網路通訊過程及實踐

2.3.1 TCP三次握手和四次揮手

明顯三次握手是建立連線,四次揮手是斷開連線,總圖如下:

這裡寫圖片描述

2.3.1.1 握手

(1)首先,Client端傳送連線請求報文(SYN=1,seq=client_isn)

(2)Server段接受連線後回覆ACK報文,併為這次連線分配資源。(SYN=1,seq=client_isn,ack = client_isn+1)

(3)Client端接收到ACK報文後也向Server段發生ACK報文,並分配資源,這樣TCP連線就建立了。(SYN=0,seq=client_isn+1,ack = server_isn+1)

三次握手過程如下圖所示:

這裡寫圖片描述

2.3.1.2 揮手

注意:
中斷連線端可以是Client端,也可以是Server端。

這裡寫圖片描述

(1)假設Client端發起中斷連線請求,也就是傳送FIN報文。

(2) Server端接到FIN報文後,意思是說"我Client端沒有資料要發給你了",但是如果你還有資料沒有傳送完成,則不必急著關閉Socket,可以繼續傳送資料。所以 Server 端會先發送ACK,"告訴Client端,你的請求我收到了,但是我還沒準備好,請繼續你等我的訊息"。

這個時候Client端就進入 FIN_WAIT 狀態,繼續等待Server端的FIN報文。

(3)當Server端確定資料已傳送完成,則向Client端傳送FIN報文,"告訴Client端,好了,我這邊資料發完了,準備好關閉連線了"。

(4)Client端收到FIN報文後,"就知道可以關閉連線了,但是他還是不相信網路,怕Server端不知道要關閉,所以傳送 ACK 後進入 TIME_WAIT 狀態,如果 Server 端沒有收到 ACK 則可以重傳“,Server端收到ACK後,"就知道可以斷開連線了"。

Client端等待了2MSL後依然沒有收到回覆,則證明Server端已正常關閉,那好,我Client端也可以關閉連線了。Ok,TCP連線就這樣關閉了!

注意:

(1)2個wait狀態,FIN_WAIT和TIME_WAIT

(2)如果是Server端發起,過程反過來,因為在揮手的時候c和s在對等位置。

2.3.1.3 握手揮手狀態圖

Client端所經歷的狀態如下:

這裡寫圖片描述

Server端所經歷的過程如下:

這裡寫圖片描述

2.3.1.4 注意問題

1、在TIME_WAIT狀態中,如果TCP client端最後一次傳送的ACK丟失了,它將重新發送。TIME_WAIT狀態中所需要的時間是依賴於實現方法的。典型的值為30秒、1分鐘和2分鐘。等待之後連線正式關閉,並且所有的資源(包括埠號)都被釋放。

2、為什麼連線的時候是三次握手,關閉的時候卻是四次握手?

答:因為當Server端收到Client端的SYN連線請求報文後,可以直接傳送SYN+ACK報文。其中ACK報文是用來應答的,SYN報文是用來同步的。但是關閉連線時,當Server端收到FIN報文時,很可能並不會立即關閉SOCKET,所以只能先回復一個ACK報文,告訴Client端,"你發的FIN報文我收到了"。只有等到我Server端所有的報文都發送完了,我才能傳送FIN報文,因此不能一起傳送。故需要四步握手。

3、為什麼TIME_WAIT狀態需要經過2MSL(最大報文段生存時間)才能返回到CLOSE狀態?

答:雖然按道理,四個報文都發送完畢,我們可以直接進入CLOSE狀態了,但是我們必須假象網路是不可靠的,有可以最後一個ACK丟失。所以TIME_WAIT狀態就是用來重發可能丟失的ACK報文。

2.3.1.5 附:報文詳解

TCP報文中的SYN,FIN,ACK,PSH,RST,URG

TCP的三次握手是怎麼進行的:傳送端傳送一個SYN=1,ACK=0標誌的資料包給接收端,請求進行連線,這是第一次握手;接收端收到請求並且允許連線的話,就會發送一個SYN=1,ACK=1標誌的資料包給傳送端,告訴它,可以通訊了,並且讓傳送端傳送一個確認資料包,這是第二次握手;最後,傳送端傳送一個SYN=0,ACK=1的資料包給接收端,告訴它連線已被確認,這就是第三次握手。之後,一個TCP連線建立,開始通訊。

*SYN:同步標誌
同步序列編號(Synchronize Sequence Numbers)欄有效。該標誌僅在三次握手建立TCP連線時有效。它提示TCP連線的服務端檢查序列編號,該序列編號為TCP連線初始端(一般是客戶端)的初始序列編號。在這裡,可以把 TCP序列編號看作是一個範圍從0到4,294,967,295的32位計數器。通過TCP連線交換的資料中每一個位元組都經過序列編號。在TCP報頭中的序列編號欄包括了TCP分段中第一個位元組的序列編號。

*ACK:確認標誌
確認編號(Acknowledgement Number)欄有效。大多數情況下該標誌位是置位的。TCP報頭內的確認編號欄內包含的確認編號(w+1,Figure-1)為下一個預期的序列編號,同時提示遠端系統已經成功接收所有資料。

*RST:復位標誌
復位標誌有效。用於復位相應的TCP連線。

*URG:緊急標誌
緊急(The urgent pointer) 標誌有效。緊急標誌置位

*PSH:推標誌
該標誌置位時,接收端不將該資料進行佇列處理,而是儘可能快將資料轉由應用處理。在處理 telnet 或 rlogin 等互動模式的連線時,該標誌總是置位的。

*FIN:結束標誌
帶有該標誌置位的資料包用來結束一個TCP回話,但對應埠仍處於開放狀態,準備接收後續資料。

TCP的幾個狀態對於我們分析所起的作用
在TCP層,有個FLAGS欄位,這個欄位有以下幾個標識:SYN, FIN, ACK, PSH, RST, URG.其中,對於我們日常的分析有用的就是前面的五個欄位。它們的含義是:SYN表示建立連線,FIN表示關閉連線,ACK表示響應,PSH表示有 DATA資料傳輸,RST表示連線重置。其中,ACK是可能與SYN,FIN等同時使用的,比如SYN和ACK可能同時為1,它表示的就是建立連線之後的響應,如果只是單個的一個SYN,它表示的只是建立連線。

TCP的幾次握手就是通過這樣的ACK表現出來的。但SYN與FIN是不會同時為1的,因為前者表示的是建立連線,而後者表示的是斷開連線。RST一般是在FIN之後才會出現為1的情況,表示的是連線重置。一般地,當出現FIN包或RST包時,我們便認為客戶端與伺服器端斷開了連線;而當出現SYN和SYN+ACK包時,我們認為客戶端與伺服器建立了一個連線。PSH為1的情況,一般只出現在 DATA內容不為0的包中,也就是說PSH為1表示的是有真正的TCP資料包內容被傳遞。TCP的連線建立和連線關閉,都是通過請求-響應的模式完成的。

2.3.2 Socket通訊

套接字(socket)是通訊的基石,是支援TCP/IP協議的網路通訊的基本操作單元。它是網路通訊過程中端點的抽象表示,包含進行網路通訊必須的五種資訊:連線使用的協議,本地主機的IP地址,本地程序的協議埠,遠地主機的IP地址,遠地程序的協議埠。

套接字對是一個四元組,(local ip, local port, remote ip, remote port),通過這一四元組,唯一確定了網路通訊的兩端(兩個程序或執行緒),ip地址確定主機,埠確定程序。

經典的在同一臺主機上兩個程序或執行緒之間的通訊通過以下三種方法

管道通訊(Pipes)
訊息佇列(Message queues)
共享記憶體通訊(Shared memory)
這裡有許多其他的方法,但是上面三中是非常經典的程序間通訊。

socket程式設計例項:

/////TalkClient .java

package socket;

import java.io.*;
import java.net.*;

/**
 * Created by hupo.wh on 2016/7/8.
 */
public class TalkClient {

    public static void main(String args[]) {

        try {

            Socket socket = new Socket("10.63.37.140", 4700);

            //向本機的4700埠發出客戶請求

            BufferedReader sin = new BufferedReader(new InputStreamReader(System.in));

            //由系統標準輸入裝置構造BufferedReader物件

            PrintWriter os = new PrintWriter(socket.getOutputStream());

            //由Socket物件得到輸出流,並構造PrintWriter物件

            BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            //由Socket物件得到輸入流,並構造相應的BufferedReader物件

            String readline;

            readline = sin.readLine(); //從系統標準輸入讀入一字串

            while (!readline.equals("bye")) {

                //若從標準輸入讀入的字串為 "bye"則停止迴圈

                os.println(readline);

                //將從系統標準輸入讀入的字串輸出到Server

                os.flush();

                //重新整理輸