JVM
Jvm體系總體分四大塊:類的加載機制、Jvm內存結構、GC算法垃圾回收、GC分析命令調優。
類的加載機制
類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然后在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的Class對象,Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。
類的生命周期
- 加載:查找並加載類的二進制數據,在Java堆中也創建一個java.lang.Class類的對象;
- 連接:連接又包含三塊內容:驗證、准備、解析;
- 驗證:文件格式、元數據、字節碼、符號引用驗證;
- 准備,為類的靜態變量分配內存,並將其初始化為默認值;
- 解析,把類中的符號引用轉換為直接引用;
- 初始化:為類的靜態變量賦予正確的初始值;
- 使用:new出對象程序中使用;
- 卸載,執行垃圾回收。
類加載器
- 啟動類加載器:Bootstrap ClassLoader,負責加載存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath參數指定的路徑中的,並且能被虛擬機識別的類庫;
- 擴展類加載器:Extension ClassLoader,它負責加載JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴展類加載器;
- 應用程序類加載器:Application ClassLoader,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器。
類加載機制
- 全盤負責:當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入;
- 父類委托:先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類;
- 緩存機制:緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統才會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區。這就是為什么修改了Class后,必須重啟JVM,程序的修改才會生效。
Jvm內存結構
1、內存結構
方法區和堆是所有線程共享的內存區域;而java棧、本地方法棧、程序計數器是線程私有的內存區域。
- 堆:是Java虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存;
- 方法區:與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據;
- 程序計數器:是一塊較小的內存空間,它的作用可以看做是當前線程所執行的字節碼的行號指示器;
JVM棧:與程序計數器一樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用於存儲局部變量表、操作棧、動態鏈接、方法出口等信息。每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程; - 本地方法棧:與虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是為虛擬機使用到的Native方法服務。
2、對象分配規則
- 對象優先分配在Eden區,如果Eden區沒有足夠的空間時,虛擬機執行一次Minor GC;
- 大對象直接進入老年代(大對象是指需要大量連續內存空間的對象)。這樣做的目的是避免在Eden區和兩個Survivor區之間發生大量的內存拷貝(新生代采用復制算法收集內存);
- 長期存活的對象進入老年代。虛擬機為每個對象定義了一個年齡計數器,如果對象經過了1次Minor GC那么對象會進入Survivor區,之后每經過一次Minor GC那么對象的年齡加1,知道達到閥值對象進入老年區;
- 動態判斷對象的年齡。如果Survivor區中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代;
- 空間分配擔保。每次進行Minor GC時,JVM會計算Survivor區移至老年區的對象的平均大小,如果這個值大於老年區的剩余值大小則進行一次Full GC,如果小於檢查HandlePromotionFailure設置,如果true則只進行Monitor GC,如果false則進行Full GC。
GC算法、垃圾回收
1、對象存活判斷
- 引用計數:每個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數為0時可以回收。此方法簡單,無法解決對象相互循環引用的問題;
- 可達性分析:從GC Roots開始向下搜索,搜索所走過的路徑稱為引用鏈。當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的,不可達對象。
2、GC算法
GC最基礎的算法有三種:標記 -清除算法、復制算法、標記-壓縮算法,我們常用的垃圾回收器一般都采用分代收集算法
- 標記清除算法:“標記-清除”(Mark-Sweep)算法,如它的名字一樣,算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后統一回收掉所有被標記的對象;
- 復制算法:“復制”(Copying)的收集算法,它將可用內存按容量划分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉;
- 標記壓縮算法:標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存;
- 分代收集算法:“分代收集”(Generational Collection)算法,把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。
3、垃圾回收器
- Serial收集器:串行收集器是最古老,最穩定以及效率高的收集器,可能會產生較長的停頓,只使用一個線程去回收;
- ParNew收集器:ParNew收集器其實就是Serial收集器的多線程版本;
- Parallel收集器:Parallel Scavenge收集器類似ParNew收集器,Parallel收集器更關注系統的吞吐量;
- Parallel Old 收集器:Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法;
- CMS收集器:CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器;
- G1收集器:G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特征。
4、四種引用
- 強引用:如果一個對象具有強引用,它就不會被垃圾回收器回收。即使當前內存空間不足,JVM也不會回收它,而是拋出 OutOfMemoryError 錯誤,使程序異常終止。如果想中斷強引用和某個對象之間的關聯,可以顯式地將引用賦值為null,這樣一來的話,JVM在合適的時間就會回收該對象;
- 軟引用:在使用軟引用時,如果內存的空間足夠,軟引用就能繼續被使用,而不會被垃圾回收器回收,只有在內存不足時,軟引用才會被垃圾回收器回收;
- 弱引用:具有弱引用的對象擁有的生命周期更短暫。因為當 JVM 進行垃圾回收,一旦發現弱引用對象,無論當前內存空間是否充足,都會將弱引用回收。不過由於垃圾回收器是一個優先級較低的線程,所以並不一定能迅速發現弱引用對象;
- 虛引用:顧名思義,就是形同虛設,如果一個對象僅持有虛引用,那么它相當於沒有引用,在任何時候都可能被垃圾回收器回收。
GC分析、命令調優
1、GC日志分析
摘錄GC日志一部分(前部分為年輕代gc回收;后部分為full gc回收):
2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs] 2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]
通過上面日志分析得出,PSYoungGen、ParOldGen、PSPermGen屬於Parallel收集器
2、調優命令
Sun JDK監控和故障處理命令有jps jstat jmap jhat jstack jinfo
- jps:JVM Process Status Tool,顯示指定系統內所有的HotSpot虛擬機進程;
- jstat:JVM statistics Monitoring是用於監視虛擬機運行時狀態信息的命令,它可以顯示出虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據;
- jmap:JVM Memory Map命令用於生成heap dump文件;
- jhat:JVM Heap Analysis Tool命令是與jmap搭配使用,用來分析jmap生成的dump,jhat內置了一個微型的HTTP/HTML服務器,生成dump的分析結果后,可以在瀏覽器中查看;
- jstack:用於生成java虛擬機當前時刻的線程快照;
- jinfo;JVM Configuration info 這個命令作用是實時查看和調整虛擬機運行參數。
3、調優工具
常用調優工具分為兩類,jdk自帶監控工具:jconsole和jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。
- jconsole:Java Monitoring and Management Console是從java5開始,在JDK中自帶的java監控和管理控制台,用於對JVM中內存,線程和類等的監控;
- jvisualvm:jdk自帶全能工具,可以分析內存快照、線程快照;監控內存變化、GC變化等;
- MAT:Memory Analyzer Tool,一個基於Eclipse的內存分析工具,是一個快速、功能豐富的Java heap分析工具,它可以幫助我們查找內存泄漏和減少內存消耗;
- GChisto:一款專業分析gc日志的工具。
volatile關鍵字
volatile是一個類型修飾符(type specifier),volatile的作用是作為指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值。Volatile變量具有 synchronized 的可見性特性,但是不具備原子特性。
在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內存中讀,跳過 CPU cache 這一步。
當一個變量定義為 volatile 之后,將具備兩種特性:
- 保證此變量對所有的線程的可見性,這里的“可見性”,當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。但普通變量做不到這點,普通變量的值在線程間傳遞均需要通過主內存(詳見:Java內存模型)來完成。
- 禁止指令重排序優化。有volatile修飾的變量,賦值后多執行了一個“load addl $0x0, (%esp)”操作,這個操作相當於一個內存屏障(指令重排序時不能把后面的指令重排序到內存屏障之前的位置),只有一個CPU訪問內存時,並不需要內存屏障;(什么是指令重排序:是指CPU采用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理)。
性能
volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因為它需要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。
作用
- 內存可見性;
- 有序性、禁止指令重排相關的內容。
synchronized關鍵字
修飾一個代碼塊
被修飾的代碼塊稱為同步語句塊,其作用的范圍是大括號{}括起來的代碼,作用的對象是調用這個代碼塊的對象。
修飾一個方法
被修飾的方法稱為同步方法,其作用的范圍是整個方法,作用的對象是調用這個方法的對象。
修改一個靜態的方法
其作用的范圍是整個靜態方法,作用的對象是這個類的所有對象。
修改一個類
其作用的范圍是synchronized后面括號括起來的部分,作用主的對象是這個類的所有對象。
HsahMap的原理
JDK版本區別
- 在JDK1.6,JDK1.7中,HashMap采用位桶+鏈表實現,即使用鏈表處理沖突,同一hash值的鏈表都存儲在一個鏈表里。
- 在JDK1.8中,HashMap采用位桶+鏈表+紅黑樹實現,當鏈表長度超過閾值(8)時,將鏈表轉換為紅黑樹,這樣大大減少了查找時間。
原理
HashMap基於hashing原理,我們通過put()和get()方法儲存和獲取對象。
將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,然后找到bucket位置來儲存值對象。
當獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然后返回值對象。
HashMap使用鏈表來解決碰撞問題,當發生碰撞了,對象將會儲存在鏈表的下一個節點中。HashMap在每個鏈表節點中儲存鍵值對對象。
當兩個不同的鍵對象的hashcode相同時會發生什么?
它們會儲存在同一個bucket位置的鏈表中。鍵對象的equals()方法用來找到鍵值對。
當兩個對象的hashcode相同會發生什么?
因為hashcode相同,所以它們的bucket位置相同,‘碰撞’會發生。因為HashMap使用鏈表存儲對象,這個Entry(包含有鍵值對的Map.Entry對象)會存儲在鏈表中。
如果兩個鍵的hashcode相同,如何獲取值對象?
當調用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,找到bucket位置之后,會調用keys.equals()方法去找到鏈表中正確的節點,最終找到要找的值對象。
key的選取要使用不可變的、聲明作final的對象,並且采用合適的equals()和hashCode()方法的話,將會減少碰撞的發生,提高效率。不可變性使得能夠緩存不同鍵的hashcode,這將提高整個獲取對象的速度,使用String,Interger這樣的wrapper類作為鍵是非常好的選擇。
如果HashMap的大小超過了負載因子(load factor)定義的容量,怎么辦?
默認的負載因子大小為0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會創建原來HashMap大小的兩倍的bucket數組,來重新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫作rehashing,因為它調用hash方法找到新的bucket位置。
重新調整HashMap大小存在什么問題?
當重新調整HashMap大小的時候,確實存在條件競爭,因為如果兩個線程都發現HashMap需要重新調整大小了,它們會同時試着調整大小。在調整大小的過程中,存儲在鏈表中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。
ConcurrentHashMap和Hashtable的區別
Hashtable和ConcurrentHashMap都可以用於多線程的環境,但是當Hashtable的大小增加到一定的時候,性能會急劇下降,因為迭代時需要被鎖定很長的時間。
因為ConcurrentHashMap引入了分割(segmentation),不論它變得多么大,僅僅需要鎖定map的某個部分,而其它的線程不需要等到迭代完成才能訪問map。簡而言之,在迭代的過程中,ConcurrentHashMap僅僅鎖定map的某個部分,而Hashtable則會鎖定整個map。
深拷貝和淺拷貝
淺拷貝
被復制對象的所有變量都含有與原來的對象相同的值,而所有的對其他對象的引用仍然指向原來的對象。
換言之,淺拷貝僅僅復制所考慮的對象,而不復制它所引用的對象。
深拷貝
被復制對象的所有變量都含有與原來的對象相同的值,而那些引用其他對象的變量將指向被復制過的新對象,而不再是原有的那些被引用的對象。
換言之,深拷貝把要復制的對象所引用的對象都復制了一遍。
單例模式
單例模式要求類能夠有返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用getInstance這個名稱)。
單例的實現主要是通過以下兩個步驟
- 將該類的構造方法定義為私有方法,這樣其他處的代碼就無法通過調用該類的構造方法來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例;
- 在該類內提供一個靜態方法,當我們調用這個方法時,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就創建該類的實例並將實例的引用賦予該類保持的引用。
優點
系統內存中該類只存在一個對象,節省了系統資源,對於一些需要頻繁創建銷毀的對象,使用單例模式可以提高系統性能。
缺點
當想實例化一個單例類的時候,必須要記住使用相應的獲取對象的方法,而不是使用new,可能會給其他開發人員造成困擾,特別是看不到源碼的時候。
適用場合
- 需要頻繁的進行創建和銷毀的對象;
- 創建對象時耗時過多或耗費資源過多,但又經常用到的對象;
- 工具類對象;
- 頻繁訪問數據庫或文件的對象。
線程
進程與線程
進程是指一個內存中運行的應用程序,每個進程都有自己獨立的一塊內存空間,即進程空間或(虛空間)。進程不依賴於線程而獨立存在,一個進程中可以啟動多個線程。
線程是指進程中的一個執行流程,一個進程中可以運行多個線程。比如java.exe進程中可以運行很多線程。線程總是屬於某個進程,線程沒有自己的虛擬地址空間,與進程內的其他線程一起共享分配給該進程的所有資源。
Java中的線程
在 Java程序中,有兩種方法創建線程:
- 一是對 Thread 類進行派生並覆蓋 run方法;
- 二是通過實現Runnable接口創建。
線程的狀態
- 新建狀態(New): 線程對象被創建后,就進入了新建狀態。例如,Thread thread = new Thread()。
- 就緒狀態(Runnable): 也被稱為“可執行狀態”。線程對象被創建后,其它線程調用了該對象的start()方法,從而來啟動該線程。例如,thread.start()。處於就緒狀態的線程,隨時可能被CPU調度執行。
- 運行狀態(Running) : 線程獲取CPU權限進行執行。需要注意的是,線程只能從就緒狀態進入到運行狀態。
- 阻塞狀態(Blocked) : 阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分三種:
- (01) 等待阻塞 -- 通過調用線程的wait()方法,讓線程等待某工作的完成。
- (02) 同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態。
- (03) 其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
- 死亡狀態(Dead): 線程執行完了或者因異常退出了run()方法,該線程結束生命周期。
相關API
- notify():喚醒在此對象監視器上等待的單個線程。
- notifyAll():喚醒在此對象監視器上等待的所有線程。
- wait():讓當前線程處於“等待(阻塞)狀態”,“直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法”,當前線程被喚醒(進入“就緒狀態”),是會線程釋放它所持有對象的同步鎖。
- wait(long timeout):讓當前線程處於“等待(阻塞)狀態”,“直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量”,當前線程被喚醒(進入“就緒狀態”)。
- wait(long timeout, int nanos):讓當前線程處於“等待(阻塞)狀態”,“直到其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者其他某個線程中斷當前線程,或者已超過某個實際時間量”,當前線程被喚醒(進入“就緒狀態”)。
- yield():作用是讓步。它能讓當前線程由“運行狀態”進入到“就緒狀態”,從而讓其它具有相同優先級的等待線程獲取執行權;但是,並不能保證在當前線程調用yield()之后,其它具有相同優先級的線程就一定能獲得執行權;也有可能是當前線程又進入到“運行狀態”繼續運行;是不會釋放鎖。
- sleep() :作用是讓當前線程休眠,即當前線程會從“運行狀態”進入到“休眠(阻塞)狀態”。sleep()會指定休眠時間,線程休眠的時間會大於/等於該休眠時間;在線程重新被喚醒時,它會由“阻塞狀態”變成“就緒狀態”,從而等待cpu的調度執行。sleep也是不會釋放鎖的。
- join():是讓主線程會等待子線程結束之后才能繼續運行。
並行與並發
並行:多個cpu實例或者多台機器同時執行一段處理邏輯,是真正的同時。
並發:通過cpu調度算法,讓用戶看上去同時執行,實際上從cpu操作層面不是真正的同時。並發往往在場景中有公用的資源,那么針對這個公用的資源往往產生瓶頸,我們會用TPS或者QPS來反應這個系統的處理能力。
ThreadLocal
- 作用:保存線程的獨立變量。當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。常用於用戶登錄控制,如記錄session信息。
- 實現:每個Thread都持有一個TreadLocalMap類型的變量(該類是一個輕量級的Map,功能與map一樣,區別是桶里放的是entry而不是entry的鏈表。功能還是一個map。)以本身為key,以目標為value。
- 主要方法是get()和set(T a),set之后在map里維護一個threadLocal -> a,get時將a返回。
原子類
使用atomic wrapper class如AtomicInteger、AtomicBoolean、AtomicReference等,或者使用自己保證原子的操作,則等同於synchronized。
Lock類
lock在java.util.concurrent包內,主要目的是和synchronized一樣, 兩者都是為了解決同步問題,處理資源爭端而產生的技術。共有三個實現:
ReentrantLock ReentrantReadWriteLock.ReadLock ReentrantReadWriteLock.WriteLock
與synchronized的區別:
- Lock不是Java語言內置的,synchronized是Java語言的關鍵字,因此是內置特性。Lock是一個類,通過這個類可以實現同步訪問;
- Lock和synchronized有一點非常大的不同,采用synchronized不需要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執行完之后,系統會自動讓線程釋放對鎖的占用;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現死鎖現象。
Java8的特性
Lambda表達式
Lambda允許把函數作為一個方法的參數。
Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) ); Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );
方法引用
方法引用提供了非常有用的語法,可以直接引用已有Java類或對象(實例)的方法或構造器。與lambda聯合使用,方法引用可以使語言的構造更緊湊簡潔,減少冗余代碼。
默認方法
默認方法就是一個在接口里面有了一個實現的方法。
Stream API
新添加的Stream API(java.util.stream) 把真正的函數式編程風格引入到Java中。
Date Time API
加強對日期與時間的處理
Optional類
Optional 類已經成為 Java 8 類庫的一部分,用來解決空指針異常。
Nashorn, JavaScript 引擎
Java 8提供了一個新的Nashorn javascript引擎,它允許我們在JVM上運行特定的javascript應用。
HTTP協議
HTTP(Hyper Text Transfer Protocol)超文本傳輸協議,是一種建立在TCP上的無狀態連接,整個基本的工作流程是客戶端發送一個HTTP請求,說明客戶端想要訪問的資源和請求的動作,服務端收到請求之后,服務端開始處理請求,並根據請求做出相應的動作訪問服務器資源,最后通過發送HTTP響應把結果返回給客戶端。
其中一個請求的開始到一個響應的結束稱為事務,當一個事物結束后還會在服務端添加一條日志條目。
特點
- 無狀態:協議對客戶端沒有狀態存儲,對事物處理沒有“記憶”能力,比如訪問一個網站需要反復進行登錄操作;
- 無連接:HTTP/1.1之前,由於無狀態特點,每次請求需要通過TCP三次握手四次揮手,和服務器重新建立連接。比如某個客戶機在短時間多次請求同一個資源,服務器並不能區別是否已經響應過用戶的請求,所以每次需要重新響應請求,需要耗費不必要的時間和流量;
- 基於請求和響應:基本的特性,由客戶端發起請求,服務端響應;
- 簡單快速、靈活;
- 通信使用明文、請求和響應不會對通信方進行確認、無法保護數據的完整性。
請求
HTTP請求是客戶端往服務端發送請求動作,告知服務器自己的要求。HTTP請求由狀態行、請求頭、請求正文三部分組成:
- 狀態行:包括請求方式Method、資源路徑URL、協議版本Version;
- 請求頭:包括一些訪問的域名、用戶代理、Cookie等信息;
- 請求正文:就是HTTP請求的數據。
注:請求方式Method一般有GET、POST、PUT、DELETE,含義分別是獲取、修改、上傳、刪除,其中GET方式僅僅為獲取服務器資源,方式較為簡單,因此在請求方式為GET的HTTP請求數據中,請求正文部分可以省略,直接將想要獲取的資源添加到URL中。
響應
服務器收到了客戶端發來的HTTP請求后,根據HTTP請求中的動作要求,服務端做出具體的動作,將結果回應給客戶端,稱為HTTP響應。HTTP響應由三部分組成:
- 狀態行:包括協議版本Version、狀態碼Status Code、回應短語;
- 響應頭:包括搭建服務器的軟件,發送響應的時間,回應數據的格式等信息;
- 響應正文:就是響應的具體數據。
主要關心並且能夠在客戶端瀏覽器看得到的是三位數的狀態碼,不同的狀態碼代表不同的含義。
響應模型
服務器收到HTTP請求之后,會有多種方法響應這個請求,下面是HTTP響應的四種模型:
- 單進程I/O模型:服務端開啟一個進程,一個進程僅能處理一個請求,並且對請求順序處理;
- 多進程I/O模型:服務端並行開啟多個進程,同樣的一個進程只能處理一個請求,這樣服務端就可以同時處理多個請求;
- 復用I/O模型:服務端開啟一個進程,但是同時開啟多個線程,一個線程響應一個請求,同樣可以達到同時處理多個請求,線程間並發執行;
- 復用多線程I/O模型:服務端並行開啟多個進程,同時每個進程開啟多個線程,這樣服務端可以同時處理進程數M*每個進程的線程數N個請求。
報文格式
HTTP報文是HTTP應用程序之間傳輸的數據塊,HTTP報文分為HTTP請求報文和HTTP響應報文,但是無論哪種報文,他的整體格式是類似的,大致都是由起始、首部、主體三部分組成,起始說明報文的動作,首部說明報文的屬性,主體則是報文的數據。
請求報文
響應報文
優化方案
- TCP復用:TCP連接復用是將多個客戶端的HTTP請求復用到一個服務器端TCP連接上,而HTTP復用則是一個客戶端的多個HTTP請求通過一個TCP連接進行處理。前者是負載均衡設備的獨特功能;而后者是HTTP 1.1協議所支持的新功能,目前被大多數瀏覽器所支持;
- 內容緩存:將經常用到的內容進行緩存起來,那么客戶端就可以直接在內存中獲取相應的數據了;
- 壓縮:將文本數據進行壓縮,減少帶寬;
- SSL加速(SSL Acceleration):使用SSL協議對HTTP協議進行加密,在通道內加密並加速;
- TCP緩沖:通過采用TCP緩沖技術,可以提高服務器端響應時間和處理效率,減少由於通信鏈路問題給服務器造成的連接負擔。
HTTPS協議
原理
- 首先HTTP請求服務端生成證書,客戶端對證書的有效期、合法性、域名是否與請求的域名一致、證書的公鑰(RSA加密)等進行校驗;
- 客戶端如果校驗通過后,就根據證書的公鑰的有效, 生成隨機數,隨機數使用公鑰進行加密(RSA加密);
- 消息體產生的后,對它的摘要進行MD5(或者SHA1)算法加密,此時就得到了RSA簽名;
- 發送給服務端,此時只有服務端(RSA私鑰)能解密;
- 解密得到的隨機數,再用AES加密,作為密鑰(此時的密鑰只有客戶端和服務端知道)。
特點
- 內容加密:采用混合加密技術,中間者無法直接查看明文內容;
- 驗證身份:通過證書認證客戶端訪問的是自己的服務器;
- 保護數據完整性:防止傳輸的內容被中間人冒充或者篡改。
HTTP和HTTPS的區別
- HTTP的URL 以http:// 開頭,而HTTPS 的URL 以https:// 開頭;
- HTTP是不安全的,而HTTPS是安全的;
- HTTP標准端口是80而HTTPS的標准端口是443;
- 在OSI網絡模型中,HTTP工作於應用層,而HTTPS 的安全傳輸機制工作在傳輸層;
- HTTP 無法加密,而HTTPS 對傳輸的數據進行加密;
- HTTP無需證書,而HTTPS 需要CA機構wosign的頒發的SSL證書。