Java 共享类--学习如何使 Java 应用程序启动更快以及占用更少的内存

学习如何使 Java 应用程序启动更快以及占用更少的内存
级别: 初级
 

Lakshmi Shankar (shankarl@uk.ibm.com) , Java Technology Center Development Team, IBM Hursley Labs
Simon Burns (simon_burns@uk.ibm.com) , Java Technology Center Development Team, IBM Hursley Labs
Roshan Nichani (nichanir@uk.ibm.com) , Java Technology Center Development Team, IBM Hursley Labs

2004 年 6 月 

如今 Java 应用程序面临一个问题:它们惟一可用的容器是 Java 虚拟机(Java virtual machine,JVM)进程自身。需要多个 JVM 来彼此隔离 Java 应用程序,这会带来两个主要的负面影响。第一个是每次 JVM 调用所花费的启动时间,第二个是每个 JVM 所需的内存占用。考虑到这些开销以及不能在一个 JVM 中隔离应用程序,很显然需要进行一些基础性工作才能解决这些问题。答案就是共享类。在本文中,IBM Java Technology Center Development Team 成员 Lakshmi Shankar、Simon Burns 和 Roshan Nichani 讨论了 JVM 中共享类背后的概念、它们的工作原理以及用户如何利用这种技术。他们还讨论了这种技术的几种当前实现以及在将来会有什么样的应用。
要真正彼此隔离 Java 应用程序,实质上需要多个 JVM,然而启动成本和内存占用使这种方式不那么理想。而共享类可以同时解决这两个问题。在多 JVM 环境中,共享类通过将一组核心系统类装载到共享内存中,可以在多个 JVM 中共享这些类。这些共享类放到内存的一个共享区域中,它们在这里对所有 JVM 都是保持一致的。结果,共享类只需要在第一次使用时装载到内存中,这消除了在以后每次 JVM 调用时装载它们的固定成本,并减少了每个 JVM 中的内存占用。

IBM 在 z/OS 平台上实现了共享类技术。Apple Computer Inc. 在 Mac OS X 上实现了名为 Java Shared Archive(JSA)的一种共享类,而 Sun 在 J2SE 1.5 版中引入了基于 JSA 技术的 Class Data Sharing (CDS)。让我们分析一下这些实现是如何工作的。

IBM 的实现
自 J2SE 1.3.1 以来,IBM 就在 z/OS 平台上提供了共享类技术的实现。这种实现是通过让一个主(或称 master) JVM 将核心系统类装载到共享内存完成的,那么这到底是什么意思呢?

分解堆
内存分为共享堆和 Java 堆。主 JVM 将系统堆(即共享堆)分配到共享内存中,这里是放置系统类的地方。系统堆在主 JVM 的生存周期中一直存在,并且不会受到垃圾收集(GC)的影响。每一个后续(或者worker)JVM 附加到这个系统堆上,如图 1 所示,并为自己的 Java 堆分配非共享内存,它会受垃圾收集的影响。Java 堆包含特定于每一个 worker JVM 运行的应用程序的非共享类和所有实例化的对象。

图 1. 共享类分解堆


共享类装载器
每个 worker JVM 都可以通过将类放到共享类装载器的 classpath 中而将它们装载到共享堆中。共享类与普通类的装载方式一样??使用 parent-delegation 模式。

层次结构中的每一个类装载器检查其缓存,确定这个类是否已经装载。如果还没有装载,那么类装载器就向其父类装载器传递一个检查装载请求,这样一直上溯到层次结构顶部的 primordial 或者 bootstrap 类装载器。如果没有在任何缓冲区中发现这个类,那么每一个类装载器都会试图从自己的存储库中装载这个类,如果成功,就返回这个类。否则,它将请求传递给层次结构中下面的装载器。这种模型保证了首先检查最受信任的存储库,并防止信任程度低的代码通过采用与核心 API 成员相同的名字代替受信任的核心 API 类。

如果类是 primordial 类或者定义的类装载器是共享类装载器,那么类对象将在共享堆中创建,并且类标记为共享类。图 2 显示了 bootstrap 类装载器位于类装载器层次结构的顶部,并负责装载核心 API 中的类。这些类是信任程度最高的。扩展类装载器装载 extensions 目录中的标准扩展 JAR 文件中的类。共享应用程序类装载器可以用于共享用户或者应用程序类。

图 2. 类装载器层次结构


但是在这种实现中,类到底是如何由多个 JVM 共享的呢?

假定 JVM 1 装载了 java/lang/String,这是一个由 bootstrap 类装载器装载的系统类。如果 JVM 2 想要装载 java/lang/String,由于它不能访问 JVM 1 的 bootstrap 类装载器缓存,所以它必须使用自己的 bootstrap 类装载器重新装载这个类。在这个例子中,JVM 不共享任何类,如图 3 所示。

图 3. 类没有跨 JVM 共享


因此,最好让 JVM 共享相同的类,如图 4 所描绘的。

图 4. 跨 JVM 共享的类


要解决这个问题,通过创建一个名为 namespace 的全局类缓存,将类缓存的概念加以扩展。每个 JVM 的类装载器必须在这个 namespace 上注册。当共享类装载器装载一个类时,它被同时放到本地类缓存和 namespace 中(如图 5 所示)。这样做使得其他 JVM 中的类装载器(在 namespace 上注册的)不用装载它就可以访问这个类。

图 5. Namespace 跨 JVM 共享


保护域
类装载器有一个或者多个代码源对象(从其中装载类的 JAR 文件或者目录)。这些对象用于创建保护域,它被传递给 defineClass() 方法调用。使用共享类的其他 JVM 将需要这个信息,但是不能共享保护域,因为它们包含本地信息。为了解决这个问题,将代码源放到系统堆中。打包信息也需要共享。

竞争条件
由于 JVM 会读取和写入共享数据,需要有一种方法处理竞争条件。最简单的方式是使用全局块。不过,出于性能和伸缩性的原因,应当谨慎地使用它们。

一种避免锁住所有 JVM 的方法是使用开放式原子更新(optimistic atomic updates)。例如,在装载一个类时,类装载器将检查其 namespace (在检查其自己的本地类缓存后)。如果不能找到这个类,那么它就会装载它。装载后,它会自动检查其他 JVM 没有装载这个类,然后更新 namespace。

全局与本地数据
类中有些信息(如名字)在所有 JVM 中都是一样的,而另一些信息要求是本地的,如装载这个类的类装载器。 每个 JVM 都需要有装载这个类时需要它生成的那一部分的本地副本,如图 6 所示。JVM 中类的阴影区域是本地副本。系统中类的非阴影区域是全局部分。

图 6.共享全局数据与共享本地数据


共享类时偶尔会出现的一个问题是,当一个 JVM 更新一个类时(比如通过修改静态字段),所有其他 JVM 都会看到这种改变。这种操作是不希望的,它会造成不可预料的结果。为了保证隔离性,每个 JVM 都有每个共享类的所有静态字段的副本。

JIT 编译代码
当属于共享类的代码由即时(just-in-time,JIT) 编译器编译时,它是自动共享的。这意味着不管由哪个 JVM 编译代码,所有 JVM 都会获得性能上的好处(而只有一个承担 JITing 的开销)。

启动器程序
IBM 的实现需要一个启动器(launcher),以便控制 JVM 的创建。这个启动器必须由用户以本机代码编写。如清单 1 显示了一个示例启动器的伪代码:

清单 1. 启动器伪代码


    create Master JVM (and store returned token*); 
    
    while(work to do) { 
    
        create a Worker JVM passing in token from Master JVM; 
        
        do work on Worker JVM; 
        
        terminate Worker JVM; 
        
    } 
    
    terminate Master JVM; 
    

         

* token returned from the Master JVM is the address of the shared heap.

 

Apple 的实现
Apple 的共享类技术实现是 Mac OS X 中的 Java 共享档案(Java shared archive,JSA)。

实质上,JSA 是一个内存映射到共享内存的文件,可以让多个进程(即多个 JVM )访问它。安装了 Java Runtime Environment (JRE) 后,用系统 JAR 文件中的类创建 JSA。这些核心类的内部表示存储在文档中。这个数据是静态的,因此永远也不会改变,这意味着可以共享这些类而不会有任何隔离问题。它还意味着这些类永远不会被垃圾收集。因为 Apple JVM 使用一般性 GC,因此必须保护这些类不被收集。这种保护是通过引入 immortal 对象的概念来实现的,JSA 中的所有类都指定为 immortal。

这种技术在默认情况下是可用的,使用它不需要编写任何特殊的程序??如 IBM 的实现所需要的启动器。这种实现的另一个好处是系统重启后也可以享受它的好处,不局限于特定 JVM 的生存周期。不过,它的缺点是局限于一组核心类,没有提供共享应用程序类或者 JIT 代码的能力。

Sun 的实现
Sun 的共享类技术的实现称为类数据共享(Class Data Sharing,CDS),是 J2SE 1.5 中的新功能。CDS 基于 Apple 的 JSA 实现。

与 JSA 类似, CDS 使用一个只读的内存映射文档文件。这个文件包含核心系统类的内部表示,并在启动时映射到每个 JVM 的 Java 堆中。CDS 文件既可以由 Sun JRE 安装器创建,也可以像 Sun 的 CDS 文档中所描述的那样手工创建。

同样,主要的好处是启动时的成本节省和降低内存占用。启动时间之所以减少是因为核心类不是用传统的类装载机制(即 JVM 一次映射所有核心类,而不是单独装载每一个类)装载的。所使用的内存之所以减少是因为 JVM 共享了只读类数据,而不用每一个 JVM 占有自己的副本。同时,因为类不是用传统机制装载的,所以 JVM 不会由处理任何未使用的方法。

在撰写本文的时候,Sun 的实现只限于共享核心系统类,并且不允许共享应用程序级的类或者 JIT 代码。应用程序相对于它所使用的核心类越小,启动时节省越多。

根据 Sun 的 CDS 文档,在将来共享类功能将扩展到应用程序级的类,以改进大型应用程序的启动成本。

共享类的当前应用
Java 平台的用户已经利用了共享类技术。本节描述当前部署的一些应用领域。

事务环境
共享类(不管是系统类还是应用程序类)对事务环境(如 CICS 和 DB2)中的 JVM 启动和内存占用可以有显著的效果,在这里每个事务或者应用程序都包装在单独的 JVM 中。

z/OS 2.3 上的 Customer Information Control System Transaction Server (CICS TS) 是运行 Java 事务和应用程序的主要商业产品之一(目前使用了 IBM JVM 的共享类技术)。CICS TS 2.3 引入了共享类缓存工具,它将共享类功能扩展为它所控制的 JVM 池(称为它的 JVMset)。

CICS 使用自定义启动器程序来控制 master JVM 以及所有 worker JVM 的启动,它们需要服务请求来运行 Java 组件。除了系统堆中的共享类,它们还可以共享 worker JVM 中的特殊“中间件类”和一些“应用程序类”,如图 7 所示。这个共享类缓存工具为 CICS 客户提供了很多好处。例如,Java 类是由 master JVM 装载的,每个 CICS 区域(当 CICS 启动时) 装载一次而不是每个 JVM 一次,从而在 JVMset 中减少所有 worker 的类装载成本。通过在缓冲区中保留每个类的一个副本而不是每个 worker JVM 堆一个副本,共享类缓存工具还减少了 JVMset 的总体存储需求。

图 7. CICS 中的共享类 


在处理大量事务时,由更快的启动和更有效的内存处理而获得的好处会显著增加。图 8 中第一个图显示了共享和非共享 CICS 环境中一个简单事务的启动时间。第二个图显示整体 JVM 存储成本,也分为共享和非共享 CICS 环境。可以明显地看出当 JVM 的数量增加时,每个 JVM 所需要的内存减少了(与非共享比较)。

图 8. 使用共享类的 CICS 性能图


桌面系统
在 Apple Mac OS X 平台上运行的 Java 应用程序可以自动从这种技术中获得好处,Sun J2SE 1.5 用户也会从中受益。

共享类将来可能的应用
共享类技术有可能为基于 Java 平台的其他技术的用户带来极大的好处。本节重点介绍一些可能的应用。

普及环境
谈到运行 Java 应用程序,更小的内存占用对普及计算环境(如 PDA 和移动电话)会有显著的影响。这种环境是由特殊的 VM、JIT 编译器和 J2ME 中的类构成的。不过,当普及设备变得越来越普遍时,需要多 JVM 的可能性会增加。使用包含核心系统类的内存映射文件会显著节省内存,这对于普及设备是很关键的。

网格计算
在网格计算环境中,像 CPU 时间和内存容量这样的资源决定了所发生的成本,优化通常是有好处的。

使用共享类可以为网格提供者提供更好的运行 Java 程序的能力,因此它们可以同时为用户运行更多的任务。它还使客户每次使用的成本更低,这使它们具有更高的价值,为供应商提供更大的竞争优势。

Java 应用程序
对于复杂的 J2EE 应用程序,如 IBM WebSphere,可能会装载数千个类。如果核心系统类和 WebSphere 类是共享的(而不是由每个 JVM 在使用前装载),那么应用程序启动时间可以有显著缩短。这种好处加上内存占用减少可以使 WebSphere 上运行的各个应用程序受益。

对于 Java 用户,让应用程序更快地启动的同时保持更低的内存占用肯定是有好处的。如果在启动 Java 应用程序(如 Eclipse)时核心类已经装载,那么这些应用程序可以共享核心类,因而不用在启动时单独装载它们。这种方式对于使用 Swing 或者 AWT 应用程序特别有用,这些应用程序已经由于启动缓慢和占用大量内存而受到报抱怨了。

结束语
本文提供了对 Java 共享类技术的概述和一般性介绍。我们展示了不同的共享类和它们所提供的好处,如 Java 应用程序的启动时间更快和内存占用更小。我们还分析了可以利用共享类的当前和未来的一些技术。

所有需要关注启动时间和内存占用的 Java 应用程序都可以通过共享类技术获益。当前的实现有局限性,如有限的能力或者不能共享应用程序类(或者这两者)。如果能够解决这些问题,那么更多用户会从这种技术中受益,使 Java 应用程序更有吸引力。

参考资料 

通过 Greg Travis 的“Understanding the Java ClassLoader”(developerWorks,2001 年 4 月)和 Dennis Sosnoski 的“Java 编程的动态性,第 1 部分:类和类装入 ”(developerWorks,2003 年 4 月)学习有关类装载的更多内容。


通过 Brian Goetz 的 “Java 理论与实践:JVM 1.4.1 中的垃圾收集” (developerWorks,2003 年 11 月) 学习有关 1.4.1 JVM 中垃圾收集的更多内容。


Thomas Myer 的文章 “Grid computing: Conceptual flyover for developers” (developerWorks,2003 年 5 月)提供了网格计算的很好概述。如果对这种新兴的技术感兴趣,请一定访问 developerWorks 网格计算专题。


在这个网站上查看有关 Sun's Class Data Sharing 的信息。 


这个网站提供了对 Mac OS X Java Shared Archive 的很好概述。 


通过这篇论文 1.4.1 Diagnostics Documentation(PDF)更好地理解 IBM 的 JVM。


通过白皮书“Java? Virtual Machine Choices with CICS?, Transaction Server for z/OS?, Version 2 Release 3” (PDF),获得 CICS Transaction Server for z/OS 所使用的 IBM JVM 的细节。


利用“Persistent Reusable Java Virtual Machine User’s Guide”(PDF)学习 IBM JVM for z/OS 的功能的更多内容。


在 developerWorks Java 技术专区 上可以找到数百种有关 Java 技术的参考资料。


请访问 Developer Bookstore,获取技术书籍的完整列表,其中包括数百本 Java 相关主题的书籍。


是否对无需通常的高成本入口点(entry point )或短期评估许可证的 IBM 测试产品感兴趣? developerWorks Subscription 为 WebSphere?、DB2?、Lotus?、Rational? 和 Tivoli? 产品提供了低成本的 12 个月单用户许可证,包括基于 Eclipse 的 WebSphere Studio IDE,用于开发、测试、评估和展示您的应用程序。



作者简介
Lakshmi Shankar 是英国 IBM Hursley Labs 的软件工程师。他已经在 IBM 工作两年多了,经验丰富,在 Hursley Labs 从事 Java 性能、测试和开发方面的工作。他目前是 IBM 的 Java 技术的 Class Loading 组件的所有者。 

Simon Burns 是 Shiraz (可重设的 JVM 和 IBM Java 共享类)组件的所有者和 IBM Hursley Labs 的 Java Technology 的小组负责人。他从事 JVM 开发已经三年了,专攻 Shiraz 组件和 z/OS 平台。他还与 CICS 密切合作,帮助他们利用 Shiraz 技术。
 

Roshan Nichani 是 IBM Hursley Labs 的 Java Reflection 组件所有者。他拥有丰富的经验,曾为 IBM Global Services 和 Java Technology Center 工作。他还为 CICS 工作过,帮助他们使用 Shiraz 技术。Roshan 已经在 IBM 工作了四年多。
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值