程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 實現Java中的高性能解析器

實現Java中的高性能解析器

編輯:關於JAVA

在某些情況下,你可能需要在Java中實現你自己的數據或語言解析器,也許是這種數據格式或語言缺乏標准的Java或開源解析器可以使用。或者雖然有現成的解析器實現,但它們要麼太慢,要麼太占內存,要麼就是沒有符合你所需要的特性。又或者是某個開源的解析器存在缺陷,要麼是某個開源解析器的項目中止了,原因不一而足。不過無論原因是什麼,總之事實就是你必須要自己去實現這個解析器。

當你必須自己實現一個解析器時,你對它的期望會有很多,包括性能良好、靈活、特性豐富、方便使用,以及便於維護等等。說到底,這也是你自己的代碼。在本文中,我將為你介紹在Java中實現高性能解析器的一種方式,這種方法並且獨一無二,但難度適中,不僅實現了高性能,而且它的模塊化設計方式也比較合理。這種設計是受到了VTD-XML的設計方式的啟發,後者是我所見過的最快的Java XML解析器,比起StAX和SAX這兩種標准的Java XML解析器都要快上許多。

兩種基本的解析器類型

為解析器進行分類的方式有好幾種,在這裡我將解析器分為兩種基礎類型:

順序訪問解析器

隨機訪問解析器

順序訪問是指解析器對進行數據進行解析,在數據解析完成後將其轉交給數據處理器(processor)的過程。數據處理器只能訪問當前正在進行解析的數據,它既不能訪問已解析過的數據,也不能訪問等待解析的數據。這種解析器也被稱為基於事件的解析器,例如SAX和StAX解析器。

而隨機訪問解析器是指解析器允許數據處理代碼可以隨意訪問正在進行解析的數據之前和之後的任意數據(隨機訪問)。這種解析器的例子有XML DOM解析器。

下圖展示了順序訪問解析器與隨機訪問解析器的不同之處:

順序訪問解析器只能讓你訪問當前正在解析的“視窗”或“事件”,而隨機訪問解析器允許你任意地浏覽所有已解析數據。

設計概況

我在這裡所介紹的解析器設計屬於隨機訪問解析器。

隨機訪問解析器的實現通常會慢於順序訪問解析器,因為它們一般都會為已解析數據創建某種對象樹,數據處理代碼將通過這棵樹對數據進行訪問。創建這種對象樹不僅要花費較長的CPU時間,消耗的內存也很大。

相對於從已解析數據中創建一棵對象樹的方式,另一種性能更佳的方式是為原來的數據緩沖區建立一個對應的索引緩沖區,這些索引會指向在已解析數據中找到的元素的起點與終點。數據處理代碼此時不再通過對象樹訪問數據,而是直接在包括了原始數據的緩沖區中訪問已解析數據。以下是對這兩種處理方式的圖示:

由於我找不到一個更好的名字,因此我將這種方式簡單地命名為“索引覆蓋解析器”(Index Overlay Parser)。該解析器為原始數據創建了一個覆蓋於其上的索引。這種方式讓人聯想起數據庫索引將數據保存在磁盤的方式,它為原始的、未處理的數據創建了一個索引,以實現更快地浏覽和搜索數據的目的。

如同我之前所說的,這種設計方式是受到了VTD-XML(VTD是指虛擬令牌描述符)的啟發,因此你也可以把這種解析器稱為虛擬令牌描述符解析器。但我還是傾向於索引覆蓋這個名字,因為它表現了虛擬令牌描述符的本質,即對原始數據建立的索引。

解析器設計概要

一種常規的解析器設計方式將解析過程分為兩步。第一步是將數據分解為內聚的令牌,一個令牌是已解析數據中的一個或多個字節或字符。第二步是對令牌進行解釋,並根據這些令牌構建更大的元素。以下是這兩個步驟的圖示:

這裡的元素並不一定是指XML元素(雖然XML元素也是解析器元素),而是指構成解析數據的更大的“數據元素”。比如說,在一個XML文檔中元素代表了XML元素,而在一個JSON文檔中元素則代表了JSON對象,等等。

舉例來說,<myelement>這個字符串可以被分解為以下幾個令牌:

<

myelement

>

一旦數據被分解為令牌,解析器就能夠相對容易地了解它的意義,並且決定這些令牌構成的更大的元素。解析器就能夠理解一個XML元素是由一個’<’令牌開始,隨後是一個字符串(即元素名稱),隨後有可能是一些屬性,最後以一個’>’令牌結尾。

索引覆蓋解析器設計

在這種解析器的設計方式中也包含了兩個步驟:輸入數據首先被一個令牌生成器(tokenizer)組件分解為令牌,解析器隨後將對令牌進行解析,以決定輸入數據的一個更大的元素邊界。

你也可以為解析過程加入一個可選的“元素浏覽步驟”。如果解析器從解析數據中構建出一棵對象樹,它通常會包含在整棵樹中進行浏覽的鏈接。如果我們不選擇對象樹,而是構建出一個元素索引緩沖區,我們也許需要另一個組件以幫助數據處理代碼在元素索引緩沖區中進行浏覽。

以下是我們的解析器設計的概要:

我們首先將所有數據讀入一個數據緩沖區中,為了能夠通過在解析過程中創建的索引對原始數據進行隨機訪問,所有的原始數據必須已經存在於內存中。

第二步,令牌生成器會將數據分解為令牌。令牌生成器內部的某個令牌緩沖區會將該令牌的起點索引、終點索引和令牌類型都保留下來。使用令牌緩沖區使你能夠查找之前或之後的令牌,在這種設計中解析器會利用到這一項特性。

第三步,解析器獲取了令牌生成器所產生的令牌,根據上下文對其進行驗證,並決定它所表示的元素。隨後解析器會根據從令牌生成器處獲取的令牌構建一個元素索引(即索引覆蓋)。解析器會從令牌生成器中一個接一個地獲取令牌。因此令牌生成器不必立即將所有數據都分解為令牌,它只需要每次找到一個令牌就行了。

數據處理代碼將浏覽整個元素緩沖區,利用它訪問原始數據。你也可以選擇用一個元素浏覽組件將元素緩沖區包裝起來,使浏覽元素緩沖區的工作更加簡單。

這種設計不會從解析數據中生成一棵對象樹,但它確實生成了一個可浏覽的結構,即元素緩沖區,索引(即整數數組)將指向包含了原始數據的數據緩沖區。你可以使用這些索引浏覽原始數據緩沖區中的所有數據。

本文的以下部分將分析這種設計的各方面細節。

數據緩沖區

數據緩沖區是一個包括了原始數據的字節字符緩沖區,而令牌緩沖區和元素緩沖區則包含了指向數據緩沖區的索引。

為了實現對解析數據的隨機訪問,必須以某種形式將它保留在內存中。我們在這裡沒有選擇對象樹,而是選擇了包含未處理數據本身的數據緩沖區。

將所有數據全部保留在內存中可能會導致對內存的大量消耗。如果你的數據包含了互相獨立的元素,例如日志記錄,那麼將整個日志文件導入內存很可能會造成崩潰。你應該采取的方式是只導入日志文件的一部分,其中至少包含一條完整的日志記錄。由於每一條日志記錄都可以不依賴於其它日志記錄進行解析和處理,你就不需要將整個日志文件在同一時刻加載到內存裡了。我在我的文章《使用緩沖區對流進行迭代處理》中描述了如何對一塊數據流進行迭代的方式。

令牌生成器與令牌緩沖區

令牌生成器將數據緩沖區分解為令牌,令牌的信息會保存在令牌緩沖區中,包括以下信息:

令牌的位置(起始位置的索引)

令牌長度

令牌類型(可選信息)

以上信息都保存在數組中,這裡是一段示例代碼:

   public class IndexBuffer {
       public int[]  position = null;
       public int[]  length   = null;
       public byte[] type     = null; 
     /* assuming a max of 256 types (1 byte / type) */
   }

當令牌生成器在數據緩沖區中找到令牌之後,它會將該位置(起始位置的索引)插入position數組、將令牌長度插入length數組,並將令牌類型插入type數組。

如果你不使用這個可選的令牌類型數組,你也可以在需要的時候通過令牌中的數據得出令牌的類型。這是一種性能與內存占用之間的權衡。

解析器

解析器本質上與令牌生成器非常類似,不同的是它將令牌作為輸入,而將元素索引作為輸出。和令牌類似,每個元素由它的位置(起始位置的索引)、長度和可選的元素類型幾部分組成。用以保存這些數字的結構與保存令牌的結構是完全一樣的。

在這裡type數組仍然是可選的。如果你能夠從元素的首個字節或字符中很容易地判斷元素的類型,那就無需特意保存元素的類型信息。

在元素緩沖區中所包含的元素的精確粒度取決於被解析的數據,以及之後將對數據進行處理的代碼段。舉例來說,如果你要實現一個XML解析器,你可能會選擇將每個開始標簽、屬性和結束標簽作為獨立的“解析元素”。

元素緩沖區(索引)

解析器所生成的元素緩沖區包含了引向原始數據的索引。這些索引會記錄解析器在數據中所找到的元素的位置(起始位置的索引)、長度和類型信息。你可以利用這些索引實現在原始數據的任意浏覽。

從之前的IndexBuffer代碼段中,你可以看到元素緩沖區為每個元素保留了9個字節的緩沖區,4個字節用於保存位置、另4個字節用於保存令牌長度,最後1個字節用於保存令牌類型。

你或許能夠通過某些手段來減少IndexBuffer的內存占用。舉例來說,如果你確認其中的元素不超過65535個字節,你就可以選擇使用short短整數,而不是常規的int整數來保存令牌長度信息,這樣每個元素都可以節省兩個字節,將整個內存占用減少至每個元素七個字節。

此外,如果你確認被解析文件的大小不會超過16,777,216個字節,那你只需要三個字節來保存位置信息(起始位置的索引)。那麼在position數組中的每個整數的第四個字節就可以用來保存元素類型,這樣就可以完全不用使用單獨的type數組了。如果你的令牌類型不超過128種,你就可以使用七個字節、而不是八個字節來保存令牌類型,這樣一來你就可以使用25個比特來保存位置,使得最大的位置可以達到33,554,432。如果你的令牌類型少於64種,你還可以空出一個比特以保存位置信息。

VTD-XML實際上將所有這些信息都保存在一個長整數類型中,以達到節省空間的目的。為了將幾個分離的字段加載成為一個單獨的整數或者長整數,需要進行一些比特操作,也因此會降低一些速度,但好處是節省了部分內存,這就是一種資源的權衡。

元素Navigator

元素navigator可以幫助處理數據的代碼在元素緩沖區中對數據任意浏覽。請記住一個語義化的對象或元素(例如一個XML元素)或許會包含多個解析器元素。為了簡化浏覽的實現,你可以創建一個元素navigator對象,讓它負責在語義化對象級別對解析器元素進行浏覽的操作。舉例來說,XML元素navigator可以通過在開始標簽之間跳轉的方式實現對元素緩沖區的浏覽。

是否使用元素navigator組件由你自行選擇,如果你只需要為某個單一的項目的某一個功能實現解析器,你也可以選擇不使用這種方式。但如果你希望實現的解析器能夠在多個項目中重用,或者是將它發布為開源代碼,你或許需要添加一個元素navigator組件,這取決於對解析數據的浏覽的復雜度有多高。

案例學習:一個JSON解析器

為了讓索引覆蓋解析器的設計更為直觀,我自己實現了一個基於Java的小型JSON解析器,它遵循了索引覆蓋解析器設計的方式,你可以在GitHub上找到它的完整代碼。

JSON是JavaScript對象表示法的簡稱,它是在web服務端和客戶端浏覽器之間通過AJAX進行數據交換的一種常見數據格式,這是因為web浏覽器內置了將JSON轉換為JavaScript對象的原生支持。以下篇章中我會假設你已經熟悉JSON格式了。

這裡有一個簡單的JSON示例:

  {"key1":"value1","key2":"value2",["valueA":"valueB":"valueC"]}

JSON令牌生成器將JSON字符串分別為以下令牌:

這裡的下劃線強調了每個令牌的長度。

令牌生成器還將決定每個令牌的基本類型,以下的JSON示例與之前的相同,只是加入了令牌的類型信息:

請注意這裡的令牌類型並非語義化,它只是說明了令牌的基本類型是什麼,而並沒有體現出這些令牌包含了什麼內容。

解析器會分析出基本的令牌類型,並將它們替換為語義化的類型。這裡是一個相同的JSON示例,但使用了語義化的類型(即解析器元素):

當解析器完成了對該JSON對象的解析之後,你將獲得一個索引(即元素緩沖區),它由圖中所標注的元素的位置、長度和元素類型信息所組成。接下來你就可以對該索引進行浏覽,以找出該JSON對象中你所需的數據。

JsonTokenizer.parseToken()

為了讓你了解令牌生成器和解析工作是如何實現的,我會為你展示JsonTokenizer和JsonParser中的核心代碼。請記得去Github下載完整的代碼。

以下是JsonTokenizer.parseToken()方法的實現,它將負責解析數據緩沖區中的下一個令牌:

public void parseToken() {
     skipWhiteSpace();
     this.tokenLength = 0;
	
     this.tokenBuffer.position[this.tokenIndex] = this.dataPosition;
     char nextChar = this.dataBuffer.data[this.dataPosition];
	
     switch(nextChar) {
	 case '{'   :
           this.tokenLength = 1;
           this.tokenBuffer.type[this.tokenIndex] = 
TokenTypes.JSON_CURLY_BRACKET_LEFT;
           break;
	 case '}'   :  
           this.tokenLength = 1;
           this.tokenBuffer.type[this.tokenIndex] = 
TokenTypes.JSON_CURLY_BRACKET_RIGHT;
           break;
	 case '['   :
           this.tokenLength = 1;
           this.tokenBuffer.type[this.tokenIndex] = 
TokenTypes.JSON_SQUARE_BRACKET_LEFT ;
           break;
	 case ']'   : 
           this.tokenLength = 1;
           this.tokenBuffer.type[this.tokenIndex] = 
TokenTypes.JSON_SQUARE_BRACKET_RIGHT;
           break;
	 case ','   :
           this.tokenLength = 1;
           this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_COMMA;
           break;
	 case ':'   :  
           this.tokenLength = 1;
           this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_COLON;
           break;
	 case '"'   :
           parseStringToken();
           this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_STRING_TOKEN;
           break;
	 default    : 
           parseStringToken();
           this.tokenBuffer.type[this.tokenIndex] = TokenTypes.JSON_STRING_TOKEN;
     }
     this.tokenBuffer.length[this.tokenIndex] = this.tokenLength;
  }

如你所見,這部分代碼非常簡單。我們第一步首先調用skipWhiteSpace()方法,它將忽略當前位置的數據中的空格字符。第二步是將令牌長度設為0。第三步,將當前令牌的位置(數據緩沖區中的相對位置)保存在TokenBuffer中。第四步,對下一個字符進行分析,根據字符種類(即令牌種類)的不同,將執行switch—case結構中的某條語句。最後,將當前令牌的長度保存起來。

以上就是為數據緩沖區生成令牌的全部工作了,請注意,當找到了某個字符串令牌的開頭部分之後,令牌生成器就會調用parseStringToken()方法,它會對數據進行完整的掃描,直到找到了該字符串令牌的結束為止。這種方式比起在parseToken()方法中進行各種條件判斷並處理各種不同情況執行得會更快,而且實現也更加容易。

JsonTokenizer中其余的方法都是parseToken()的輔助方法,或者是將數據的位置移至下一個令牌(即當前令牌之後的第一個位置),等等。

JsonParser.parseObject()

JsonParser類的主要方法是parseObject(),它會檢查JsonTokenizer中令牌的類型,並嘗試在輸入數據中查找該類型的JSON對象。

以下是parseObject()方法的實現:

private void parseObject(JsonTokenizer tokenizer) {
     assertHasMoreTokens(tokenizer);
     tokenizer.parseToken();
     assertThisTokenType(tokenizer, TokenTypes.JSON_CURLY_BRACKET_LEFT);
     setElementData     (tokenizer, ElementTypes.JSON_OBJECT_START);
     tokenizer.nextToken();
     tokenizer.parseToken();
     while( tokenizer.tokenType() != TokenTypes.JSON_CURLY_BRACKET_RIGHT) {                 
         assertThisTokenType(tokenizer, TokenTypes.JSON_STRING_TOKEN);
	 setElementData(tokenizer, ElementTypes.JSON_PROPERTY_NAME);          
         tokenizer.nextToken();
	 tokenizer.parseToken();
	 assertThisTokenType(tokenizer, TokenTypes.JSON_COLON);
	 tokenizer.nextToken();
	 tokenizer.parseToken();
	 if(tokenizer.tokenType() == TokenTypes.JSON_STRING_TOKEN) {             
             setElementData(tokenizer, ElementTypes.JSON_PROPERTY_VALUE);
	 } else if(tokenizer.tokenType() == TokenTypes.JSON_SQUARE_BRACKET_LEFT) {
	     parseArray(tokenizer);
	 }
         tokenizer.nextToken();
	 tokenizer.parseToken();
	 if(tokenizer.tokenType() == TokenTypes.JSON_COMMA) {
	     tokenizer.nextToken();  //skip , tokens if found here.             
             tokenizer.parseToken();
	 }
      }
      setElementData(tokenizer, ElementTypes.JSON_OBJECT_END);
  }

  private void setElementData(JsonTokenizer tokenizer, byte elementType) {
     this.elementBuffer.position[this.elementIndex] = tokenizer.tokenPosition();     
     this.elementBuffer.length  [this.elementIndex] = tokenizer.tokenLength();         
     this.elementBuffer.type    [this.elementIndex] = elementType;      
     this.elementIndex++;
  }

parseObject()方法能夠接受的信息包括:一個左大括({)後接著一個字符串令牌;或是一個逗號後跟著一個字符串令牌;或是某個數組的開始符號([);或是另一個JSON對象。當JsonParser從JsonTokenizer中獲得了這些令牌之後,就將它們的開始位置、長度和語義信息保存在它自己的elementBuffer字段中。數據處理代碼就可以隨後浏覽elementBuffer中的信息,從輸入數據中獲取所需的數據了。

看過了JsonTokenizer和JsonParser的核心代碼部分之後,你應該對令牌生成器和解析器的工作方式有所了解了。如果要完整地了解代碼的工作方式,你可能需要查看JsonTokenizer和JsonParser的完整實現。它們的代碼都不超過115行,理解它們應該不是難事。

查看本欄目

性能基准測試

VTD-XML已經為它的XML解析器與StAX、SAX和DOM解析器進行過大量的性能基准比較測試了,從性能上來看VTD-XML無疑是最大的贏家。

為了讓使用者對索引覆蓋解析器的性能建立起信心,我也對我的JSON解析器實現與Google的JSON解析器——GSON,進行了性能對比。GSON的方式是從某個JSON輸入(字符串或流)中創建一棵對象樹。

請記住,GSON是一個非常成熟的產品,品質優秀,經過了大量的測試,並且接受用戶的錯誤報告。而我的JSON解析器還只是處於概念產品的級別。這次測試僅僅是對性能的表現,這個結果也不代表最終的結論。也請注意閱讀該測試的相關討論。

這裡有一些關於構建該測試的具體細節:

為了使JIT預熱以減少啟動時的負載,對該JSON的輸入解析一共運行了1千萬次。

該測試一共對三個不同的文件重復運行了相同的次數,以測試解析器解析小文件、中等文件和大文件的效果。文件的大小分別為64字節、406字節和1012字節。因此測試的過程就是首先對小文件進行1千萬次解析,並分析其結果,然後解析中等文件並分析結果,最後是解析大文件並分析結果。

在解析和分析工作開始前,文件已經全部加載到內存中,因此避免了將文件加載的時間算到整個解析時間裡。

對1千萬次解析的分析過程會在自己的進程中進行,這意味著每個文件都在獨立的進程中進行解析,在每個時間點只有一個文件在進行解析。

每個文件會進行3次分析,因此對文件的1千萬次解析工作一共會進行3次,每1次的分析工作是順序進行的,而沒有采用並行方式。

測試結果表格包括以下三列:

原始數據緩沖區的迭代數目

JSON解析器

GSON

第一列中的內容是原始數據緩沖區中的所有數據的迭代數目,這個數字僅僅是用以表示極限的最小時間,即理論上處理所有這些數據的最小時間。當然不可能有任何解析器能夠達到這一速度,不過這個數字能夠起到參照作用,以顯示出解析器和原始迭代速度的差距。第二列中顯示了我的JSON解析器的運行時間,第三列則是Google的GSON解析器的運行時間。

以下數據是對三個文件(64字節、406字節、1012字節)各運行1千萬次解析所需的毫秒數:

如你所見,索引覆蓋的實現比起GSON(一種對象JSON解析器)要快得多。雖然結果在預計之中,不過你現在能夠了解到它們的性能差距到底有多大了。

值得注意的一點是,在測試進程的執行過程中,內存占用的指標一直非常穩定。盡管GSON創建了大量的對象樹,但它的內存占用並沒有瘋狂地增長。而索引覆蓋方式的內存占用也非常穩定,比起GSON還要小了1兆左右,這有可能是因為加載到JVM中的GSON代碼庫較大的緣故。

關於測試結果

如果我們只是簡單地說對一個為數據創建對象樹的解析器(GSON)和一個標記出數據中所找到的元素位置的解析器進行比較,這種說法有欠公平。我們還需要分析一下具體比較了哪些內容。

在一個運行中的應用程序對文件進行解析通常包含以下步驟:

首先從磁盤或者網絡上加載數據,然後對數據進行解析,最後進行數據處理。

為了准確測量數據解析部分的速度,我將被解析的文件預先加載入內存中,並且測試代碼對數據完全不做任何處理。這種方式雖然測量了純粹的解析速度,但這一性能差別並不能代表在實際運行中的應用程序一定會獲得更好的性能,原因如下:

一個流解析器通常能夠在所有數據加載到內存之前就開始解析正在加載中的數據,而我的JSON解析器目前還沒有實現這一功能,這意味著雖然它在單純的解析速度上要快上一籌,但運用在實際運行中的應用程序上時,由於它必須等待所有數據加載完成,因此真實的完成速度不一定會更快。下圖就表現了這一過程:

為了加快整體的解析速度,你也可以對我的解析器進行一些修改,讓它能夠邊加載數據邊進行解析,不過這樣做也許會稍稍降低單純的解析性能。當然,最終的運行速度或者還是得到一些提升。

與上面的情況類似的是,我的JSON解析器對已解析的數據也沒有進行任何處理。如果你需要從大量的已解析數據中抽取字符串,那麼GSON已經為你的需求做好了准備工作,因為它已經為已解析數據創建了一棵對象樹。下圖就表現了這一過程:

如果你打算使用GSON,那麼它或許已經為你實現了在數據處理中所需的數據抽取過程,如果整個數據處理過程可以省略數據抽取(例如抽取為字符串)這一步驟,那麼它的整體速度還要再快一點。

因此,為了准確地測量解析器對你的應用程序的影響,你必須將不同的解析器在你的應用程序中的表現進行測量。我仍然確信使用索引覆蓋解析器的速度要更快,但具體有多少差距還不好說。

對索引覆蓋解析器的總體討論

我經常聽到一種關於索引覆蓋解析器的爭論,這種說法認為由於索引覆蓋解析器為了實現對原始數據的索引,而不是將原始數據抽取為對象樹,它在解析時必須將所有數據讀入內存中,這種方式在解析大文件時會對內存產生很大的負擔。

這種說法其實就是表明了流解析器(例如SAX或StAX)能夠解析巨大的文件,而不需要將整個文件讀入內存中。但這種說法成立的前提是,該文件中的數據可以分為多個小塊進行解析與處理,而且每個小塊可以獨立地被解析與處理。舉例來說,一個大XML文件包含了一系列的元素,每個元素都可以進行獨立的解析和處理(類似於一個日志記錄集合)。但如果你的數據可以以獨立的小塊進行分別解析的話,那麼你也完全可以實現一個能夠做到這一點的索引覆蓋解析器。

而如果該文件不能夠分解為多個獨立的小塊進行解析的話,那無論如何你必須將信息加載到某種結構中,以便代碼在處理之後的小塊時訪問這一部分信息。而如果你能夠在流解析器中做到這一點的話,那麼也同樣可以在一個索引覆蓋解析器做到這一點。

那些為輸入數據創建對象樹的解析器往往會占用更大的內存,因為對象樹的內存占用會超過原始數據的尺寸。其原因在於不僅每個對象實例會占用內在,而且對象之間的引用也占用了一部分內存數據。

此外,由於所有數據必須一次性全部加載到內存中,因此你需要預先為數據緩沖區預留足以保存全部數據的空間。但如果在開始解析某個文件的數據時,你還不知道整個文件的大小,又該怎麼做呢?

假設你有一個允許用戶上傳文件的web應用程序(或者是web service,或其它類型的服務端應用程序),你很難判斷這些文件會有多大,那又如何能夠在開始解析之前為它們分配足夠大小的緩沖區呢?當然,出於安全性的考慮,你應該設定一個允許上傳文件的最大尺寸,否則用戶可以通過上傳超大文件使你的系統完全崩潰,或者編寫一段程序以模擬浏覽器上傳文件的操作,讓這段程序不停地向你的服務器發送數據。你可以考慮為緩沖區分配與允許上傳文件的最大尺寸相同的值,這樣可以保證你的緩沖區對於有效的上傳不會占用所有的內存。如果緩沖的尺寸真的過大,那一定是因為你的用戶上傳了超大的文件。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved