精简出最小jre (经典文章-转载)

基本知道思路了,我把写的程序打包成jar,能双击运行了,然后拷贝一个jre到程序目录下,具体是这样的,目录叫dict,dict下面有dict.jar、jre(目录),然后写了一个cmd脚本:
@echo off
set path=%cd%\jre\bin
java -jar -verbose:class dict.jar >>class.txt
pause
这样程序使用的就是当前目录下的jre,程序运行后,最好把所有的功能使用一遍,这样输出了一个文件class.txt,里面有所有需要的class,其格式如下:
[Opened D:\data\dict\jre\lib\rt.jar]
[Loaded java.lang.Object from D:\data\dict\jre\lib\rt.jar]
[Loaded java.io.Serializable from D:\data\dict\jre\lib\rt.jar]
[Loaded java.lang.Comparable from D:\data\dict\jre\lib\rt.jar]
[Loaded java.lang.CharSequence from D:\data\dict\jre\lib\rt.jar]
[Loaded org.apache.lucene.index.CompoundFileReader$FileEntry from file:/D:/data/dict/dict.jar]

我们依照这个文件来裁剪rt.jar:
首先在utralEdit中进行一些处理,去掉所有不是rt.jar中的class的行,去掉from后面的,去掉loaded等无关项目,再把“.”替换成“/”.这个可以利用正则表达式等轻松处理。处理完后得到的文件类似如下格式:
java/lang/Object
java/io/Serializable
java/lang/Comparable
java/lang/CharSequence
java/lang/String
然后写一个脚本或者程序处理,将rt中需要的的class拷贝到另一个对应的文件夹rt1,我用java写了一个,没有时间仔细改,但能完成人物了。代码如下:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader; public class ReduceRt {
//文件拷贝
public static boolean copy(String file1,String file2)
{
try //must try and catch,otherwide will compile error
{
// instance the File as file_in and file_out
java.io.File file_in=new java.io.File(file1);
java.io.File file_out=new java.io.File(file2);
FileInputStream in1=new FileInputStream(file_in);
FileOutputStream out1=new FileOutputStream(file_out);
byte[] bytes=new byte[1024];
int c;
while((c=in1.read(bytes))!=-1)
out1.write(bytes,0,c);
in1.close();
out1.close();
return(true); //if success then return true
}

catch(Exception e)
{
System.out.println("Error!");
return(false); //if fail then return false
}
}
//读取路径,copy
public static int dealClass(String needfile ,String sdir,String odir) throws IOException
{
int sn = 0; //成功个数

File usedclass = new File(needfile);
if(usedclass.canRead())
{
String line = null;
LineNumberReader reader = new LineNumberReader(new InputStreamReader(new FileInputStream(usedclass),"UTF-8"));
while((line = reader.readLine())!=null)
{
line = line.trim();
int dirpos =line.lastIndexOf("/");
if(dirpos>0)
{
String dir = odir+ line.substring(0,dirpos);
File fdir = new File(dir);
if(!fdir.exists())
fdir.mkdirs();
String sf = sdir + line + ".class";
String of = odir + line + ".class";
boolean copy_ok=copy(sf.trim(),of.trim());
if(copy_ok)
sn++;
else
{
System.out.println(line);
}

}

}
}
return sn;

}

public static void main(String[] args)
{
String needfile = "usedclass.txt";
String sdir = "./rt/";
String odir = "./rt1/";
try {
int sn = dealClass(needfile,sdir,odir);
System.out.print(sn);
} catch (IOException e) {
// TODO 自动生成 catch 块
e.printStackTrace();
}

}

}

我裁剪出来的rt大小为500多k。然后将rt1里面的目录和文件打包成rt.zip,改名为rt.jar,然后替换原来的rt.jar。具体的步骤可以参考上面提到的那两篇文章。

#######################

如何制作最小的RCP程序压缩包(包含JRE)

Java开发程序,发布时总要考虑的问题就是怎么在使用者的机器上装好JRE。要考虑的问题很多:使用者有没有能力独自安装JRE,使用者已有的 JRE 和我们需要的版本是不是一致,会不会出现版本问题,等等。使用.NET要考虑的问题就少些。现在.NET CLR似乎已经很普及了,看好多D版的 Win XP都会自己安装最新的.NET CLR,而且似乎它的安装界面也比JRE友好些。彻底解决安装JRE的问题的方案,就是让我们的应用程序自己背着JRE!这样,我们的程序就像传统的Win32应用程序一样,双击就可以执行,不用管所在的机器上是否有JRE,是什么版本的JRE,无论怎样,我有我自己的!要做到这一点,其实非常容易。
王森在他的《Java深度历险》(强力推荐这本书,内容少而精)的第一章就解释了JDK,JRE,JVM之间的关系。解释了我们执行java.exe时发生的事情。其中提到,java.exe依照一套逻辑来寻找可以用的JRE,首先查找自己所在的目录下有没有 JRE(据王森讲这样说不确切,我没有JDK全部的源代码,在此无从考证);其次查找自己的父目录下有没有JRE;最后才是查询Windows的注册表。
通常我们在安装好了JRE的机器上的任何一个目录下都可以执行java.exe。因为它在安装时被复制到了windows的system32目录下,而后者无论如何都会在path环境变量中。这个java.exe最终必然会访问注册表来确定真正的JRE的所在地。若我们要求每一个应用程序都自带JRE,必然不能走这条路。但,逻辑的第二条讲,java.exe会在它的父目录下查找JRE,解决方案就在这一条中。
假设我们的应用程序打好了包,叫做 MyApp.jar,放在MyApp的目录下。我们在MyApp目录下,可以执行java ?jar MyApp.jar来运行我们的程序。我们安装的是 JRE 1.5,在C:\Program Files\Java\jre1.5.0下。现在,我们只需要简单的将jre1.5.0目录搬到MyApp目录下,顺便改个容易写的名字比如叫jre。现在,我们的应用程序就象这样:
MyApp
        MyApp.jar
        Jre
               Jre1.5.0目录下的全部内容
Java.exe 就在jre目录下的bin目录中。根据第二条逻辑,java.exe会在它的父目录中查找jre,实验证实,它会查找lib目录,而lib就在jre目录下。因此,这样java.exe就会确定jre的所在然后正常执行java程序,不会去管我们是否安装了JRE,注册表中是否有注册项这些杂事了。
试一下,在命令行下进入MyApp的目录下,假设它在C盘,将path指向MyApp下的JRE:
set path=c:\MyApp\jre\bin
然后运行:
java ?verbose ?jar MyApp.jar
加上verbose参数以确定我们确实用了这一套被搬出了家的JRE。
程序可以运行,并且在命令行输出的前几行,可以看到:
[Opened C:\MyApp\jre\lib\rt.jar]
[Opened C:\MyApp\jre\lib\jsse.jar]
[Opened C:\MyApp\jre\lib\jce.jar]
[Opened C:\MyApp\jre\lib\charsets.jar]
因此程序读取的确实是它的私有的JRE。
至此,我们似乎完成了任务。但是现在我们的私有JRE仍不完美,缺点是太大。JRE 1.5有接近70MB,作为我们的私有的JRE,好多内容都是可以抛弃的。Jre目录下的license都可以不要,bin下的执行文件只需要保留java.exe或者javaw.exe,lib下只要保留rt,jsse, jce,charsets几个库就可以了。除了i386和zi两个子目录外,其余的子目录都可以不要。Zi下只需要保留自己地区的子目录和其下的一些文件就可以。Lib下除了库之外的属性文件等等都要保留。这样清理一番,JRE仍然有接近50MB。还可以继续清理几个库文件里面不需要的内容,这需要仔细的整理,会很费功夫。最好能写出一个自动工具帮助我们整理它们。从Sun公司上下到的JMF里面附带的用Java写的媒体播放器就自带了JRE,只有几个 MB。
清理过后需要运行几遍我们的应用程序,以确保我们的JRE不缺少东西。
如果我们希望能有一个程序直接启动我们的应用程序,那就还要费些功夫。最简单的方法是弄出一个快捷方式来,但是快捷方式的路径不能是相对的,不方便我们安装。我想到的方案就是用Win32程序包装一下。在VS.NET下写一个Win32小程序:
int PASCAL WinMain( HINSTANCE hInstance,
     HINSTANCE hPrevInstance,
     LPSTR lpszCmdLine,
     int nCmdShow )
{
     STARTUPINFO si;
     PROCESS_INFORMATION pi;

     ZeroMemory( &si, sizeof(si) );
     si.cb = sizeof(si);
     ZeroMemory( π, sizeof(pi) );

     // Start the child process.
     if( !CreateProcess( "jre\\bin\\javaw.exe",//执行的程序名
                         "jre\\bin\\javaw.exe -jar MyApp.jar", // 带参数的执行程序
             NULL,              // Process handle not inheritable.
             NULL,              // Thread handle not inheritable.
             FALSE,             // Set handle inheritance to FALSE.
             0,                 // No creation flags.
             NULL,              // Use parent's environment block.
             NULL,              // Use parent's starting directory.
             &si,               // Pointer to STARTUPINFO structure.
             π )              // Pointer to PROCESS_INFORMATION structure.
     )
     {
             ErrorExit( "CreateProcess failed." );
     }

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

     // Close process and thread handles.
     CloseHandle( pi.hProcess );
     CloseHandle( pi.hThread );
}
基本上是按照MSDN文档中的例子照搬的。将它编译成一个EXE文件,我们的任务才全部完成。双击这个EXE文件,我们的程序启动了,看起来和传统的Win32程序没有两样,JRE完全被隐藏在底层。

###################

如何制作最小的RCP程序压缩包(包含JRE)

如果开发完了一个RCP应用程序,要安装到客户端,那么这个安装文件会有多大呢,我们当然希望是越小越好。

我们先算一下普通方式下的文件大小:

jre1.5 安装程序 16M

rcp3.2 runtime 9M

rcp应用程序(包含用到的第三方的lib) 此处假设 2-3M

那么将这些文件打成包后的大小将为28M左右,一个普通的rcp安装程序居然会有这么大。这实在有点令人难以接受。

难道就不能再小一点吗?我们多么希望有一个小巧的RCP安装程序啊。答案是肯定的,我们完全可以将RCP安装程序控制在10M以内,甚至更小。

此处只介绍如何压制一个最小的RCP压缩包,至于如何制作安装程序,已经超出了讨论的范畴,只要有了最小的压缩包,不论用何种安装程序,都可以制作出10M以下的RCP安装程序。

第一步: jre 减肥

jre1.5安装程序有16M,这可是一个大东西,客户想要运行RCP程序,首先就要安装JRE。这也是很多客户反感的,jre里面包含了太多的东西,很多是rcp程序根本用不到的,比如swing库,如果全是用swt开发,swing包就多此一举了。 而且JRE的安装程序也不见的那么健壮,笔者就曾经两次遇到在不同的机器上不能成功安装jre的情况,而且通过添加删除程序也删不掉,非常烦人。其实完全没有必要安装JRE,只需要在rcp安装目录下建一个jre目录,里面包含jre用到的文件就可以了。rcp程序启动时,会首先查找当前目录下有没有jre目录,如果有,就用里面的jre,如果没有才去注册表查找jre。接下来,我们看看这个jre目录里面都有哪些东西,一些不要的统统删掉,至于删掉哪些,要根据情况而定,这个需要反复实验才能确定哪些有用,哪些没用。最后bin目录笔者保留了必须的dll和exe文件,llib目录里面,只保留了rt.jar和charsets.jar这两个库。但是rt.jar还是太大了32M,既然要减肥,那就彻底减到底吧,用winrar或者其他解压缩工具打开rt.jar,看看哪些包里面的class不需要,就统统删掉。例如,客户端不需要swing,javax.swing包干掉,客户端不需要rmi,javax.rmi包干掉,删来删去,最后rt.jar变成了10多M, charsets.jar这个包也挺大8M,里面包含了不同的字符集编码,其实很多字符集都用不到,根据情况挑选你所用的吧。

到了这一步,jre已经瘦了一圈了,但还是不能达到我们的目的,如果用普通的压缩工具压缩jre目录后,基本可以达到10到12多M。这离我们的目标还差好大一快呢。jre还的减肥,这次狠一点,拿出我们的杀手武器pack200,pack200是java1.5自带的(在jre\bin\目录下)一个针对class文件进行压缩的工具,由于专门针对class文件进行了优化,压缩比高的惊人(当然速度也比普通压缩软件慢多了)pack200的用法请自行参考相关文档

。先用pack200把rt.jar,和charsets.jar压缩一下,然后用其他压缩软件对jre整个目录压缩一下,压缩后的大小让你吃惊,如果用rar,压缩出来的是4M,zip高一些4.8M。可能是笔者删的东西太多了,所以会这么小。但这里还包含一个8M的charsets.jar文件。笔者试过,如果不包括charset.jar,用rar压缩后大小为2.88M,这实在太惊人了,有谁能想象一个只有2.88M的JRE,遗憾的是charset.jar是必须的,你可以删掉里面一些不要的字符集这样能压出来的jre也再3M-4M之间。必须注意的是,解压缩的时候,还要用pack200解开压缩后的jar文件。整个步骤就是压缩两遍,第一遍用pack200压缩所有的jar文件,第二遍再用一个其他压缩软件压缩jre目录。这样就能得到一个很小的jre压缩包。

看到这里,有人开始怀疑,这个3M多的JRE能用吗?笔者就曾将这个jre放到eclipse目录下,eclipse启动一切正常,进去后可以继续写我的java代码,还可以编译java文件(其实eclispe本身不需要tools.jar,它自己就带了一个很强的java编译器),从cvs下载文件也不成问题,试了一圈,没发现有什么出错的地方。当然,包不齐,少了那个class文件,就会出错了,所以删除class文件的时候,尽量不要多删。如果你很熟悉每个class文件的用途,就可以放心的去删了。如果SUN能出一个 MINI JRE 那就更好了。

第二步: RCP插件减肥

记不清从eclipse3.1起的那个版本,已经开始支持将插件打包成一个jar文件,甚至这个插件里面包含着其他的jar文件,这在3.1以前只能创建一个插件目录。既然插件可以打包成jar文件,那么pack200就派上用场了,同压缩jre一样,此处就不在叙述了。


值的注意的问题是,有的插件jar文件里面包含一个目录lib,lib里面又包含了其他的jar文件,那么用pack200对这个插件jar压缩的时候,lib里面的jar文件是不会压缩的。这个也不是什么问题,只要写个小程序,对lib里面的jar文件压缩一下就行了。

笔者实验的所做的RCP的插件压缩后的大小为6M多,这里面包括rcp runtime 必须的插件,以及自己开发的rcp程序,用到的第三方库,以及eclipse的一些插件emf,gef,jface-databinding等,这些加起来压缩后总共6M多。如果你用的插件不是那么多,压缩后的肯定更小。 这样加上jre,整个程序控制在了10M以内。

让人非常讨厌的是,从eclispe3.2M5 起,又加了一个com.ibm.icu的插件,这个插件竟然有3M多,而且这个插件是rcp runtime必须的。其实这个插件又是一个和字符集相关的插件,里面很多字符集是程序用不到的,除非你的程序要支持多语言,但也不会把所有的语言都囊括吧。如果每个字符集都能做成一个插件,只挂接自己想需要的,哪可真是太好不过了。希望eclispe3。3会改进这一点。
 

 

posted on 2009-06-09 10:18 HQ 阅读(2613) 评论(0)  编辑 收藏 引用 所属分类: JAVA开发

只有注册用户登录后才能发表评论。

导航

统计

常用链接

留言簿(2)

随笔档案(7)

文章分类(14)

相册

搜索

积分与排名

最新评论

阅读排行榜

评论排行榜