Pack200 と圧縮


この章では、次のトピックについて説明します。

はじめに

サーバやネットワークの利用度および帯域幅を向上させるために、Java アプリケーションやアプレットの配備の際に gzip と Pack200 という 2 つの圧縮形式を利用できるようになりました。

どちらかの技術を使用して圧縮した JAR ファイルをネットワーク上で伝送し、受信側のアプリケーションで圧縮解除して復元します。

理論

HTTP 1.1 (RFC 2616) プロトコルでは、HTTP 圧縮を扱います。HTTP 圧縮により、アプリケーションでは JAR ファイルを圧縮された JAR ファイルのまま配備することができます。サポートされる圧縮技術は gzipcompressdeflate です。

JDK/JRE バージョン 5.0 では、HTTP 圧縮は RFC 2616 に準拠して Java Web Start と Java Plug-in に実装されています。サポートされる技術は gzip および pack200-gzip です。

要求側のアプリケーションは、HTTP 要求をサーバに送信します。HTTP 要求にはフィールドが複数あります。Accept-Encoding (AE) フィールドは pack200-gzip または gzip に設定され、アプリケーションが pack200-gzip または gzip 形式を処理できることをサーバに通知します。

サーバの実装では、ファイル拡張子が .pack.gz または .gz である要求された JAR ファイルを検索し、検索したファイルを返します。サーバは送信しているファイルのタイプに応じて、応答ヘッダの Content-Encoding (CE) フィールドに pack200-gzipgzip、または null を設定し、オプションとして Content-Type (CT) をアプリケーションまたは Java アーカイブとして設定します。したがって、要求側のアプリケーションでは、CE フィールドを検査することで、対応する変換を実行し、元の JAR ファイルに復元します。



この例は、単純なサーブレットまたはサーバモジュールと HTTP 1.1 準拠の Web サーバを使用して実現できます。

ファイルを稼働時に圧縮すると、Pack200 を使用する場合は特にサーバのパフォーマンスが低下するため、推奨されません。

Tomcat サーブレットの例:

/**
*  A simple HTTP Compression Servlet
 */

import java.util.*;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.zip.*;
import java.net.*;

/**
* The servlet class.
 */
public class ContentType extends HttpServlet {

private static final String JNLP_MIME_TYPE          = "application/x-java-jnlp-file";
private static final String JAR_MIME_TYPE           = "application/x-java-archive";
private static final String PACK200_MIME_TYPE       = "application/x-java-pack200";

// HTTP Compression RFC 2616 : Standard headers
public static final String ACCEPT_ENCODING          = "accept-encoding";
public static final String CONTENT_TYPE             = "content-type";
public static final String CONTENT_ENCODING         = "content-encoding";

// HTTP Compression RFC 2616 : Standard header for HTTP/Pack200 Compression
public static final String GZIP_ENCODING            = "gzip";
public static final String PACK200_GZIP_ENCODING    = "pack200-gzip";
       
private void sendHtml(HttpServletResponse response, String s) 
throws IOException {
PrintWriter out = response.getWriter();
         
out.println("<html>");
out.println("<head>");
out.println("<title>ContentType</title>");
out.println("</head>");
out.println("<body>");
out.println(s);
out.println("</body>");
out.println("</html>");
    }

    /* 
* Copy the inputStream to output ,
     */    
private void sendOut(InputStream in, OutputStream ostream) 
throws IOException {
byte buf[] = new byte[8192];

int n = in.read(buf);
while (n > 0 ) {
ostream.write(buf,0,n);
n = in.read(buf);
        }
ostream.close();
in.close();
    }
    
boolean doFile(String name, HttpServletResponse response) {
File f = new File(name);
if (f.exists()) {
getServletContext().log("Found file " + name);

response.setContentLength(Integer.parseInt(
Long.toString(f.length())));

response.setDateHeader("Last-Modified",f.lastModified());
return true;
        }
getServletContext().log("File not found " + name);
return false;
    }
    
    
/** Called when someone accesses the servlet. */
public void doGet(HttpServletRequest request, 
HttpServletResponse response) 
throws IOException, ServletException {
        
String encoding = request.getHeader(ACCEPT_ENCODING);
String pathInfo = request.getPathInfo();
String pathInfoEx = request.getPathTranslated();
String contentType = request.getContentType();
StringBuffer requestURL  = request.getRequestURL();
String requestName = pathInfo; 
        
ServletContext sc = getServletContext();
sc.log("----------------------------");
sc.log("pathInfo="+pathInfo);
sc.log("pathInfoEx="+pathInfoEx);
sc.log("Accept-Encoding="+encoding);
sc.log("Content-Type="+contentType);
sc.log("requestURL="+requestURL);
        
if (pathInfoEx == null) {
response.sendError(response.SC_NOT_FOUND);
return;
        }
String outFile = pathInfo;
boolean found = false;
String contentEncoding = null;
        

// Pack200 Compression
if (encoding != null && contentType != null &&
contentType.compareTo(JAR_MIME_TYPE) == 0 &&
encoding.toLowerCase().indexOf(PACK200_GZIP_ENCODING) > -1){

contentEncoding = PACK200_GZIP_ENCODING;
            
            
if (doFile(pathInfoEx.concat(".pack.gz"),response)) {
outFile = pathInfo.concat(".pack.gz") ;
found = true;
} else {
// Pack/Compress and transmit, not very efficient.
found = false;
            }
        }

// HTTP Compression
if (found == false && encoding != null &&
contentType != null &&
contentType.compareTo(JAR_MIME_TYPE) == 0 && 
encoding.toLowerCase().indexOf("gzip") > -1) {
                
contentEncoding = GZIP_ENCODING;

if (doFile(pathInfoEx.concat(".gz"),response)) {
outFile = pathInfo.concat(".gz");
found = true;
            }             
        }

// No Compression
if (found == false) { // just send the file
contentEncoding = null;
sc.log(CONTENT_ENCODING + "=" + "null");
doFile(pathInfoEx,response);
outFile = pathInfo;
        }

response.setHeader(CONTENT_ENCODING, contentEncoding);
sc.log(CONTENT_ENCODING + "=" + contentEncoding + 
" : outFile="+outFile);


if (sc.getMimeType(pathInfo) != null) {
response.setContentType(sc.getMimeType(pathInfo));
        }
        
InputStream in = sc.getResourceAsStream(outFile);
OutputStream out = response.getOutputStream();

if (in != null) {
try {
sendOut(in,out);
} catch (IOException ioe) {
if (ioe.getMessage().compareTo("Broken pipe") == 0) {
sc.log("Broken Pipe while writing");
return;
} else  throw ioe;
            }
} else response.sendError(response.SC_NOT_FOUND);
        
    }

}


GZIP 圧縮

GZIP は無償で提供される圧縮プログラムで、Java.util.zip.GZIPInputStream および Java.util.zip.GZIPOutputStream により JRE および JDK 内で利用できます。
コマンド行バージョンは、ほとんどの Unix オペレーティングシステム、Windows Unix ツールキット (Cygwin および MKS) において利用できます。他の多くのオペレーティングシステムに対応する GZIP は、http://www.gzip.org/ からダウンロードできます。

gzip を使用する場合は、圧縮された jar ファイルを圧縮するよりも、圧縮されていない jar ファイルを圧縮したほうがより効果的です。短所としては、ファイルはターゲットとなるシステムに圧縮されずに格納されます。

次に例を示します。
圧縮されたそれぞれのエントリを含む jar ファイルを gzip を使用して圧縮する
Notepad.jar       46.25 kb
Notepad.jar.gz   43.00 kb

「格納」されたエントリを含む jar ファイルを gzip を使用して圧縮する
Notepad.jar      987.47 kb
Notepad.jar.gz   32.47 kb

上記のように、圧縮されていない jar ファイルのダウンロードサイズは 14% 削減されたのに対して、圧縮された jar ファイルのダウンロードサイズは 3 % しか削減されていません。

Pack200 圧縮

Pack200 では、JAR 内のクラスファイルの密度とサイズに応じて、サイズの大きいファイルを非常に効率よく圧縮できます。JAR ファイルに含まれているのがクラスファイルだけで、その大きさが数 MB 程度しかなければ、9 分の 1 に圧縮することも可能です。 

前述の例と同じ jar ファイルを使用すると次のような結果になります。
Notepad.jar      46.25 kb
Notepad.jar.pack.gz  22.58 kb

この場合同じ jar ファイルのサイズは 50% 削減されます。

Pack200 は Java クラスファイルに対して、もっとも効率のよい方法です。次のような複数の技術を使用して、効果的に JAR ファイルのサイズを小さくします。

  1. クラスファイル内の定数プールデータをマージしてソートし、アーカイブ内の同じ場所に配置します。
  2. 冗長なクラス属性を削除します。
  3. 内部データ構造を格納します。
  4. 増分 (デルタ) および可変長エンコーディングを使用します。
  5. 2 次圧縮に最適なコーディングのタイプを選択します。
Pack200 は、JDK または JRE の bin ディレクトリにある、Command Line Interfaces pack200(1)、unpack200(1) によって使用できます。
Pack200 インタフェースは、Java からプログラムを使って呼び出すことができます。Java.util.jar.Pack200 の API リファレンス、および JavaDoc リファレンスを参照してください。

ファイルをパック (圧縮) する手順

1. JAR ファイルのサイズ、JAR ファイルの内容、および対象とするユーザの帯域幅を検討します。

これらの要因すべてを考慮して圧縮技術を選択します。unpack200 は、可能なかぎり効率的な動作をするように設計されていて、また短時間で元のファイルを復元します。JAR ファイルのサイズが大きく (2 MB 以上)、そのほとんどがクラスファイルである場合は、Pack200 が推奨されます。そのほとんどがリソースファイル (JPEG、GIF、データなど) である大きい JAR ファイルの場合は、gzip が推奨されます。

2.  Pack200 セグメント機能

Pack200 では、パックファイル全体をメモリにロードします。しかし、ターゲットシステムのメモリとリソースに制限がある場合、Pack200.Packer.SEGMENT_LIMIT を小さな値に設定することで、パックおよびアンパックで必要となるメモリ量を減らすことができます。Pack200.Packer.SEGMENT_LIMIT=-1  に設定すると、生成するセグメントを強制的に 1 つにするためサイズの削減に効果がありますが、パックおよびアンパックするシステムでは、Java ヒープを多く使用します。これらのパックされたセグメントのいくつかは、1 つのパックされたファイルを生成するために連結されることに注意してください。

3. JAR ファイルの署名

Pack200 では、生成される JAR ファイルの内容を再配置します。jarsigner では、クラスファイルの内容をハッシュ化し、マニフェスト内の暗号化されたダイジェストにハッシュを格納します。アンパックアプリケーションをパックされたファイルに対して実行すると、クラスの内容が再配置されるため、署名が無効になります。そのため、pack200 および unpack200 を使用して先に JAR ファイルを正規化してから、署名する必要があります。

(このように行える理由: パックプログラムが実行するクラスファイル構造の再配置はべき等であるため、2 回目のパックでは、最初のパックで生成された順序が変更されない。またアンパックプログラムでは、どのようなアーカイブ要素の伝送順序に対しても特定のバイトイメージを生成することが、JSR 200 仕様で保証されている)

HelloWorld.jar を使用するとします。


ステップ 1:  jar ファイルを正規化するためにファイルを再パックします。再パックでは、パックの実行とファイルのアンパックが 1 回のステップで行われます。

% pack200 --repack HelloWorld.jar

ステップ 2: 再パックを使用して正規化した後に、jar ファイルに署名します。

% jarsigner -keystore myKeystore HelloWorld.jar ksrini

署名した jar ファイルを検証して署名されていることを確認します。

% jarsigner -verify HelloWorld.jar
jar verified.


jar ファイルが実行可能であることを確認します。

% Java -jar HelloWorld.jar
HelloWorld


ステップ 3: ファイルをパックします。

% pack200 HelloWorld.jar.pack.gz HelloWorld.jar

ステップ 4: ファイルをアンパックします。

% unpack200 HelloWorld.jar.pack.gz HelloT1.jar

ステップ 5: jar ファイルを検証します。

% jarsigner -verify HelloT1.jar
jar verified.


// Test the jar ...
% Java -jar HelloT1.jar
HelloWorld


検証後、圧縮されたパックファイル HelloWorld.jar.pack.gz が配備されます。

4. サイズ削減のテクニック 

 Pack200 は、デフォルトでは High Fidelity (Hi-Fi) モードで動作します。したがって、JAR ファイルの個別エントリごとの属性とクラスにある元の属性はすべて保持されます。一般的にパックされたファイルサイズは増加する傾向がありますが、ダウンロードのサイズを削減するために使用できるテクニックのいくつかを説明します。
  1. 更新時刻: JAR ファイル内の各エントリの更新時刻が重要でない場合は、オプション  Pack200.Packer.MODIFICATION_TIME="LATEST" を設定できます。こうすることで、パックファイルで転送される更新時刻はセグメントごとに 1 つになります。最新時刻は、そのセグメント内すべてのエントリでもっとも新しく更新された時刻になります。 

  2. 圧縮指示: 上記と同様に、アーカイブ内の各エントリの圧縮状態が必要ない場合は、Pack200.Packer.DEFLATION_HINT="false" と設定します。こうすると個別の圧縮指示は転送されないため、わずかですがダウンロードするサイズが小さくなります。しかし、jar ファイルは変換されるときに、「格納」されたエントリを含むために、ターゲットシステムではディスク消費領域が増加します。

    次に例を示します。

    pack200 --modification-time=latest --deflate-hint="true" tools-md.jar.pack.gz tools.jar

    注: 上記の最適化は、何千ものエントリを含む JAR ファイルでは効果が顕著になります。

  3. 属性: JAR ファイルの配備時には必要のないクラス属性があります。これらの属性はクラスファイルから除くことが可能で、ダウンロードするサイズを大幅に減らすことができます。しかし、必須の実行時属性は保持するように注意してください。

    1. デバッグ属性: 行番号やソースファイルといったデバッグ情報は必要ないため (これらは通常はアプリケーションのスタックトレース内にある)、これらの属性は Pack200.Packer.STRIP_DEBUG=true を指定して破棄できます。これにより、パックされたファイルのサイズは、一般的に約 10% 減ります。

      例:
      pack200 --strip-debug tools-stripped.jar.pack.gz tools.jar

    2. その他の属性: 経験豊富なユーザは、他のストリップ関連プロパティを使用して、他の属性を削除できます。ただしその場合は、一層の注意が必要です。 生成される JAR ファイルを想定されるすべての Java 実行システムでテストし、その実行システムが削除した属性に依存していないことを確認してください。

5. 未知の属性の処理

Pack200 は、Java 仮想マシン仕様により定義される標準的な属性に対応しますが、コンパイラはカスタム属性の影響を受けません。カスタム属性が存在すると、デフォルトでは、Pack200 はクラスを通過して警告メッセージを発行します。これらの、「通過」するクラスファイルにより、パックしたファイルのサイズが大きくなる場合があります。JAR ファイルのクラス内に、未知の属性が多く使われている場合、パックされた出力のサイズが非常に大きくなる場合があります。その場合は、次の方法を検討してください。

属性が実行時に不要であると判断される場合、属性を取り除きます。これはプロパティ Pack200.Packer.UNKNOWN_ATTRIBUTE=STRIP または pack200 --unknown-attribute=strip foo.pack.gz foo.jar
を設定することにより実現されます。

pack200 --unknown-attribute=strip foo.pack.gz foo.jar

属性が実行時に必要で、拡大の原因になる場合、警告メッセージから属性を特定し、適切な配置を行います。この方法については、Pack200 JSR 200 の仕様、および Java API リファレンスの Pack200.Packer で説明されています。

コンパイラが、Pack200 のレイアウト仕様内で実装されない属性を定義することも可能ですが、パックの際に障害の元となる場合があります。その場合、クラスファイル名をリソースのようにすることで、クラスファイル全体を「通過」させることができます。次のように指定します。

pack200 --pass-file="com/acme/foo/bar/baz.class" foo.pack.gz foo.jar

または、ディレクトリとその内容全体を以下のように指定します。

pack200 --pass-file="com/acme/foo/bar/" foo.pack.gz foo.jar
6. インストーラ
インストールプログラムにおいて Pack200 テクノロジの機能を利用するためには、製品の JAR ファイルは、Pack200 を使用して圧縮し、インストールのときに圧縮解除する必要があります。インストールに JRE または JDK が含められている場合、配布された「bin」ディレクトリにある unpack200 (Unix) または unpack200.exe (Windows) を自由に使用することができます。この実装は純粋な C++ アプリケーションなので、実行するために Java ランタイムは必要はありません。

Windows:  インストーラは、GZIP よりも優れたアルゴリズムを使用してエントリを圧縮します。次のように pack200 を使用すれば、インストーラ自体の圧縮アルゴリズムによって、よりよい圧縮効果を得られます。

pack200 --no-gzip foo.jar.pack foo.jar

これにより、gzip として圧縮された出力ファイルになることを防ぐことができます。

unpack200 は、Windows コンソールのアプリケーションです。つまりインストール中には MS-DOS ウィンドウを表示します。これを防ぐには次に示すように、このウィンドウを非表示にする WinMain による起動ツールを使用します。


サンプルコード:
#include "windows.h" #include <stdio.h>

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR     lpCmdLine, int       nCmdShow) { STARTUPINFO si; memset(&si, 0, sizeof(si)); si.cb = sizeof(si);

PROCESS_INFORMATION pi; memset(&pi, 0, sizeof(pi));

//Test //lpCmdLine = "c:/build/windows-i586/bin/unpack200 -l c:/Temp/log c:/Temp/rt.pack c:/Temp/rt.jar"; int ret = CreateProcess(NULL,			/* Exec. name */ lpCmdLine,		/* cmd line */ NULL,			/* proc. sec. attr. */ NULL,			/* thread sec. attr */ TRUE,			/* inherit file handle */ CREATE_NO_WINDOW | DETACHED_PROCESS, /* detach the process/suppress console */ NULL,			/* env block */ NULL,			/* inherit cwd */ &si,			/* startup info */ &pi);			/* process info */ if ( ret == 0) ExitProcess(255);

// Wait until child process exits. WaitForSingleObject( pi.hProcess, INFINITE );

DWORD exit_val;

// Be conservative and return if (GetExitCodeProcess(pi.hProcess, &exit_val) == 0) ExitProcess(255);

ExitProcess(exit_val); // Return the error code of the child process

return -1; }

テスト

パックされているものもアンパックされているものも含め、すべての JAR ファイルは、アプリケーションのテスト合格基準に沿って問題がないことをテストする必要があります。コマンド行インタフェース pack200 を使用すると、gzip とデフォルト設定を使用して出力ファイルが圧縮されます。単なるパックファイルを作成して、圧縮にはユーザが指定したオプションで gzip を使用したり、別の圧縮プログラムを使用したりすることが可能です。

詳細情報

詳細については、「配備ツール」pack200 および unpack200 を参照してください。