注: この Java 配備ガイドでは、Java SE 6 update 10 リリースより前にリリースされた機能について説明します。最新情報については、Java Rich Internet Application の開発および配備を参照してください。

Pack200 と圧縮

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

はじめに

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

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

理論

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

SDK/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 および SDK 内で利用できます。
コマンド行バージョンは、ほとんどの Unix オペレーティングシステム、Windows Unix ツールキット (Cygwin および MKS) において利用できます。ほかの多くのオペレーティングシステムに対応する GZIP は、http://www.gzip.org/ からダウンロードできます。

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

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

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

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

Pack200 圧縮

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

前述の例と同じ jar ファイルを使用します。
Notepad.jar      46.25kb
Notepad.jar.pack.gz  22.58kb

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

注: 大きなサイズの jar ファイルを署名すると、セキュリティーエラーによりステップ 5 が失敗することがあります。このエラーの多くは、バグ 5078608 が原因です。リリースノートに説明されている回避方法を実行してください。

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

  1. クラスファイル内の定数プールデータをマージしてソートし、アーカイブ内の同じ場所に配置します。
  2. 冗長なクラス属性を削除します。
  3. 内部データ構造を格納します。
  4. 増分 (デルタ) および可変長エンコーディングを使用します。
  5. セカンダリ圧縮に最適なコーディングのタイプを選択します。

Pack200 は、SDK または JRE ディレクトリの bin ディレクトリにある、コマンド行インタフェース pack200(1)、unpack200(1) によって使用できます。
Pack200 インタフェースは、Java からプログラムを使って呼び出すことができます。Java.util.jar.Pack200 の API リファレンス、および JavaDoc リファレンスを参照してください。

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

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

これらの要因すべてを考慮して圧縮技術を選択します。unpack200 は、可能なかぎり効率的な動作をするように設計されていて、また短時間で元のファイルを復元します。JAR ファイルのサイズが大きく (2MB 以上)、そのほとんどがクラスファイルである場合は、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 ファイルの構築に使用したものと同じ鍵で署名する必要があります。あるいは、再パック、再署名、および検証を行う前に、META-INF ディレクトリにあるすべての署名ファイルを削除します。署名ファイルの名前は MANIFEST.MF*.DSA、および *.SF です。

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

% jarsigner -verify HelloWorld.jar
jar verified.

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

% 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.

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 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 または SDK が含められている場合、配布された「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 を使用したり、別の圧縮プログラムを使用したりすることが可能です。

詳細情報

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

Java Standard Edition 6 での更新

Java SE 6 では、Java クラスファイル形式が更新されました。詳細については、「JSR 202: Java Class File Specification Update」を参照してください。JSR 202 により、次の理由から、Pack200 エンジンを適宜更新する必要があります。

変更点を最小限に抑え、ユーザーに意識させないようにするために、圧縮プログラムは入力クラスファイルのバージョンに基づき、適切なバージョンのパックファイルを生成します。

また、下位互換性を保持するために、入力 JAR ファイルが JDK 1.5 以前のクラスファイルで統一して構成されている場合、1.5 互換のパックファイルが生成されます。それ以外の場合は、Java SE 6 互換の pack200 ファイルが生成されます。詳細については、Pack200 のマニュアルページを参照してください。


Copyright © 1993, 2013, Oracle and/or its affiliates. All rights reserved.