(接上文《架構設計:系統存儲(20)——圖片服務器:需求和技術選型(2)》)java
以前的兩篇文章介紹了圖片系統的技術組件選型和技術方案設計,從這篇文章開始咱們將搭建工程進行詳細的編碼開發和效果測試。這裏要說明一下,因爲文章篇幅的限制不可能貼出全部的代碼,這樣也不符合讀者的閱讀習慣。因此筆者的辦法是,只經過後續的文章內容介紹詳細的設計要點和代碼片斷,經過這些講解讀者基本能夠清楚整個詳細設計的思路,而整個圖片服務工程代碼會上傳到了CSDN的下載區(http://download.csdn.net/detail/yinwenjie/9740380),若是對工程感興趣那麼讀者能夠直接下載——免費下載。 web
因爲咱們將要進行的是圖片處理操做,而圖片處理比起讀者常常涉及的業務系統來講是一個比較生澀的領域,在通常的業務系統中也就只是須要作到上傳或者下載/顯示圖片就OK了。而圖片服務是專門進行圖片處理的,簡單的處理包括透明度、旋轉、縮放、翻轉、重合等,複雜的處理還有背景虛化、人臉識別等等。因此在正式進入編碼前,本文有必要向讀者介紹一些基本的圖片知識,特別是圖片顏色模型的知識。固然,若是讀者自己就有很豐富的圖片處理知識了,則能夠直接跳過本節的介紹。算法
目前在互聯網上使用最多的圖片都屬於位圖,例如JPG、GIF、PNG和BMP圖片都屬於位圖。它們的相同點在於都依靠RGB色彩模式描述圖片中的每個點,從而構成一張圖片。它們的不一樣點則體如今文件頭結構、壓縮算法、掃描方式以及像素深度支持等方面。舉個例子,BMP(bitmap)圖片使用一種無損壓縮算法可以保證還原全部像素點,可是圖片大小過大,因此相同像素規模的圖片若是使用BMP格式,須要的存儲容量就更大,並且不利於進行網絡傳輸。再例如,PNG圖片除了能夠存儲24位深,還能夠存儲額外8位甚至16位的Alpha通道描述來表示每一個像素點的透明度,可是JPG圖片卻沒有這個特性,它只支持24位深度。數組
那麼什麼是GRB呢?這些圖片格式的每個點都由三個基礎色進行描述: 紅(Red)、綠(Green)、藍(Blue),經過三原色之間的比例就能夠調製出不一樣的顏色,將必定規模的顏色點組合起來就是一張圖片了。通常來講計算機爲每個原色準備了8位bit進行描述(就是1個byte),三原色就是24位bit,而且大多數圖片的顏色深度就是24位。緩存
這樣看來24位顏色深度的一個圖片點,最多能夠記錄256 * 256 * 256 = 16777216種顏色。那麼相似PNG圖片使用的32位/48位顏色深度有表明什麼意思呢?多出來的8位/16位爲了記錄這個點的可見度(透明度),這個記錄範圍的數值又稱爲Alpha通道。這樣同一個顏色配合不一樣的可見度就能夠知足更豐富顏色展現要求。另外,這也是JPG文件不能支持透明度的緣由——它的格式規範中不帶有對Alpha通道的支持。服務器
另外因爲一些圖片類型也更淺的位深,因此一些圖片爲了減小極端狀況下的使用/傳輸空間,也會使用16位/8位的RGB描述模式。例如使用16位RGB模式時,紅色位描述爲5個bit、綠色位描述爲6bit、藍色位描述爲5bit。在Java原生的圖片處理模塊中,使用TYPE_USHORT_565_RGB、TYPE_USHORT_555_RGB標識對16位RGB模式進行描述。網絡
JAVA模塊中帶有的圖片處理功能(Java Image I/O API),可以支持對JPG、PNG、BMP、WBMP、GIF格式的文件進行讀寫操做,並且能夠支持到單個像素點級別的操做——也就是說讀者能夠經過這套API對具體的某一個RBG描述信息進行操做。經過這套API要完成在圖片上添加形狀、縮放圖片、裁剪圖片等操做也很是簡單,只須要幾行代碼。首先咱們來看一下Java Image I/O API中會使用的幾個概念,以便後續進行代碼編寫:多線程
從這個類的直觀名稱能夠理解成被緩存的圖片信息,它將一張圖片通過格式分析後,放置在內存中的一個可訪問區域。開發熱源能夠從這個可訪問區域提取到不少關於這張圖片有用的信息,例如能夠取得ColorModel表示的顏色份量,裏面包括了每個像素的RGB信息和Alpha信息;還能夠取得Raster表示的像素矩陣,經過它能夠讀取一個範圍內的像素點。開發人員對這個區域進行讀寫操做,實際上就是對這張圖片進行像素級別操做。如下代碼能夠加載一張位圖到BufferedImage中:架構
...... BufferedImage srcImage = javax.imageio.ImageIO.read(new File("mmexport1444022819048.jpg")); ......
須要注意的是,BufferedImage一旦被被加載其像素規模就不能改變了,例如您最初加載了一個800 * 600的JPG文件到BufferedImage中,在操做過程當中您就不能將這個BufferedImage縮小成600 * 400的。若是要這樣作,您只能建立一個新的BufferedImage,並將進行縮放後的計算結果加載到這個新的BufferedImage中。如下的方式能夠建立一個空的BufferedImage:併發
...... // 如下代碼建立一個600 * 400的BufferedImage BufferedImage outputImage = new BufferedImage(600, 400, BufferedImage.TYPE_INT_RGB); ......
注意最有一個參數BufferedImage.TYPE_INT_RGB,它指定了BufferedImage須要支持的RGB規則。TYPE_INT_RGB表示使用一個int數值表示24位深度的RGB規則,TYPE_USHORT_565_RGB表示,使用一個short數值,表示16位深度的RGB規則,其中紅色佔5位,綠色佔6位,藍色佔5位。再例如TYPE_INT_ARGB和TYPE_4BYTE_ABGR分別表示以int數值或一個4位的byte數組表示ARGB規則,換句話說就是這個BufferedImage支持圖片透明度的表示。
最直觀的理解就是,BufferedImage至關於在內存區域中的畫布,這個畫布上能夠有一張或者多張圖片,也能夠沒有任何圖片。你能夠在畫布上對每一個像素進行讀寫操做,可是你不能改變畫布的大小。
Graphics類抽象一點說是進行圖形操做的上下文控制的類,具體一點說是圖形畫筆工具。經過這個類(以及它的子類)開發人員能夠方便的在畫布上繪製不一樣的規則形狀、圖片或者文字。
上圖中呈現的Graphics子類結構能夠支持不一樣的繪圖場景,例如Graphics2D類基本上能夠處理全部主流2維位圖的畫布繪製,在咱們的圖片服務系統中使用最多的畫筆類也是它。如下代碼能夠在畫布上繪製一個規則的矩形,並填充顏色:
...... // 建立一個100 * 80 的畫布 BufferedImage outputImage = new BufferedImage(100, 80, BufferedImage.TYPE_INT_RGB); // 得到這個畫布的畫筆,也可使用createGraphics建立畫筆 Graphics graphics = outputImage.getGraphics(); // 從畫布上10,10的座標開始,繪製一個60 * 40的矩形 graphics.setColor(Color.RED); graphics.drawRect(10, 10, 60, 40); // 處理 graphics.dispose(); ......
輸出到文件,就能夠看到如如下所示的效果了。
紅色矩形在畫布上被繪製出來,但爲何初始化的畫布是黑色呢?這是由於咱們使用的BufferedImage構造方法將使用RGB=0000 0000 0000 0000 0000 0000的數值進行每一個像素點的初始化,實際上就是黑色的RGB值。咱們換一種方式進行BufferedImage的初始化,就能夠將BufferedImage中的像素點初始化成白色:
......
int width = 100, height = 80;
int size = width * height;
int[] pixels = new int[size];
// 如今設置每個像素點的RGB值爲白色(整數的表示就是16777215,16進制的表示就是FFFFFF)
for(int index = 0 ; index < size ; index++) {
pixels[index] = 0xFFFFFF;
}
// size就是像素規模大小
DataBuffer dataBuffer = new DataBufferInt(pixels, size);
// 初始化的Raster類,就是像素矩形數組的封裝
WritableRaster raster = Raster.createPackedRaster(dataBuffer, width, height, width, new int [] { 0xFF0000, 0xFF00, 0xFF }, null );
DirectColorModel directColorModel = new DirectColorModel(24, 0xFF0000, 0xFF00, 0xFF);
// 生成 BufferedImage, 這樣BufferedImage中的每一個像素就是白色了
BufferedImage outputImage = new BufferedImage(directColorModel, raster, true , null );
......
上文多處已經提到,圖片處理操做是計算密集型操做,很是消耗CPU資源和內存資源。而JAVA Image I/O API又是基於java進行的圖片像素級操做,其處理性能自己就不及C/C++。因此對JVM的內存優化就顯得很是重要了。這裏咱們假設讀者已經知道了JVM的基本構造,直接講解JVM的幾個優化注意點:
關於-Xmx最大堆內存:雖然咱們假設的圖片處理場景是一個百萬級PV的中等電商平臺/對C端系統,因此單個圖片服務系統單位時間內須要處理的圖片請求數量也是比較大的,首先建議設置Xmx內存大小在8+GB或者設置內存大小爲操做系統可用內存的60%以上。注意也不能太大了,這要依據您的CPU性能設定,不然就會出如今時不時進行full gc出現明顯卡頓現象——CPU性能不夠形成full gc耗時過長。
關於回收器的選擇:回收器的設置是最關鍵的,咱們知道在早期的JDK版本中提供的回收器,都是採用中斷用戶線程的方式進行,不管是針對新生代的Serial、ParNew仍是針對年老代的SerialOld,都是這樣。但若是在高併發環境下,若是出現用戶線程的停頓現象,就會在很是短的時間內形成大量請求等待,嚴重影響服務器的處理效率。因此對JVM的優化要特別注意這個點,建議採用JDK 1.7+ 64位以上的運行版本,並設置JVM到server模式。
首先是指定年老代使用的回收器,這個推薦使用標記-清除算法的CMS(響應時間優先回收器)就行了,它會在儘可能保證用戶線程運行的狀況下對待回收區域進行屢次標標記回收。還要注意,CMS回收器並不能保證在回收時用戶線程絕對不中止,而是使用兩次短暫的掛起操做取代以前回收器使用的一次較長的掛起操做。另外注意,CMS的線程數量有一個默認值,這個默認值是(cpu內核數量 + 3)/4。雖然這個數量是能夠設置的,可是筆者並不建議本身去設定這個值,而是保證您的操做系統上至少有8個或以上(16個最佳)CPU內和數量(若是達到或超過24核,就要經過-XX:ParallelGCThreads參數控制一下了),-XX:+UseConcMarkSweepGC參數可開啓CMS。
接着是新生代使用的回收器,若是您指定了年老代使用回收器爲CMS,那麼新生代的回收器就不能使用ParallelScavenge回收器了(吞吐量優先回收器),由於兩種回收器不兼容。首先CMS也支持 可使用ParNew回收器,這是一個單線程Serial GC的一個多線程版本,雖然在進行GC時會出現用戶線程掛起的狀況,但因爲它是多線程的版本且咱們存儲的數據特色,決定了ParNew不會出現太大的性能瓶頸。
關於年老代和新生代的比例問題:雖然咱們常說JVM優化的目的減小移動到年老代的對象數量和減小full gc的狀況,但因爲咱們在內存中將要存儲和操做的數據比較特別——圖片文件數據,形象來講是BufferedImage對象、較大的byte[]數組對象。這首先些對象的特色是數據量較大,一張原始圖片小則500KB,大則會達到1MB(不會再大了,由於咱們對上傳文件的大小作了限制)。其次圖片處理速度較慢,一張顏色深度在24位大小在1MB的JPG文件,等比例縮放成一張200KB的圖片耗費的時間在200ms左右,甚至有時會超過500ms(看CPU性能了等客觀因素了)。因此這樣的數據存放存在年輕代區域,就會形成很是頻繁的複製操做和向年老代的移動操做。這樣咱們的優化思路就須要有針對性:減小年輕代的內存空間,並設置一個閾值,在圖片數據超過這個閾值時就直接將對象放入年老代,而後在年老代中由CMS GC進行回收。例如當JVM堆內存數量爲8GB時,能夠經過-Xmn參數設置年輕代(eden+ 2 survivor space)的空間大小爲1GB;經過-XX:PretenureSizeThreshold=<byte size> 參數設置直接進入年老代的對象大小值;經過-XX:CMSInitiatingOccupancyFraction=50參數設定當年老代已使用的內存大小達到50%時,開始CMS GC。
關於持久代的問題:在咱們的圖片服務中,JVM的持久代並不會存儲太多數據,特別是咱們的工程中須要加載IOC容器的class信息並很少,且常量信息也很少的狀況下。何況在JDK Version 1.8+ 的版本中JVM模型已經取消了對持久代的支持。因此持久代並不須要作太多針對性的優化,最後JDK Version 1.8+ 也是本文推薦使用的。
除了JAVA 提供的JAVA Image I/O API之外,還有一些基於JAVA開發的第三方圖形組件,不過若是您搜索Goolge就會發現不少第三方圖形組件已經沒有再維護了,若是各位讀者有興趣能夠進行研究:例如Java Image Filters、JMagick等。
JAVA提供的JAVA Image I/O API 圖片處理工具雖然能夠進行像素級別的操做,可是相對於C/C++提供的圖片處理性能來講仍是較弱,那麼要進行圖形高效運算的語言基礎仍是C/C++爲宜。目前流行的2D和3D圖像處理軟件也可能是基於C/C++構建,例如OpenGL、DirectX等。另外圖形處理都是CPU密集型工做,對計算機的運算資源和內存資源要求都比較高,目前的發展趨勢是使用專門的GPU代替CPU進行運算,這也是爲何不管是咱們使用JAVA Image I/O API仍是基於Nginx的Image模塊爲系統提供簡單的圖片處理功能,對CPU要求都很是高的緣由。
============================= (接下文)