关于作者

用户名:tonnyshen
笔名:tonnyshen
地区:
行业:其他

日历  

快速登录

+ 用户名:
+ 密 码:

在线留言



访问统计:
文章个数:7
评论个数:2
留言条数:0




Powered by BlogDriver 2.1

燎原

 

文章

J2EE项目10大风险

避免本文所列之10大J2EE风险,确保企业级Java项目成功
  作者:Humphrey Sheil
   翻译:Blueski

说明:
     本文已在51CMM网站《中国系统分析员》杂志第3期刊载。
     原文在 http://www.javaworld.com/javaworld/jw-03-2001/jw-0330-ten.html
摘要
     当你开始着手组织一个企业级Java项目的时候,就如同开始同时轮回地扔好几个魔术小球: 业主关系处理、持续而漫长的设计开发过程,以及保持健全与完整性,等等。每一个“小球”都会带来其固有的风险,有些显而易见,有些则不易发现。尽管如此,所有这些风险都是完全可以避免的。本文作者Humphrey Sheil分析了威胁到企业级Java项目成功的10大风险, 并一一列出了风险规避的策略方法。

--------------------------------------------------------------------------------

      在过去这段时期里,我担任过程序员、高级设计师以及架构设计师等工作,见识过很优秀的企业级Java项目,也见识过不好的,甚至很"丑陋"的项目。有时候我会自己问自己,为什么一个项目可以取得成功,而另一个却走向失败?很难定义出某种规则或标准来表明各个不同的项目应该如何成功,J2EE项目也并不例外。但与此相反的是,我们可以从各个角度和层次上去考察项目失败的原因,如果很好地避开了这些风险,项目就可以取得成功。在本文中,我将提出排名前10位的企业级Java项目风险,供读者参考。
在各种各样的风险中,有些风险只是延缓了项目的进度,有些带来了一些不必要的工作,而另一些则会把成功的可能性彻底地消除。不过,如果预先有了足够的准备和清醒的认识,那么并没有不可避免的事情。这好比如果你是一名旅行者,你清楚地知道前面的道路在什么方向,做了充分的准备,又有一位清楚知道哪里有危险的向导,这样就会比较顺利地到达自己的目的地。
本文采用了以下结构来描述风险: 
·         风险名称:风险的标题(使用粗体)
·         项目阶段:在哪个项目阶段会发生风险情况
·         影响阶段:会影响到以后的哪些阶段
·         症状:    风险产生时的症状
·         规避方案:如何规避风险或者把其对项目的影响降低到最小程度
·         备注:    风险相关的补充说明和提示
      通过对企业级Java项目的仔细考察,本文将J2EE项目过程分解为以下几个阶段:
·         提供商选择: 在开始你的J2EE项目之前,要选择最合适的提供商,从应用服务器到开发工具组合,一直至工作期间享用的咖啡的厂商。:) 
·         设计: 在遵照一系列严格的规范和软件工程方法的前提下,可以开始进行足够充分的设计, 然后再很自然地进入开发阶段。在开发之前,要周全地考虑好正在做什么,以及如何往下做的问题。另外,我使用了一些设计模板来确信在进入开发之前,已经想到了所有的问题和可能的解决方案。但是,我有时也在该阶段做一些编码,有时候这样做可以回答一些问题,有效地判断出性能上和模块划分上的问题。 
·         开发: 也就是程序开发阶段,选择一些好的开发工具,进行精良的设计等等,在这个阶段将显示其优越性,并且可以给开发带来很大的帮助。 
·         稳定性/负载测试:在该阶段,系统架构师和项目经理应该冻结住产品特性,并把焦点放在质量以及产品参数(允许的并发用户数量,故障恢复情况,等等)上。质量和性能在该阶段应得到足够的重视。当然,最好应该避免在前阶段写出不良的运行缓慢的代码而到本阶段来作很多的修改。
·         成熟期:这不是一个真正的项目阶段,而是一个固定的准备阶段。过去潜伏的错误(来自于糟糕的设计和开发、错误的厂商选择)可能出现并影响你的系统。
图1 由于各种原因而受到影响的项目阶段
 
OK,以下让我们进入 top 10 项目风险!
 

--------------------------------------------------------------------------------

风险1:没有真正理解 Java, EJB, 和J2EE

这个问题可以分解为3个部分,以便于分析。
描述: 没有真正理解Java
项目阶段:开发
影响阶段:设计、稳定性测试、成熟期
对系统性能的影响:可维护性、可扩展性、性能
症状:
·         重复开发了JDK核心API中的功能或类
·         不懂得以下列表中的某些项(这只是一些主题或者实际例子而已):
o        垃圾收集器 (train, generational, incremental, synchronous, asynchronous)
o        对象在何时能被进行垃圾收集 -- dangling references
o        使用的继承机制及其权衡
o        over-riding和over-loading方法
o        为什么java.lang.String (在这里用你所中意的类代替) 提供的性能不好
o        Java中的pass-by参考语义和EJB中pass-by值的语义的比较
o        使用 == 或者使用equals() 方法 for nonprimitives
o        在不同平台上Java线程的运行顺序方式(例如是否是抢先方式的)
o        新线程和本地线程的比较
o        Hotspot技术(以及为什么旧的性能调整技术降低了Hotspot 的优化效果)
o        JIT,以及什么时候好的JIT变得不好(未安装的JAVA编译器,以及你的代码运行得刚够良好)
o        API搜集
o        RMI
规避方案:
你需要不断改进Java方面的知识,尤其是深入了解Java的优势和不足之处。Java的存在价值已经远不止是一种语言,理解平台(JDK及工具等)也是同样重要的。具体地说,你应该是经过认证的Java程序员,如果你不是的话,也许你有时会为还有那么多不知道的内容而感到惊讶。另外,你可以加入Java的邮件列表。以前我曾加盟过的每一个公司都加入了这样的邮件列表,从同行中学到技术,这将是你最好的资源。
备注:
如果你或者你的团队中的成员不真正了解编程语言和平台,怎么还能保持成功的希望呢?强干的Java程序员之于EJB和J2EE,就象是鸭子之于水一样。与此相反,比较弱的、没有经验的程序员只能开发出质量低劣的J2EE应用程序。
描述: 没有真正理解EJB
项目阶段:
设计
影响阶段:
开发、稳定化
对系统的影响:
维护
症状:
·         EJB在第一次被调用后没有再被使用到(尤其是stateless session bean)
·         没有重复利用价值的EJB
·         不理解开发者要做什么,容器提供什么
·         EJB没有依照规范定义(fire线程, 加载了本地库,试图执行I/O,等等)
解决方案:
要改进关于EJB方面的知识,可以找一个周末来阅读EJB规范 (1.1版有314页),然后阅读2.0规范(524页!),这样可以了解到1.1没有定义到的而在2.0规范中补充的内容。EJB开发者从18.1及18.2章节开始阅读是比较合适的。
备注:
不要从提供商的角度去看EJB,要确切地知道规范所支持的标准EJB模型和基于这些模型的特殊应用之间的区别。这也会有助于你迁移到别的提供商的时候所用。
描述: 没有真正理解J2EE
项目阶段:
设计
影响阶段:
开发
对系统的影响:
维护、扩展性、性能
症状:
·         "Everything is an EJB"的设计方式
·         用手工事务管理取代了容器-提供的机制
·         自定义方式的安全处理 -- J2EE平台在企业级计算中,从表示逻辑到后台处理,已具有最完整的集成安全架构;但很少用到其全部功能。
解决方案:
学习J2EE的关键组件,并且了解它们的优缺点,依次用它们替代每一个服务;“知识就是力量”在这里是行之有效的。
备注:
只有知识能够弥补这些问题。好的Java开发者会成为好的EJB开发者,此后也应逐渐成为J2EE得道高手。Java和J2EE知识掌握得越多,设计和开发工作就会越出色。在设计阶段一切都会有条不紊。

--------------------------------------------------------------------------------

风险2: 过度设计(Over-engineering) (采用 EJB或者不采用EJB)
项目阶段:
设计
影响的项目阶段:
开发
对系统的影响:
维护、扩展性、性能
症状:
·         过于庞大的EJB
·         开发者无法解释EJB做什么,以及其间的联系
·         无法重复使用的EJB、组件或者服务
·         EJB启动了新的事务,而该事务本该由一个已存在的EJB启动
·         为了安全,把数据分离级别定得太高
解决方案:
过度工程化的解决之道直接来自于极限编程 (XP)方法:用最小的设计和编程来满足需求,除此之外别无它干。除非你需要明确知道今后可能的需求,如将来的负载要求,或者系统在最高负载下的表现,否则大可不必为系统将来的情况做太多考虑或猜测。另外,J2EE平台已经定义了可伸缩性及出错恢复等特性,可以让服务器系统为你进行处理。
在最小的系统中,只包含一个个小组件,这些组件只做一件事,只要把这些要求做到的进行实现,系统稳定性就已经得到了提高,而且,你的系统的可维护性会变得很强,在未来要增加功能以满足新的需求也将变得容易。
备注:
除了上面所列方案之外,可以推行设计模式 -- 它们可以显著地改进你的系统设计。EJB模型本身也广泛使用了设计模式。例如,每个EJB所带的Home 接口就是Finder和Factory模式的实例。EJB的remote接口扮演了一种实际bean实现的代理,并且对于提供容器的能力也是至关重要的,这些容器截取调用信号并提供诸如透明(transparent)负载均衡的服务。忽视设计模式也是危险的一部分。
我常提到要反对的另外一种危险是:仅仅是为了使用EJB而使用EJB。在你的应用中的某一部分可能并不需要EJB,甚至你的整个应用都不需要。这是过度工程化所走的极端,而且我确实也目睹了一些良好的servlet和JavaBean应用被重构为EJB,而这样做并没有很好的技术上的理由。

--------------------------------------------------------------------------------

风险3: 没有将业务规则和逻辑表现形式相分离
项目阶段:
设计
影响的项目阶段:
开发
对系统的影响:
维护、扩展性、性能
症状:
·         过于庞大、没有边际的JSP程序
·         在业务逻辑改变的时候必须修改JSP
·         在要求改变界面显示的时候需要修改并重新配置EJB和其它后台组件
规避方案:
J2EE平台使你有机会将表示逻辑和导航控制相分离,进而与业务规则相分离。这被称为模式2结构。
备注:
可以使用具有一致性的设计来进行用户界面框架的连接。(例如可以使用taglib),这将帮助你避免逻辑分离的问题。有许多现成的好的方法可供选择。对每一个分别进行评估,然后采用最合适的框架。

--------------------------------------------------------------------------------

风险4: 没有在开发环境中进行适当的配置
项目阶段:
开发
影响的项目阶段:
稳定化、并发、成熟期
对系统的影响:
你的权衡
症状:
·         经过多日或数周的时间才能过渡到成熟系统
·         风险存在与过渡期,带有很多不确定性,有些主要的功能场景没有被测试到
·         实际系统中的数据和开发、测试中的数据不同
·         无法在开发者机器上进行组建
·         应用行为在开发、稳定化及产品环境中各不相同
规避方案:
解决之道是忠实地在开发环境中配置实际的环境,让开发所用环境接近于要实施产品的环境。如果未来环境是JDK 1.2.2及Solaris 7,那么不要在JDK 1.3及Red Hat Linux上进行开发。对于所用的应用服务器也是如此。同样,要快速地看一下产品数据库中的数据,并将这样的数据用于测试。不要依赖于人工创建的数据。如果产品数据很敏感,则要使之变得不敏感,然后把它配置起来。开发中未能预期到的产品数据将对以下过程产生破坏:
·         数据检验规则
·         系统测试行为
·         系统组件构建(特别地包括:EJB-EJB以及EJB-数据库)
最为糟糕的是,这样还可能产生异常、空指针,以及你从没见过的问题。
备注:
开发人员常把安全性问题放到稳定化阶段才开始解决。要防止这样的陷阱产生,你也可以花费同样多的时间在业务逻辑中改进安全性。
成熟期是一个复杂的过程,其中充满了技术性问题和非技术性问题。你可能会陷于想不到的一大堆问题中,这就是成熟化所意味的一切。开发及稳定化环境过程为你提供了制造更多这样的问题,以及发现这样的问题的地方,不断去做,就可以大大减少风险。
你做的工程越多,你就越能了解什么是可行的,什么是不可行的。你可以对工程问题进行记录,以避免同样的错误重复发生。

--------------------------------------------------------------------------------

风险5: 选择了错误的提供商
项目阶段:
提供商选择
影响阶段:
设计、开发、稳定化/负载测试,成熟化
对系统的影响:
可伸缩性、性能、可维护性及稳定性
症状:
·         开发人员要使用更多的时间来处理工具方面的问题,而不是很有成效地使用这些工具
·         为了应付已知的和未知的问题,而不得不进行显著的系统重新设计
·         在不同的工具之间很难进行集成(应用服务器与IDE工具,IDE工具与调试器,源码控制与合成工具,等等)
·         对于IDE工具和调试器等,开发人员往往排斥它们,而推崇自己所喜欢的工具
规避方案:
为了避免风险5,你需要一个很好的提供商选择过程,风险10的规避也适用于此。
要真正衡量一种IDE工具是否最合适的方法是真正地进行使用。而唯一来评估一种J2EE应用的方法是建立一种概念试验来进行证明,在试验中要包含你的应用框架。事实上,你也不希望在花费了3个月时间进行了培训和开发后,在使用时又发现一些bug。
假设在开发到一半的时候,突然发现你的工具集有问题,那么你早应该知道,有些工具确实比另一些更重要。如果你所选的应用服务器不能充分满足你的需要,你只好修改原先的设定。如果IDE不好,则需要设置最低限度的代码标准,并让开发人员任意选择他们认为最为有效的工具。
备注:
要真正了解到哪一个供应商对一项特殊的任务来说最合适,其实并不是一件一次性决定的事情。你需要不断地跟踪与评估这个市场。例如,在过去的一年里我用过4种不同的IDE工具,这取决于我使用了什么样的应用服务器、平台,是否使用EJB等。

--------------------------------------------------------------------------------

风险6: 不了解你的提供商
项目阶段:
提供商选择
影响阶段:
提供商选择阶段后面的所有阶段:设计、开发、稳定化/负载测试、成熟化
对系统的影响:
可维护性、可伸缩性、性能
症状:
·         开发所用周期超过了最坏预测的周期1/3以上
·         提供商已经提供了某项功能,但开发者在不知道的情况下重新进行了该项功能的开发
规避方案:
为了规避这样的风险,你可以尽可能地订阅提供商的网上资源,例如邮件列表、新闻组、版本信息(尤其是其中的bug修复补丁的说明等),你能从中得到无法估量之多的收获。
一旦你已经选定了提供商,那么立即就要投资进行培训,并且尽可能赶在项目启动以前。然后,逐渐在团队中建立起对此提供商的认识及信任。试着建立几个EJB并部署一下,再用你的表示层技术 (Swing GUI, JSP等)来调用它们。如果你既要搭建开发环境,又要同时在实现项目目标,就会产生一些不必要的冲突。实际上,我也见到过一直没有进行构建过程的情况:“我们没有时间。”因此,这些工作必须提早进行。有些人会说:“我们的计划中没有为我们提供这些时间。”我的回答是:“你的计划中并没有不给你时间使你不这么做啊。”
备注:
在J2EE世界里,各提供商产品的技术兼容性究竟如何?让我们看一下IBM和BEA的具体分析吧。两者都分别在各自的应用服务器中支持EJB 1.1。那么,实际上BEA WebLogic 5.1和IBM WebSphere 3.5究竟有多少相似之处呢?
1.        BEA WebLogic和IBM WebSphere的系统配置和管理方式几乎完全不同。
2.        IBM在WebSphere中采用了全面的GUI环境,而与之相对的是,BEA 在WebLogic中提供一整套命令行。
3.        IBM WebSphere使用IIOP来和CORBA异常进行通讯,这些异常对程序员来说是可见的;WebLogic根本没有CORBA构造,而缺省使用t3协议。
4.        WebSphere和Visual Age衔接紧密,而WebLogic是IDE无关的,实际上,你几乎可以使用任何的开发工具。
由此可见,差异还是相当多。如果你是一种应用服务器的专家,并不意味着你就是所有应用服务器的专家。这种区别体现在IDE,debugger,build工具,配置管理等等方面。具备某提供商的某项特殊工具的使用经验,可以在评估该提供商的竞争对手产品时具有一些便利。但是,不要奢望在不同产品之间进行无缝的转移或衔接。因此,你不得不花费足够多的时间在熟练掌握这些工具上。

--------------------------------------------------------------------------------

风险7: 设计中没有充分考虑到可伸缩性和产品性能
项目阶段:
设计
受影响的项目阶段:
开发、负载测试及成熟化
对系统的影响:
可伸缩性、性能、可维护性
症状:
·         无法忍受的速度缓慢
·         系统给服务器端增加的沉重负担,而无法利用到一些聚簇技术。
规避方案:
把精力集中于性能和可伸缩性方面的需求,明确开发中要达到的性能指标。如果你需要每秒50个事务,而你的EJB设计只能提供40个,那么你就需要考虑替代方案,诸如存储过程,批处理,或者重新考虑OLTP的设计。
尽可能让你的提供商加入进来,他们应该非常清楚其产品的强项和弱处在哪里,然后给你提供最直接的帮助。
备注:
本风险与风险2 (over-engineering)似乎有些冲突。实际上,两者相互影响。 我对风险2给出的解决方案是,只在绝对必要的情况下才进行构建。而对与性能和可伸缩性,你要预先划分好什么是必须要做的。
如果你实现就识别出系统需要非常强的可伸缩性,并把它作为一个比较关键的需求,那么你首先需要选择一个带有很强的簇支持及事务型缓存的应用服务器。另外,你应把业务对象设计为EJB,从而可以充分利用服务器架构的优势。 XP也没有问题,你仍然是只做绝对必要的工作。
我把这样的观点看作是一种检查和平衡的方法。我们只需要最简单可能性的系统,该系统只提供客户所需要的功能与行为即可。

--------------------------------------------------------------------------------

风险8: 陈旧的开发过程
项目阶段:
开发
影响阶段:
稳定化,成熟化
对系统的影响:
可维护性、代码质量
症状:
·         项目计划看上去似乎类似于瀑布模型: “首先草构设计,然后在一个很长的周期里进行开发。”
·         由于不存在构建(build)过程,每次构建都象是噩梦
·         构建的日期等于损失开发的日期,因为什么也没有做成
·         在集成以前组件没有分别被充分地测试过,而集成测试意味着将2个不稳定的组件放在一起,然后查看堆栈里的跟踪结果。
规避方案:
好的软件方法学将提高你的软件生命期。此前我已经提到XP方法,你可以在网上找到很多这方面的资料。
备注:
JUnit可以用来进行单元测试,Ant工具可以进行编译与构建,这2种工具都对XP方法有很好的支持。

--------------------------------------------------------------------------------

风险9: 没有好的架构方式
项目阶段:
开发
影响阶段:
开发、稳定化、成熟期
对系统的影响:
可维护性、可伸缩性、代码质量
症状:
·         在代码中使用了很多次的核心库中发现Bug。
·         没有建立日志标准 -- 于是系统的输出很难读取或者解析。
·         不良的不一致的异常处理。在有些站点中我们甚至可以看到,出错信息直接暴露给了最终用户,例如在用户在他的购物车核帐时发送一条SQLException堆栈跟踪信息,用户接着会怎么做?打电话给数据库管理员要求对primary key约束进行修补吗?
以下任务已经被开发者以各种方式处理了无数次了,这些都有必要放在任何构架设计的第一批目标中。 
·         日志
·         异常处理
·         与资源的连接(数据库,名字服务等)
·         构建JSP页
·         数据合法性检查
规避方案:
我是一个轻方法学的信徒和实践者。我在JavaWorld 上的第一篇文章 -- "Frameworks Save the Day" -- 就是研讨在企业Java环境中的架构。即使你已经开始开发了,此时考虑一下架构仍然是值得的。可能你不得不忍受一下重构带来的异常处理和日志处理,但从长远来看还是值得的,这样即省时间又省钱。
备注:
让我们想一下在构架中基于组件开发的可重用性的不同等级。第一级别是plumbing,具有0.9以上的可重用比例,也就是说,有90%的项目可以对它重复利用。 服务定义得越详细,重用比例就越低。换句话说,我需要构建一个会计服务,但要提供这些资源与用法的管理,以便于其它50%项目中可以对它们进行重复利用。但是对那些项目来说,能得到这些资源,那真是太好了!

--------------------------------------------------------------------------------

风险10: 项目计划和设计基于市场效应,而脱离了技术现实
备注: 不断有新人加入到Java/EJB的开发领域中来,不理解Java的人数一般比想象中还要多。
项目阶段:
所有阶段都会受到影响,包括提供商的选择
影响阶段:
所有阶段都会受到影响
对系统的影响:
可维护性、可扩展性、设计质量、代码质量
症状:
·         轻率地进行技术决策,认为EJB只是为了便携式处理的方便
·         选择提供商的时候没有随即进行产品的试用
·         在项目的生命周期内还需要更换工具
规避方案:
不要轻易相信项目外部的任何人的看法,这些人可能已经有一些既得利益,不要相信提供商的说法(除非你早已经了解),也不要相信白皮书。如果你要取得来自真实世界的关于应用服务器的建议,可以在网上取得。你还可以下载这些工具进行评估,用它们做一些原型,并运行一下其中的样例。(好的提供商都有这样的样例)。
总的来说,为你的项目选择最好的提供商及工具需要时间,而你可能没有太多的时间。你可以把选择范围限制在3-4个对象,然后用一周时间进行比较和检验。最后从中选出比较满意的工具和产品。
备注:
如果你缺少J2EE经验,则可能会在项目前期就产生问题。在前期所确定的决策会影响整个过程,并进而影响项目的成功。好的J2EE咨询专家将能够帮助你选择好的提供商,并为设计和开发刻划出一个好的构形。

--------------------------------------------------------------------------------

仅仅只有这10项风险吗?

10只是一个特定的数字,显然,还有更多更多的风险会存在。只是我可以保证的是,如果你克服了所列的各项风险,那么你的项目会有出色的表现并已打好了成功的基础。
还有一项需要注意,即没有任何东西可以代替经验和计划。如果你没有经验,那么一定要想办法取得并积累。千万不要一边做项目一边进行培训。在开发之前要预先做好充分的准备,最好是在设计以前就进行准备。可以让你的团队接受Java/J2EE顾问的指导,并确保这样的指导能够传递到整个其他的团队成员。
最后,还有必要提到以下几点:
·         软件工程的外界影响
·         什么时候进行单元测试,什么时候进行集成测试?
·         设计模式
·         异常处理
结论
总的说来,以上10大风险是你在企业级Java项目开发过程中将面对的主要困难。我也相信在你的旅程中一定还有更多的陷阱,但我比较确信的是我所提到的风险已经涵盖了主要的问题。最后让我们按照优先级重新列举一下10大风险: 
1.        没有真正理解Java, 没有真正理解EJB, 没有真正理解J2EE
2.        过度设计(Over-engineering) 
3.        没有将业务规则和逻辑表现形式相分离
4.        没有在开发环境中进行适当的配置
5.        选择了错误的提供商
6.        不了解你的提供商
7.        设计中没有充分考虑到可伸缩性和产品性能
8.        陈旧的开发过程
9.        没有好的架构方式
10.     项目计划和设计基于市场效应,而脱离了技术现实
最后,让我祝你好运! 

--------------------------------------------------------------------------------

译后记:
我基本上没有做过J2EE项目,但仍有足够勇气翻译这样的文章。在国内软件公司里,极端情况下也许到处都是风险,这样也就无所谓风险了。对于选择J2EE技术路线,自然会有J2EE特有的风险,因此本文中的风险往往也是特别针对J2EE项目的。另外,对于J2EE项目,我们不应该忽视的一点是,其技术上的风险会更大一些。
 

- 作者: tonnyshen 2005年10月26日, 星期三 11:37  回复(0) |  引用(0) 加入博采

Java的垃圾回收机制详解和调优
Java的垃圾回收机制详解和调优

1.JVM的gc概述

  gc即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存。java语言并不要求jvm有gc,也没有规定gc如何工作。不过常用的jvm都有gc,而且大多数gc都使用类似的算法管理内存和执行收集操作。

  在充分理解了垃圾收集算法和执行过程后,才能有效的优化它的性能。有些垃圾收集专用于特殊的应用程序。比如,实时应用程序主要是为了避免垃圾收集中断,而大多数OLTP应用程序则注重整体效率。理解了应用程序的工作负荷和jvm支持的垃圾收集算法,便可以进行优化配置垃圾收集器。

  垃圾收集的目的在于清除不再使用的对象。gc通过确定对象是否被活动对象引用来确定是否收集该对象。gc首先要判断该对象是否是时候可以收集。两种常用的方法是引用计数和对象引用遍历。

  1.1.引用计数

  引用计数存储对特定对象的所有引用数,也就是说,当应用程序创建引用以及引用超出范围时,jvm必须适当增减引用数。当某对象的引用数为0时,便可以进行垃圾收集。

  1.2.对象引用遍历

  早期的jvm使用引用计数,现在大多数jvm采用对象引用遍历。对象引用遍历从一组对象开始,沿着整个对象图上的每条链接,递归确定可到达(reachable)的对象。如果某对象不能从这些根对象的一个(至少一个)到达,则将它作为垃圾收集。在对象遍历阶段,gc必须记住哪些对象可以到达,以便删除不可到达的对象,这称为标记(marking)对象。

  下一步,gc要删除不可到达的对象。删除时,有些gc只是简单的扫描堆栈,删除未标记的未标记的对象,并释放它们的内存以生成新的对象,这叫做清除(sweeping)。这种方法的问题在于内存会分成好多小段,而它们不足以用于新的对象,但是组合起来却很大。因此,许多gc可以重新组织内存中的对象,并进行压缩(compact),形成可利用的空间。

  为此,gc需要停止其他的活动活动。这种方法意味着所有与应用程序相关的工作停止,只有gc运行。结果,在响应期间增减了许多混杂请求。另外,更复杂的gc不断增加或同时运行以减少或者清除应用程序的中断。有的gc使用单线程完成这项工作,有的则采用多线程以增加效率。

  2.几种垃圾回收机制

  2.1.标记-清除收集器

  这种收集器首先遍历对象图并标记可到达的对象,然后扫描堆栈以寻找未标记对象并释放它们的内存。这种收集器一般使用单线程工作并停止其他操作。

  2.2.标记-压缩收集器

  有时也叫标记-清除-压缩收集器,与标记-清除收集器有相同的标记阶段。在第二阶段,则把标记对象复制到堆栈的新域中以便压缩堆栈。这种收集器也停止其他操作。

  2.3.复制收集器

  这种收集器将堆栈分为两个域,常称为半空间。每次仅使用一半的空间,jvm生成的新对象则放在另一半空间中。gc运行时,它把可到达对象复制到另一半空间,从而压缩了堆栈。这种方法适用于短生存期的对象,持续复制长生存期的对象则导致效率降低。

  2.4.增量收集器

  增量收集器把堆栈分为多个域,每次仅从一个域收集垃圾。这会造成较小的应用程序中断。

  2.5.分代收集器

  这种收集器把堆栈分为两个或多个域,用以存放不同寿命的对象。jvm生成的新对象一般放在其中的某个域中。过一段时间,继续存在的对象将获得使用期并转入更长寿命的域中。分代收集器对不同的域使用不同的算法以优化性能。

  2.6.并发收集器

  并发收集器与应用程序同时运行。这些收集器在某点上(比如压缩时)一般都不得不停止其他操作以完成特定的任务,但是因为其他应用程序可进行其他的后台操作,所以中断其他处理的实际时间大大降低。

  2.7.并行收集器

  并行收集器使用某种传统的算法并使用多线程并行的执行它们的工作。在多cpu机器上使用多线程技术可以显著的提高java应用程序的可扩展性。

  3.Sun HotSpot

  1.4.1 JVM堆大小的调整

  Sun HotSpot 1.4.1使用分代收集器,它把堆分为三个主要的域:新域、旧域以及永久域。Jvm生成的所有新对象放在新域中。一旦对象经历了一定数量的垃圾收集循环后,便获得使用期并进入旧域。在永久域中jvm则存储class和method对象。就配置而言,永久域是一个独立域并且不认为是堆的一部分。

  下面介绍如何控制这些域的大小。可使用-Xms和-Xmx 控制整个堆的原始大小或最大值。

  下面的命令是把初始大小设置为128M:

  java –Xms128m

  –Xmx256m为控制新域的大小,可使用-XX:NewRatio设置新域在堆中所占的比例。

  下面的命令把整个堆设置成128m,新域比率设置成3,即新域与旧域比例为1:3,新域为堆的1/4或32M:

java –Xms128m –Xmx128m
–XX:NewRatio =3可使用-XX:NewSize和-XX:MaxNewsize设置新域的初始值和最大值。

  下面的命令把新域的初始值和最大值设置成64m:

java –Xms256m –Xmx256m –Xmn64m

  永久域默认大小为4m。运行程序时,jvm会调整永久域的大小以满足需要。每次调整时,jvm会对堆进行一次完全的垃圾收集。

  使用-XX:MaxPerSize标志来增加永久域搭大小。在WebLogic Server应用程序加载较多类时,经常需要增加永久域的最大值。当jvm加载类时,永久域中的对象急剧增加,从而使jvm不断调整永久域大小。为了避免调整,可使用-XX:PerSize标志设置初始值。

  下面把永久域初始值设置成32m,最大值设置成64m。

java -Xms512m -Xmx512m -Xmn128m -XX:PermSize=32m -XX:MaxPermSize=64m

  默认状态下,HotSpot在新域中使用复制收集器。该域一般分为三个部分。第一部分为Eden,用于生成新的对象。另两部分称为救助空间,当Eden充满时,收集器停止应用程序,把所有可到达对象复制到当前的from救助空间,一旦当前的from救助空间充满,收集器则把可到达对象复制到当前的to救助空间。From和to救助空间互换角色。维持活动的对象将在救助空间不断复制,直到它们获得使用期并转入旧域。使用-XX:SurvivorRatio可控制新域子空间的大小。

  同NewRation一样,SurvivorRation规定某救助域与Eden空间的比值。比如,以下命令把新域设置成64m,Eden占32m,每个救助域各占16m:

java -Xms256m -Xmx256m -Xmn64m -XX:SurvivorRation =2

  如前所述,默认状态下HotSpot对新域使用复制收集器,对旧域使用标记-清除-压缩收集器。在新域中使用复制收集器有很多意义,因为应用程序生成的大部分对象是短寿命的。理想状态下,所有过渡对象在移出Eden空间时将被收集。如果能够这样的话,并且移出Eden空间的对象是长寿命的,那么理论上可以立即把它们移进旧域,避免在救助空间反复复制。但是,应用程序不能适合这种理想状态,因为它们有一小部分中长寿命的对象。最好是保持这些中长寿命的对象并放在新域中,因为复制小部分的对象总比压缩旧域廉价。为控制新域中对象的复制,可用-XX:TargetSurvivorRatio控制救助空间的比例(该值是设置救助空间的使用比例。如救助空间位1M,该值50表示可用500K)。该值是一个百分比,默认值是50。当较大的堆栈使用较低的sruvivorratio时,应增加该值到80至90,以更好利用救助空间。用-XX:maxtenuring threshold可控制上限。

  为放置所有的复制全部发生以及希望对象从eden扩展到旧域,可以把MaxTenuring Threshold设置成0。设置完成后,实际上就不再使用救助空间了,因此应把SurvivorRatio设成最大值以最大化Eden空间,设置如下:

java … -XX:MaxTenuringThreshold=0 –XX:SurvivorRatio=50000 …

  4.BEA JRockit JVM的使用

  Bea WebLogic 8.1使用的新的JVM用于Intel平台。在Bea安装完毕的目录下可以看到有一个类似于jrockit81sp1_141_03的文件夹。这就是Bea新JVM所在目录。不同于HotSpot把Java字节码编译成本地码,它预先编译成类。JRockit还提供了更细致的功能用以观察JVM的运行状态,主要是独立的GUI控制台(只能适用于使用Jrockit才能使用jrockit81sp1_141_03自带的console监控一些cpu及memory参数)或者WebLogic Server控制台。

  Bea JRockit JVM支持4种垃圾收集器:

  4.1.1.分代复制收集器

  它与默认的分代收集器工作策略类似。对象在新域中分配,即JRockit文档中的nursery。这种收集器最适合单cpu机上小型堆操作。

  4.1.2.单空间并发收集器

  该收集器使用完整堆,并与背景线程共同工作。尽管这种收集器可以消除中断,但是收集器需花费较长的时间寻找死对象,而且处理应用程序时收集器经常运行。如果处理器不能应付应用程序产生的垃圾,它会中断应用程序并关闭收集。

  分代并发收集器 这种收集器在护理域使用排它复制收集器,在旧域中则使用并发收集器。由于它比单空间共同发生收集器中断频繁,因此它需要较少的内存,应用程序的运行效率也较高,注意,过小的护理域可以导致大量的临时对象被扩展到旧域中。这会造成收集器超负荷运作,甚至采用排它性工作方式完成收集。

  4.1.3.并行收集器

  该收集器也停止其他进程的工作,但使用多线程以加速收集进程。尽管它比其他的收集器易于引起长时间的中断,但一般能更好的利用内存,程序效率也较高。

  默认状态下,JRockit使用分代并发收集器。要改变收集器,可使用-Xgc:<gc_name>,对应四个收集器分别为gencopy,singlecon,gencon以及parallel。可使用-Xms和-Xmx设置堆的初始大小和最大值。要设置护理域,则使用-Xns:java –jrockit –Xms512m –Xmx512m –Xgc:gencon –Xns128m…尽管JRockit支持-verbose:gc开关,但它输出的信息会因收集器的不同而异。JRockit还支持memory、load和codegen的输出。

  注意 :如果 使用JRockit JVM的话还可以使用WLS自带的console(C:\bea\jrockit81sp1_141_03\bin下)来监控一些数据,如cpu,memery等。要想能构监控必须在启动服务时startWeblogic.cmd中加入-Xmanagement参数。

  5.如何从JVM中获取信息来进行调整

  -verbose.gc开关可显示gc的操作内容。打开它,可以显示最忙和最空闲收集行为发生的时间、收集前后的内存大小、收集需要的时间等。打开-xx:+ printgcdetails开关,可以详细了解gc中的变化。打开-XX: + PrintGCTimeStamps开关,可以了解这些垃圾收集发生的时间,自jvm启动以后以秒计量。最后,通过-xx: + PrintHeapAtGC开关了解堆的更详细的信息。为了了解新域的情况,可以通过-XX:=PrintTenuringDistribution开关了解获得使用期的对象权。

  6.Pdm系统JVM调整

  6.1.服务器:前提内存1G 单CPU

  可通过如下参数进行调整:-server 启用服务器模式(如果CPU多,服务器机建议使用此项)

  -Xms,-Xmx一般设为同样大小。 800m

  -Xmn 是将NewSize与MaxNewSize设为一致。320m

  -XX:PerSize 64m

  -XX:NewSize 320m 此值设大可调大新对象区,减少Full GC次数

  -XX:MaxNewSize 320m

  -XX:NewRato NewSize设了可不设。

  -XX: SurvivorRatio

  -XX:userParNewGC 可用来设置并行收集

  -XX:ParallelGCThreads 可用来增加并行度

  -XXUseParallelGC 设置后可以使用并行清除收集器

  -XX:UseAdaptiveSizePolicy 与上面一个联合使用效果更好,利用它可以自动优化新域大小以及救助空间比值

  6.2.客户机:通过在JNLP文件中设置参数来调整客户端JVM

  JNLP中参数:initial-heap-size和max-heap-size

  这可以在framework的RequestManager中生成JNLP文件时加入上述参数,但是这些值是要求根据客户机的硬件状态变化的(如客户机的内存大小等)。建议这两个参数值设为客户机可用内存的60%(有待测试)。为了在动态生成JNLP时以上两个参数值能够随客户机不同而不同,可靠虑获得客户机系统信息并将这些嵌到首页index.jsp中作为连接请求的参数。

  在设置了上述参数后可以通过Visualgc 来观察垃圾回收的一些参数状态,再做相应的调整来改善性能。一般的标准是减少fullgc的次数,最好硬件支持使用并行垃圾回收(要求多CPU)。

- 作者: tonnyshen 2005年10月26日, 星期三 11:35  回复(0) |  引用(0) 加入博采

JVM之class文件结构
cloud    Matrix-与Java共舞
 

 

JVM之class文件结构
 定义:u1   1个字节为单位的非负值
         u2   2个字节为单位的非负值
         u3   . . . . . . . .  (其他以此类推 )
 
 
    Java文件结构用类似struct的描述如下:
 
    ClassFile {
            u4 magic;               // 必须为: 0xCAFEBABE


            u2 minor_version;
            u2 major_version;       //CLASS文件结构主次版本号 JAVA2支持45.0-46.
0
            u2 constant_pool_count; //记录常量信息
            cp_info constant_pool[constant_pool_count-1];   //计数从1开始
            u2 access_flags;        //class/interface访问权限
            u2 this_class;          //指向constant_poll中的有效索引值
            u2 super_class;         //0或指向constant_poll中的有效索引值,对于in
terface必须为非0
            u2 interfaces_count;    //superinterfaces的个数
            u2 interfaces[interfaces_count];  //计数[0,count-1) 对应constant_po
ol中的一个索引值
            u2 fields_count;
            field_info fields[fields_count];  //主要用于记录class及实例中的变量
            u2 methods_count;
            method_info methods[methods_count];
            u2 attributes_count;
            attribute_info attributes[attributes_count];
    }
 
    cp_info {
            u1 tag;


            u1 info[];
    }
        tag 意义如下:
 
        CONSTANT_Class                7
        CONSTANT_Fieldref             9
        CONSTANT_Methodref            10
        CONSTANT_InterfaceMethodref   11
        CONSTANT_String               8
        CONSTANT_Integer              3
        CONSTANT_Float                4
        CONSTANT_Long                 5
        CONSTANT_Double               6
        CONSTANT_NameAndType          12
        CONSTANT_Utf8                 1
 
        此时cp_info分别对应结构变化为
        1. CONSTANT_Class
        CONSTANT_Class_info {
                u1 tag;
                u2 name_index;
        }


 
        2. CONSTANT_Fieldref
          CONSTANT_Fieldref_info {
                    u1 tag;
                    u2 class_index;  //constant_pool的索引,对应CONSTANT_Class_info
                    u2 name_and_type_index;//constant_pool的索引,对应CONSTANT_NameAndType_info
          }
 
        3. CONSTANT_Methodref
           CONSTANT_Methodref_info {
                    u1 tag;
                    u2 class_index;
                    u2 name_and_type_index;
           }
 
        4. CONSTANT_InterfaceMethodref
           CONSTANT_InterfaceMethodref_info {
                    u1 tag;
                    u2 class_index;
                    u2 name_and_type_index;


           }
 
        5. CONSTANT_String
           CONSTANT_String_info {
                    u1 tag;
                    u2 string_index;
            }
 
        6. CONSTANT_Integer
            CONSTANT_Integer_info {
                    u1 tag;
                    u4 bytes;
            }
 
 
        7. CONSTANT_Float
           CONSTANT_Float_info {
                    u1 tag;
                    u4 bytes;
           }
 
        8. CONSTANT_Long


            CONSTANT_Long_info {
                    u1 tag;
                    u4 high_bytes;
                    u4 low_bytes;
             }
 
        9. CONSTANT_Double
           CONSTANT_Double_info {
                    u1 tag;
                    u4 high_bytes;
                    u4 low_bytes
           }
 
        10.CONSTANT_NameAndType
            CONSTANT_NameAndType_info {
                    u1 tag;
                    u2 name_index;
                    u2 descriptor_index;
            }
 
        11.CONSTANT_Utf8
            CONSTANT_Utf8_info {


                    u1 tag;
                    u2 length;
                    u1 bytes[length];
            }
 
    access_flags意义如下:
 
        ACC_PUBLIC     0x0001
        ACC_FINAL      0x0010
        ACC_SUPER      0x0020
        ACC_INTERFACE  0x0200
        ACC_ABSTRACT   0x0400
 
      如果是interface那么必须置ACC_INTERFACE,如果没有置ACC_INTERFACE则定义的是一
个类而非接口。
      如果设置了ACC_INTERFACE,那么ACC_ABSTRACT位也必须被设置,当然也可以设置AC
C_PUBLIC。
      ACC_SUPER用以表明invokespecial语义,Sun公司老的JAVA编译器没有设置ACC_SUPER
,并且老的JVM
    忽略ACC_SUPER位,但新的编译器应该实现invokespecial语义。
      其他未指明的位保留将来使用,并且编译器应当将其置为0,同时Java虚拟机应当忽
略他们。


 
   this_class:  constant_pool中的索引值,指向的元素的cp_info等价为CONSTANT_Class
_info
 
        CONSTANT_Class_info {
            u1 tag;                 //必须为CONSTANT_Class (7)
            u2 name_index;          //为指向constant_pool中的一个索引值
        }
 
            name_index :指向的元素的cp_info等价为CONSTANT_Utf8_info
 
                CONSTANT_Utf8_info {
                       u1 tag;               //必须为CONSTANT_Utf8 (1)
                       u2 length;
                       u1 bytes[length];     //Utf8编码的字符串
                }
 
 
    field_info {
            u2 access_flags;   //访问控制权
            u2 name_index;     //constant_pool中的索引,对应于CONSTANT_Utf8_info描述。


            u2 descriptor_index; //constant_pool中的索引,对应于CONSTANT_Utf8_info描述。
            u2 attributes_count;
            attribute_info attributes[attributes_count]; //attribute_info将在mothods后描述。
    }
       field_info中access_flages意义如下:
 
           ACC_PUBLIC     0x0001
           ACC_PRIVATE    0x0002
           ACC_PROTECTED  0x0004
           ACC_STATIC     0x0008
           ACC_FINAL      0x0010
           ACC_VOLATILE   0x0040
           ACC_TRANSIENT  0x0080
 
           其中很显然不能同时为ACC_FINAL和ACC_VOLATILE 且前三项是互斥的。
           interface必须置ACC_PUBLIC, ACC_STATIC,ACC_FINAL位,且不能置其他位。
           其他未指明的位保留将来使用,并且编译器应当将其置为0,同时Java虚拟机应当忽略他们。
 
 


    methods指明了类中的所有方法。
 
        method_info {
                    u2 access_flags;
                    u2 name_index;   //指向constant_pool的入口,对应为CONSTANT_Utf8_info
                    u2 descriptor_index;  //指向constant_pool的入口,对应为CONSTANT_Utf8_info
                    u2 attributes_count;
                    attribute_info attributes[attributes_count];
                    //此处只能出现Code、Exceptions、Synthetic、Deprecated四种类型的属性
        }
           access_flags访问权描述如下:
            ACC_PUBLIC        0x0001
            ACC_PRIVATE       0x0002
            ACC_PROTECTED     0x0004
            ACC_STATIC        0x0008
            ACC_FINAL         0x0010
            ACC_SYNCHRONIZED  0x0020
            ACC_NATIVE        0x0100
            ACC_ABSTRACT      0x0400


            ACC_STRICT        0x0800
 
 
    attribute_info {
            u2 attribute_name_index;  //constant_pool中的索引,对应于CONSTANT_Utf8_info描述。
            u4 attribute_length;
            u1 info[attribute_length];
    }
 
    现在已经预定义的属性有:
 
      1. SourceFile : attribute_info被替代为:
 
           SourceFile_attribute {
                    u2 attribute_name_index;
                    u4 attribute_length;
                    u2 sourcefile_index; //指向constant_pool中的一个CONSTANT_Utf8_info 结构。
           }
 
      2. Constantvalue : attribute_info被替代为:


 
          Constantvalue_attribute {
                    u2 attribute_name_index;
                    u4 attribute_length;    //必须为2
                    u2 constantvalue_index;
          }
 
          对于constantvalue_index意义如下:
                  long                              CONSTANT_Long
                  float                             CONSTANT_Float
                  double                            CONSTANT_Double
                  int, short, char, byte, boolean   CONSTANT_Integer
                  String                            CONSTANT_String
 
          Constantvalue用于field_info 中,用于描述一个static常量,且此时field_info的access_flags应为ACC_STATIC
 
 
      3. Code : attribute_info被替代为:
 
           Code_attribute {
                    u2 attribute_name_index;


                    u4 attribute_length;
                    u2 max_stack;  //执行此函数时可用的栈的最大深度
                    u2 max_locals; //执行此函数可用到的最大本地变量数目,包括参数。
                                   // 注意:一个long/double相当于2个变量数目.
                    u4 code_length; //本函数用到的代码长度。
                    u1 code[code_length]; //实现本函数的真正字节码
                    u2 exception_table_length;
                    {   u2 start_pc;
                        u2 end_pc; //捕获违例时执行代码数组中的[start_pc, end_pc)部分
                        u2  handler_pc; //现在还不大明白他是干嘛的!!
                        u2  catch_type; //指向constant_pool的索引,对应CONSTANT_Class_info
                    }exception_table[exception_table_length];
                    u2 attributes_count;
                    attribute_info attributes[attributes_count];
           }
 
                CONSTANT_Class_info {
                    u1 tag;         //必须为CONSTANT_Class (7)
                    u2 name_index;  //不用我再说了吧?


                 }
 
           Code属性用于method_info结构中。
 
      4. Exceptions : attribute_info被替代为:
 
           Exceptions_attribute {
                  u2 attribute_name_index;
                  u4 attribute_length;
                  u2 number_of_exceptions;
                  u2 exception_index_table[number_of_exceptions];
           }
 
      5. InnerClasses  : attribute_info被替代为:
 
           InnerClasses_attribute {
                  u2 attribute_name_index;
                  u4 attribute_length;
                  u2 number_of_classes;
                  {  u2 inner_class_info_index;
                     u2 outer_class_info_index;
                     u2 inner_name_index;


                     u2 inner_class_access_flags;
                  } classes[number_of_classes];
            }
 
      6. Synthetic : attribute_info被替代为:
 
           Synthetic_attribute {
                 u2 attribute_name_index;  //不用废话了吧?
                 u4 attribute_length;     //必须为0
           }
          Synthetic用在 field_info、 method_info 中,
          一个没有出现在源程序中的变量必须使用Synthetic标记。
 
      7. LineNumberTable : attribute_info被替代为:
 
          LineNumberTable_attribute {
                    u2 attribute_name_index;
                    u4 attribute_length;
                    u2 line_number_table_length;
                    {  u2 start_pc;        //代码数组中的开始处
                       u2 line_number;     //源文件中的行号(对于每一非空行都有这么一项)


                    } line_number_table[line_number_table_length];
          }
          LineNumberTable用于Code属性中,通常用于调试。
 
 
      8. LocalVariableTable : attribute_info被替代为:
 
              LocalVariableTable_attribute {
                    u2 attribute_name_index;
                    u4 attribute_length;
                    u2 local_variable_table_length;
                    {   u2 start_pc;
                        u2 length; //当解释到代码数组的[start_pc,start_pc+length]
                                   //时变量必须被赋值??
                        u2 name_index;
                        u2 descriptor_index;
                        u2 index;  //到本地变量数组的一个索引
                    } local_variable_table[local_variable_table_length];
              }
 
      9. Deprecated : attribute_info被替代为:


 
              Deprecated_attribute {
                        u2 attribute_name_index;
                    u4 attribute_length;   //必须为0
              }
 
 
      当然你也可以定义自己的属性,但要你自己的编译器和虚拟机实现。JVM将忽略自己
不认可的属性。
 
来实践一下吧!
编写一个最简单的程序:
class Test
{
        public static void main(String[] args)
        {
                System.out.println("Hello World!");
        }
}
 
c:\work>javac Test.java
 


c:\work>filedump Test.class
 
File Dump V0.3 Beta by cloud (safesuite@363.net).
 
01:00    ca fe ba be 00 03 00 2d 00 1d 0a 00 06 00 0f 09   .......-........
01:01    00 10 00 11 08 00 12 0a 00 13 00 14 07 00 15 07   ................
01:02    00 16 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29   .....<init>...()
01:03    56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e   V...Code...LineN
01:04    75 6d 62 65 72 54 61 62 6c 65 01 00 04 6d 61 69   umberTable...mai
01:05    6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67   n...([Ljava/lang
01:06    2f 53 74 72 69 6e 67 3b 29 56 01 00 0a 53 6f 75   /String;)V...Sou
01:07    72 63 65 46 69 6c 65 01 00 09 54 65 73 74 2e 6a   rceFile...Test.j
>d
02:00    61 76 61 0c 00 07 00 08 07 00 17 0c 00 18 00 19   ava.............
02:01    01 00 0c 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 07   ...Hello World!.
02:02    00 1a 0c 00 1b 00 1c 01 00 04 54 65 73 74 01 00   ..........Test..
02:03    10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63   .java/lang/Objec
02:04    74 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79   t...java/lang/Sy
02:05    73 74 65 6d 01 00 03 6f 75 74 01 00 15 4c 6a 61   stem...out...Lja
02:06    76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61   va/io/PrintStrea
02:07    6d 3b 01 00 13 6a 61 76 61 2f 69 6f 2f 50 72 69   m;...java/io/Pri
>d


03:00    6e 74 53 74 72 65 61 6d 01 00 07 70 72 69 6e 74   ntStream...print
03:01    6c 6e 01 00 15 28 4c 6a 61 76 61 2f 6c 61 6e 67   ln...(Ljava/lang
03:02    2f 53 74 72 69 6e 67 3b 29 56 00 20 00 05 00 06   /String;)V. ....
03:03    00 00 00 00 00 02 00 00 00 07 00 08 00 01 00 09   ................
03:04    00 00 00 1d 00 01 00 01 00 00 00 05 2a b7 00 01   ............*...
03:05    b1 00 00 00 01 00 0a 00 00 00 06 00 01 00 00 00   ................
03:06    01 00 09 00 0b 00 0c 00 01 00 09 00 00 00 25 00   ..............%.
03:07    02 00 01 00 00 00 09 b2 00 02 12 03 b6 00 04 b1   ................
>d
04:00    00 00 00 01 00 0a 00 00 00 0a 00 02 00 00 00 05   ................
04:01    00 08 00 06 00 01 00 0d 00 00 00 02 00 0e         ..............
>
 
解读一下:
 
ca fe ba be     magic
00 03 00 2d     次主版本号,换算一下:   45.3 (注意,不是高字节在前,别犯职业病!)
00 1d           constant_pool元素个数加一  :  29  那么constant_pool就是[1-28]
constant_pool: 1-28
 
1.  0a 00 06 00 0f


                0x0a :CONSTANT_InterfaceMethodref  0x06 :class index  0x0f :name-type-index
2.  09 00 10 00 11
                0x09 : CONSTANT_Fieldref             0x10:  . . .       0x11 :
. . . .
3.  08 00 12     0x08 : CONSTANT_String    0x12 : string_index
4.  0a 00 13 00 14     0x0a同于1.
5.  07 00 15     0x07 : CONSTANT_Class     0x15 : name_index
6.  07 00 16
7.  01 00 06 3c 69 6e 69 74 3e 01  ...<init>
                0x01   CONSTANT_Utf8     0x06  : string length   "<init>" : 构造
函数
8.  01 00 03 28 29 56   ...()V   函数,无参数
                0x01   . . . . . .       0x03  : . . . .         "()V"    :  .
. .
9.  01 00 04 43 6f 64 65    ...Code
10. 01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65  ...LineNumberTable
11. 01 00 04 6d 61 69 6e    ...main
12. 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56
           ...([Ljava/lang/String;)V   函数,参数为String[]类型
13. 01 00 0a 53 6f 75 72 63 65 46 69 6c 65   ...SourceFile
14. 01 00 09 54 65 73 74 2e 6a 61 76 61      ...Test.java


15. 0c 00 07 00 08         0x0c:CONSTANT_NameAndType  07 : name-index 08:name-t
ype-index
16. 07 00 17
17. 0c 00 18 00 19
18. 01 00 0c 48 65 6c 6c 6f 20 57 6f 72 6c 64 21  ...Hello World!
19. 07 00 1a
20. 0c 00 1b 00 1c
21. 01 00 04 54 65 73 74   ...Test
22. 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74   ...java/lang/Obj
ect
23. 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d   ...java/lang/Sys
tem
24. 01 00 03 6f 75 74    ...out
25. 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b
       ...Ljava/io/PrintStream;
26. 01 00 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d...java/io
/PrintStream
27. 01 00 07 70 72 69 6e 74 6c 6e   ...println
28. 01 00 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56
       ...(Ljava/lang/String;)V
 
 


00 20    access_flags
00 05    this_class
00 06    super_class
00 00    interfaces_count
00 00    fields_count
00 02    methods_count
 
methods[2]:
 
method_info {
    u2 access_flags;
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}
0.   00 00     access_flags;
     00 07     name_index;     看看constant_pool的第7项: <init>  表明当前描述构造函数
     00 08     descriptor_index;
     00 01     attributes_count;
     00 09     attribute_name_index   0x09 看看constant_pool的第9项,简单明了Code !!
     00 00 00 1d  attribute_length   = 29
     00 01     max_stack
     00 01     max_locals
     00 00 00 05  code_length
     2a b7 00 01 b1       JVM定义的操作码代码段数组
     00 00    exception_table_length
     00 01    attributes_count     一个,接下来是attribute_info结构
     00 0a    attribute_name_index  看看constant_pool的第10项: LineNumberTable(显然调试用)
     00 00 00 06  attribute_length
     00 01    line_number_table_length
     00 00    start_pc
     00 01    line_number
 
1.   00 09    access_flags   PUBLIC & STATIC
     00 0b    name_index;                   表明当前描述main函数
     00 0c    descriptor_index;   ...([Ljava/lang/String;)V
     00 01    attributes_count;
     00 09    attribute_name_index   Code
     00 00 00 25    attribute_length   = 37
     00 02    max_stack


     00 01    max_locals
     00 00 00 09   code_length
     b2 00 02 12 03 b6 00 04 b1    代码数组  codeArray1[0-8]
     00 00    exception_table_length
     00 01    attributes_count    接下来是attribute_info结构
     00 0a    attribute_name_index  LineNumberTable
     00 00 00 0a  attribute_length
     00 02    line_number_table_length
     00 00    start_pc
     00 05    line_number
     00 08    start_pc     : codeArray1[8] = 0xb1 -->  return
     00 06    line_number  第6行源程序为  }
 
 
00 01     attributes_count
00 0d     attribute_name_index   属性为SourceFile
00 00 00 02  attribute_length
00 0e     sourcefile_index    constant_pool[0x0e] --- >  "Test.java"
 
 
接下来我们看看main()函数对应的代码:
 


b2 00 02 12 03 b6 00 04 b1
 
0xb2  0x00  0x02 : getstatic #2
         看看constant_pool[0x02] :09 00 10 00 11
                0x09 : CONSTANT_Fieldref         0x10:  class index   0x11 :name-type-index
          constant_pool[0x10]:  --> constant_pool[0x17]  : java/lang/System
          constant_pool[0x11]:    0x18 : class index   0x19 :name-type-index
                   constant_pool[0x18] : out
                   constant_pool[0x19] : Ljava/io/PrintStream
 
                     即 System.out 为 java.io.PrintStream 类型
 
 
 
0x12  0x03  : ldc #3
          看看 constant_pool[3] : 08 00 12     0x08 : CONSTANT_String    0x12 : string_index
                        指向一个字符串  constant_pool[0x12]: "Hello World!"
          故该指令加载了字符串索引到本地栈
0xb6  0x00  0x04: invokevirtual #4
                        ------->到constant_pool查查 0x13  :class   0x14 :name-t


                                               看看constant_pool[0x13]:java/io/PrintStream
                                               constant_pool[20] :-->  00 1b 00 1c
                                               constant_pool[0x1b]:println
                                               . . . .            :(Ljava/lang/String;)V
 
       故该指令调用了 java.io.PrintStream.println(java.lang.String)
           而参数则为 ldc #3 加载的 "Hello World!"
0xb1  : return


- 作者: tonnyshen 2005年01月25日, 星期二 11:47  回复(2) |  引用(0) 加入博采

什么是jvm?你很清楚地了解它吗?
2004-05-21    Kei    Matrix-与Java共舞   
 

 

什么是jvm?你很清楚地了解它吗?
 在Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(ByteCode),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。
    可以说,Java虚拟机是Java语言的基础。它是Java技术的重要组成部分。Java虚拟机是一个抽象的计算机,和实际的计算机一样,它具有一个指令集并使用不同的存储区域。它负责执行指令,还要管理数据、内存和寄存器。Java解释器负责将字节代码翻译成特定机器的机器代码。Java是一种简单的语言。它用到的概念不多,而且多为程序员所熟悉。如果你是一名程序员,掌握Java对你来说是易如反掌的事。即使你没有学过任何编程语言,学习Java也要比学习C++要容易的多。 
    由于Java最初是为控制电子产品设计的,因此它必须简单明了。为了保证这种简单性,Java去掉了C++中许多复杂的、冗余的、有二义性的概念,例如操作符重载、多继承、数据类型自动转换等。为了将程序员从复杂的内存管理的负担中解脱出来,同时也是为了减少错误,Java使用了自动内存垃圾收集机制,程序员只要在需要的时候申请即可,不需要释放,而由Java自己来收集、释放内存中的无用的块。
    与C++相比,Java有着更强的面向对象特性,是一种比较纯粹的面向对象语言。一般我们使用的一些所谓的面向对象的编程语言,如C++,Object Pascal等,实际上都是一种混合型的语言,即在过程式的语言中加上面向对象的扩展。在Java中,几乎万物皆对象,就连一些基本数据类型,如整型、字符型、浮点型等,在Java中都可以作为对象处理。Java的面向对象特性几乎可以与Smalltalk媲美,但是其适用于分布式计算环境的特性却远远超过了Smalltalk。
   Java是一种支持分布式操作的程序设计语言。使用Java提供的URL类,用户可以象访问本地文件一样访问网络上的对象,使用非常方便。在客户机/服务器的模式下,Java还可以将运算从服务器端分散到客户端,提高系统的效率,避免了服务器的瓶颈制约。Java的网络类库支持分布式的编程。Socket类提供可靠的流式网络的连接,支持TCP/IP协议。通过编写协议句柄,程序员还可以扩充Java支持的协议集合。
 
Java提供非常有效的安全控制。由于Java应用于网络程序的开发,因而安全性变的至关重要。因为Java小程序需要下载到客户端解释执行,所以,如果没有安全控制,就会给一些网络黑客以可乘之机,这对用户来说是非常危险的。所幸的是,Java的安全机制可以有效的防止病毒程序的产生、下载程序对本地文件系统的破坏,以及网络黑客窃取密码和入侵。
 
Java是一种非常健壮的语言。因为在Java中使用了以下手段:
 
不支持指针。在C++程序中,指针的错误使用通常的程序中BUG的元凶。在Java中彻底去掉了指针,杜绝了内存的非法访问,从而保证了程序的可靠性。
强类型语言。
 
自动内存垃圾收集机制。Java自动收集无用的内存单元,进而防止了由于内存泄漏导致的动态内存分配问题。
 
完善的异常处理机制,既简化了错误处理任务和恢复,也增加了程序的可读性。
 
Java具有非常好的平台无关性和可移植性。因为Java最初是为对电子产品编程而设计的,所以它具有完美的平台无关性。它使用一种与平台无关的代码──字节码,而不是通常的特定机器上的机器码,由平台上的Java虚拟机中的Java解释器解释执行。Java虚拟机是免费的,在许多平台上都有。
 
Java提供了良好的可移植性。使用Java作为编程语言,只要进行一次程序开发工作,所开发的程序不需要经过任何改动,便能在各种平台上运行。Java使用两种方法使Java的应用程序不依赖与具体的系统:
 
采用基于国际标准的数据类型。Java的原始数据类型在任何机器上都是一样的,例如整型总是32位,长整型总是64位等。
 
提供了一个用于访问底层操作系统功能的可扩展类库。
 
Java是一种高性能的语言。"鱼与熊掌不可兼得",通常,健壮性、安全性、平台无关性、可移植性等方面的提高总是要以牺牲性能为代价的。Java也不例外,Java的内存管理增加了运行时系统的复杂性,因为Java运行时系统必须内嵌一个内存管理模块;同样,Java程序的解释执行的效率也要低于直接执行编译后的源码的效率。但是Java采用了一些很好的措施来弥补这些性能上的差距:
 
生成高效的字节码。Java字节码的设计充分考虑了性能的因素,字节码的格式简单,解释器可以生成高效的机器码。
 
提供了即时编译和嵌入C代码的可选措施。即时编译是指在运行时把字节码编译成机器码。支持多线程。Java提供了对多线程的语言级的接口,而且Java环境本身就是多线程的。


Java对多线程有良好的支持。多线程技术可以提高程序执行的并发度,提高图形用户界面的交互性能。Java提供了语言内置的多线程控制,简化了多线程应用程序的开发,还支持线程的同步控制。
 
Java是一种动态的语言。动态特性是面向对象特性的一个延伸,它使得程序能够适应不断变化的执行环境。Java的动态性主要表现在以下几个方面:
 
Java的类有运行时的表示,这样,即使在运行时刻,程序也能辨别类之间的关系和类型信息,可以动态的从本地或网上把一个类链接到运行系统中去。
 
后期联编。Java的类在运行过程中动态的装载,因此,Java可以在分布式的环境中动态的维护应用程序和Java类库之间的一致性。当类库升级后,应用程序无需重新编译,也一样可以利用新类库中新增的功能。
 
支持动态数据类型和动态协议。通过编写协议句柄,Java可以支持新的、自定义的传输协议,编写内容句柄,可以支持新的数据类型。
 
至于应用,就不必说了!


- 作者: tonnyshen 2005年01月25日, 星期二 11:40  回复(0) |  引用(0) 加入博采

Java堆的管理--垃圾回收
 
2004-05-21    刘学超    gceclub.sun.com.cn   
 

 

Java堆的管理--垃圾回收

作者简介

刘学超,华中师范大学计算机科学系网络与通讯研究所,你可以通过shuechao_lau@hotmail.com与他联系。

1  引言

Java的堆是一个运行时数据区,类的实例(对象)从中分配空间。Java虚拟机(JVM)的堆中储存着正在运行的应用程序所建立的所有对象,这些对象通过new、newarray、anewarray和multianewarray等指令建立,但是它们不需要程序代码来显式地释放。一般来说,堆的是由垃圾回收 来负责的,尽管JVM规范并不要求特殊的垃圾回收技术,甚至根本就不需要垃圾回收,但是由于内存的有限性,JVM在实现的时候都有一个由垃圾回收所管理的堆。垃圾回收是一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能。

2  垃圾收集的意义

在C++中,对象所占的内存在程序结束运行之前一直被占用,在明确释放之前不能分配给其它对象;而在Java中,当没有对象引用指向原先分配给某个对象的内存时,该内存便成为垃圾。JVM的一个系统级线程会自动释放该内存块。垃圾收集意味着程序不再需要的对象是"无用信息",这些信息将被丢弃。当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用。事实上,除了释放没用的对象,垃圾收集也可以清除内存记录碎片。由于创建对象和垃圾收集器释放丢弃对象所占的内存空间,内存会出现碎片。碎片是分配给对象的内存块之间的空闲内存洞。碎片整理将所占用的堆内存移到堆的一端,JVM将整理出的内存分配给新的对象。

垃圾收集能自动释放内存空间,减轻编程的负担。这使Java 虚拟机具有一些优点。首先,它能使编程效率提高。在没有垃圾收集机制的时候,可能要花许多时间来解决一个难懂的存储器问题。在用Java语言编程的时候,靠垃圾收集机制可大大缩短时间。其次是它保护程序的完整性, 垃圾收集是Java语言安全性策略的一个重要部份。

垃圾收集的一个潜在的缺点是它的开销影响程序性能。Java虚拟机必须追踪运行程序中有用的对象, 而且最终释放没用的对象。这一个过程需要花费处理器的时间。其次垃圾收集算法的不完备性,早先采用的某些垃圾收集算法就不能保证100%收集到所有的废弃内存。当然随着垃圾收集算法的不断改进以及软硬件运行效率的不断提升,这些问题都可以迎刃而解。

3  垃圾收集的算法分析

 

Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾收集算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。

大多数垃圾回收算法使用了根集(root set)这个概念;所谓根集就量正在执行的Java程序可以访问的引用变量的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。垃圾收集首选需要确定从根开始哪些是可达的和哪些是不可达的,从根集可达的对象都是活动对象,它们不能作为垃圾被回收,这也包括从根集间接可达的对象。而根集通过任意路径不可达的对象符合垃圾收集的条件,应该被回收。下面介绍几个常用的算法。

3.1  引用计数法(Reference Counting Collector)

引用计数法是唯一没有使用根集的垃圾回收得法,该算法使用引用计数器来区分存活对象和不再使用的对象。一般来说,堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1。当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。

基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜地必须 实时运行的程序。但引用计数器增加了程序执行的开销,因为每次对象赋给新的变量 ,计数器加1,而每次现有对象出了作用域生,计数器减1。

3.2  tracing算法(Tracing Collector)

tracing算法是为了解决引用计数法的问题而提出,它使用了根集的概念。基于tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。在扫描识别过程中,基于tracing算法的垃圾收集也称为标记和清除(mark-and-sweep)垃圾收集器.

3.3  compacting算法(Compacting Collector)

为了解决堆碎片问题,基于tracing的垃圾回收吸收了Compacting算法的思想,在清除的过程中,算法将所有的对象移到堆的一端,堆的另一端就变成了一个相邻的空闲内存区,收集器会对它移动的所有对象的所有引用进行更新,使得这些引用 在新的位置能识别原来 的对象。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

3.4  coping算法(Coping Collector)

该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于coping算法的垃圾 收集就从根集中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。

一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。

3.5  generation算法(Generational Collector)

stop-and-copy垃圾收集器的一个缺陷是收集器必须复制所有的活动对象,这增加了程序等待时间,这是coping算法低效的原因。在程序设计中有这样的规律:多数对象存在的时间比较短,少数的存在时间比较长。因此,generation算法将堆分成两个或多个,每个子堆作为对象的一代(generation)。由于多数对象存在的时间比较短,随着程序丢弃不使用的对象,垃圾收集器将从最年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后,上次运行存活下来的对象移到下一最高代的子堆中,由于老一代的子堆不会经常被回收,因而节省了时间。

3.6  adaptive算法(Adaptive Collector)

在特定的情况下,一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。

4  透视Java垃圾回收

4.1  命令行参数透视垃圾收集器的运行

使用System.gc()可以不管JVM使用的是哪一种垃圾回收的算法,都可以请求Java的垃圾回收。在命令行中有一个参数-verbosegc可以查看Java使用的堆内存的情况,它的格式如下:

java -verbosegc classfile

可以看个例子:

class TestGC 
{
	public static void main(String[] args) 
	{
		new TestGC();
		System.gc();
		System.runFinalization();
	}
}

在这个例子中,一个新的对象被创建,由于它没有使用,所以该对象迅速地变为可达,程序编译后,执行命令: java -verbosegc TestGC 后结果为:

[Full GC 168K->97K(1984K), 0.0253873 secs]

机器的环境为,Windows 2000 + JDK1.3.1,箭头前后的数据168K和97K分别表示垃圾收集GC前后所有存活对象使用的内存容量,说明有168K-97K=71K的对象容量被回收,括号内的数据1984K为堆内存的总容量,收集所需要的时间是0.0253873秒(这个时间在每次执行的时候会有所不同)。

4.2  finalize方法透视垃圾收集器的运行

在JVM垃圾收集器收集一个对象之前 ,一般要求程序调用适当的方法释放资源,但在没有明确释放资源的情况下,Java提供了缺省机制来终止化该对象心释放资源,这个方法就是finalize()。它的原型为:

protected void finalize() throws Throwable

在finalize()方法返回之后,对象消失,垃圾收集开始执行。原型中的throws Throwable表示它可以抛出任何类型的异常。

之所以要使用finalize(),是由于有时需要采取与Java的普通方法不同的一种方法,通过分配内存来做一些具有C风格的事情。这主要可以通过"固有方法"来进行,它是从Java里调用非Java方法的一种方式。C和C++是目前唯一获得固有方法支持的语言。但由于它们能调用通过其他语言编写的子程序,所以能够有效地调用任何东西。在非Java代码内部,也许能调用C的malloc()系列函数,用它分配存储空间。而且除非调用了free(),否则存储空间不会得到释放,从而造成内存"漏洞"的出现。当然,free()是一个C和C++函数,所以我们需要在finalize()内部的一个固有方法中调用它。也就是说我们不能过多地使用finalize(),它并不是进行普通清除工作的理想场所。

在普通的清除工作中,为清除一个对象,那个对象的用户必须在希望进行清除的地点调用一个清除方法。这与C++"破坏器"的概念稍有抵触。在C++中,所有对象都会破坏(清除)。或者换句话说,所有对象都"应该"破坏。若将C++对象创建成一个本地对象,比如在堆栈中创建(在Java中是不可能的),那么清除或破坏工作就会在"结束花括号"所代表的、创建这个对象的作用域的末尾进行。若对象是用new创建的(类似于Java),那么当程序员调用C++的delete命令时(Java没有这个命令),就会调用相应的破坏器。若程序员忘记了,那么永远不会调用破坏器,我们最终得到的将是一个内存"漏洞",另外还包括对象的其他部分永远不会得到清除。

相反,Java不允许我们创建本地(局部)对象--无论如何都要使用new。但在Java中,没有"delete"命令来释放对象,因为垃圾收集器会帮助我们自动释放存储空间。所以如果站在比较简化的立场,我们可以说正是由于存在垃圾收集机制,所以Java没有破坏器。然而,随着以后学习的深入,就会知道垃圾收集器的存在并不能完全消除对破坏器的需要,或者说不能消除对破坏器代表的那种机制的需要(而且绝对不能直接调用finalize(),所以应尽量避免用它)。若希望执行除释放存储空间之外的其他某种形式的清除工作,仍然必须调用Java中的一个方法。它等价于C++的破坏器,只是没后者方便。

下面这个例子向大家展示了垃圾收集所经历的过程,并对前面的陈述进行了总结。

class Chair {
  static boolean gcrun = false;
  static boolean f = false;
  static int created = 0;
  static int finalized = 0;
  int i;
  Chair() {
    i = ++created;
    if(created == 47) 
      System.out.println("Created 47");
  }
  protected void finalize() {
    if(!gcrun) {
      gcrun = true;
      System.out.println(
        "Beginning to finalize after " +
        created + " Chairs have been created");
    }
    if(i == 47) {
      System.out.println(
        "Finalizing Chair #47, " +
        "Setting flag to stop Chair creation");
      f = true;
    }
    finalized++;
    if(finalized >= created)
      System.out.println(
        "All " + finalized + " finalized");
  }
}

public class Garbage {
  public static void main(String[] args) {
    if(args.length == 0) {
      System.err.println("Usage: \n" +
        "java Garbage before\n  or:\n" +
        "java Garbage after");
      return;
    }
    while(!Chair.f) {
      new Chair();
      new String("To take up space");
    }
    System.out.println(
      "After all Chairs have been created:\n" +
      "total created = " + Chair.created +
      ", total finalized = " + Chair.finalized);
    if(args[0].equals("before")) {
      System.out.println("gc():");
      System.gc();
      System.out.println("runFinalization():");
      System.runFinalization();
    }
    System.out.println("bye!");
    if(args[0].equals("after"))
      System.runFinalizersOnExit(true);
  }
}

上面这个程序创建了许多Chair对象,而且在垃圾收集器开始运行后的某些时候,程序会停止创建Chair。由于垃圾收集器可能在任何时间运行,所以我们不能准确知道它在何时启动。因此,程序用一个名为gcrun的标记来指出垃圾收集器是否已经开始运行。利用第二个标记f,Chair可告诉main()它应停止对象的生成。这两个标记都是在finalize()内部设置的,它调用于垃圾收集期间。另两个static变量--created以及finalized--分别用于跟踪已创建的对象数量以及垃圾收集器已进行完收尾工作的对象数量。最后,每个Chair都有它自己的(非static)int i,所以能跟踪了解它具体的编号是多少。编号为47的Chair进行完收尾工作后,标记会设为true,最终结束Chair对象的创建过程。(关于这个例子的更具体的分析和说明请参看《Java编程思想》的第四章)

5  关于垃圾收集的几点补充

经过上述的说明,可以发现垃圾回收有以下的几个特点:

(1)垃圾收集发生的不可预知性:由于实现了不同的垃圾收集算法和采用了不同的收集机制,所以它有可能是定时发生,有可能是当出现系统空闲CPU资源时发生,也有可能是和原始的垃圾收集一样,等到内存消耗出现极限时发生,这与垃圾收集器的选择和具体的设置都有关系。

(2)垃圾收集的精确性:主要包括2 个方面:(a)垃圾收集器能够精确标记活着的对象;(b)垃圾收集器能够精确地定位对象之间的引用关系。前者是完全地回收所有废弃对象的前提,否则就可能造成内存泄漏。而后者则是实现归并和复制等算法的必要条件。所有不可达对象都能够可靠地得到回收,所有对象都能够重新分配,允许对象的复制和对象内存的缩并,这样就有效地防止内存的支离破碎。

(3)现在有许多种不同的垃圾收集器,每种有其算法且其表现各异,既有当垃圾收集开始时就停止应用程序的运行,又有当垃圾收集开始时也允许应用程序的线程运行,还有在同一时间垃圾收集多线程运行。

(4)垃圾收集的实现和具体的JVM 以及JVM的内存模型有非常紧密的关系。不同的JVM 可能采用不同的垃圾收集,而JVM 的内存模型决定着该JVM可以采用哪些类型垃圾收集。现在,HotSpot 系列JVM中的内存系统都采用先进的面向对象的框架设计,这使得该系列JVM都可以采用最先进的垃圾收集。

(5)随着技术的发展,现代垃圾收集技术提供许多可选的垃圾收集器,而且在配置每种收集器的时候又可以设置不同的参数,这就使得根据不同的应用环境获得最优的应用性能成为可能。

针对以上特点,我们在使用的时候要注意:

(1)不要试图去假定垃圾收集发生的时间,这一切都是未知的。比如,方法中的一个临时对象在方法调用完毕后就变成了无用对象,这个时候它的内存就可以被释放。

(2)Java中提供了一些和垃圾收集打交道的类,而且提供了一种强行执行垃圾收集的方法--调用System.gc(),但这同样是个不确定的方法。Java 中并不保证每次调用该方法就一定能够启动垃圾收集,它只不过会向JVM发出这样一个申请,到底是否真正执行垃圾收集,一切都是个未知数。

(3)挑选适合自己的垃圾收集器。一般来说,如果系统没有特殊和苛刻的性能要求,可以采用JVM的缺省选项。否则可以考虑使用有针对性的垃圾收集器,比如增量收集器就比较适合实时性要求较高的系统之中。系统具有较高的配置,有比较多的闲置资源,可以考虑使用并行标记/清除收集器。

(4)关键的也是难把握的问题是内存泄漏。良好的编程习惯和严谨的编程态度永远是最重要的,不要让自己的一个小错误导致内存出现大漏洞。

(5)尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后,自动设置为null,暗示垃圾收集器来收集该对象,还必须注意该引用的对象是否被监听,如果有,则要去掉监听器,然后再赋空值。

6  结束语

一般来说,Java开发人员可以不重视JVM中堆内存的分配和垃圾处理收集,但是,充分理解Java的这一特性可以让我们更有效地利用资源。同时要注意finalize()方法是Java的缺省机制,有时为确保对象资源的明确释放,可以编写自己的finalize方法。


- 作者: tonnyshen 2005年01月25日, 星期二 11:27  回复(0) |  引用(0) 加入博采

Java虚拟机的深入研究
2004-05-21    刘学超    gceclub.sun.com.cn   
 

 

Java虚拟机的深入研究

1  Java技术与Java虚拟机

说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成: Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。它们的关系如下图所示:

图1  Java四个方面的关系

运行期环境代表着Java平台,开发人员编写Java代码(.java文件),然后将之编译成字节码(.class文件)。最后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。从上图也可以看出Java平台由Java虚拟机和Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序可以运行在这个平台上。这个平台的结构如下图所示:

在Java平台的结构中, 可以看出,Java虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操作系统, 其中依赖于平台的部分称为适配器;JVM 通过移植接口在具体的平台和操作系统上实现;在JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 可以在任何Java平台上运行而无需考虑底层平台, 就是因为有Java虚拟机(JVM)实现了程序与操作系统的分离,从而实现了Java 的平台无关性。

那么到底什么是Java虚拟机(JVM)呢?通常我们谈论JVM时,我们的意思可能是:

  1. 对JVM规范的的比较抽象的说明;
  2. 对JVM的具体实现;
  3. 在程序运行期间所生成的一个JVM实例。

对JVM规范的的抽象说明是一些概念的集合,它们已经在书《The Java Virtual Machine Specification》(《Java虚拟机规范》)中被详细地描述了;对JVM的具体实现要么是软件,要么是软件和硬件的组合,它已经被许多生产厂商所实现,并存在于多种平台之上;运行Java程序的任务由JVM的运行期实例单个承担。在本文中我们所讨论的Java虚拟机(JVM)主要针对第三种情况而言。它可以被看成一个想象中的机器,在实际的计算机上通过软件模拟来实现,有自己想象中的硬件,如处理器、堆栈、寄存器等,还有自己相应的指令系统。

JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,因此当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。下面我们从JVM的体系结构和它的运行过程这两个方面来对它进行比较深入的研究。

2  Java虚拟机的体系结构

刚才已经提到,JVM可以由不同的厂商来实现。由于厂商的不同必然导致JVM在实现上的一些不同,然而JVM还是可以实现跨平台的特性,这就要归功于设计JVM时的体系结构了。

我们知道,一个JVM实例的行为不光是它自己的事,还涉及到它的子系统、存储区域、数据类型和指令这些部分,它们描述了JVM的一个抽象的内部体系结构,其目的不光规定实现JVM时它内部的体系结构,更重要的是提供了一种方式,用于严格定义实现时的外部行为。每个JVM都有两种机制,一个是装载具有合适名称的类(类或是接口),叫做类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫做运行引擎。每个JVM又包括方法区、堆、Java栈、程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一起组成的体系结构图为:

图3  JVM的体系结构

JVM的每个实例都有一个它自己的方法域和一个堆,运行于JVM内的所有的线程都共享这些区域;当虚拟机装载类文件的时候,它解析其中的二进制数据所包含的类信息,并把它们放到方法域中;当程序运行的时候,JVM把程序初始化的所有对象置于堆上;而每个线程创建的时候,都会拥有自己的程序计数器和Java栈,其中程序计数器中的值指向下一条即将被执行的指令,线程的Java栈则存储为该线程调用Java方法的状态;本地方法调用的状态被存储在本地方法栈,该方法栈依赖于具体的实现。

下面分别对这几个部分进行说明。

执行引擎处于JVM的核心位置,在Java虚拟机规范中,它的行为是由指令集所决定的。尽管对于每条指令,规范很详细地说明了当JVM执行字节码遇到指令时,它的实现应该做什么,但对于怎么做却言之甚少。Java虚拟机支持大约248个字节码。每个字节码执行一种基本的CPU运算,例如,把一个整数加到寄存器,子程序转移等。Java指令集相当于Java程序的汇编语言。

Java指令集中的指令包含一个单字节的操作符,用于指定要执行的操作,还有0个或多个操作数,提供操作所需的参数或数据。许多指令没有操作数,仅由一个单字节的操作符构成。

虚拟机的内层循环的执行过程如下: 
do{ 
取一个操作符字节; 
根据操作符的值执行一个动作; 
}while(程序未结束)

由于指令系统的简单性,使得虚拟机执行的过程十分简单,从而有利于提高执行的效率。指令中操作数的数量和大小是由操作符决定的。如果操作数比一个字节大,那么它存储的顺序是高位字节优先。例如,一个16位的参数存放时占用两个字节,其值为:

第一个字节*256+第二个字节字节码。

指令流一般只是字节对齐的。指令tableswitch和lookup是例外,在这两条指令内部要求强制的4字节边界对齐。

对于本地方法接口,实现JVM并不要求一定要有它的支持,甚至可以完全没有。Sun公司实现Java本地接口(JNI)是出于可移植性的考虑,当然我们也可以设计出其它的本地接口来代替Sun公司的JNI。但是这些设计与实现是比较复杂的事情,需要确保垃圾回收器不会将那些正在被本地方法调用的对象释放掉。

Java的堆是一个运行时数据区,类的实例(对象)从中分配空间,它的管理是由垃圾回收来负责的:不给程序员显式释放对象的能力。Java不规定具体使用的垃圾回收算法,可以根据系统的需求使用各种各样的算法。

Java方法区与传统语言中的编译后代码或是Unix进程中的正文段类似。它保存方法代码(编译后的java代码)和符号表。在当前的Java实现中,方法代码不包括在垃圾回收堆中,但计划在将来的版本中实现。每个类文件包含了一个Java类或一个Java界面的编译后的代码。可以说类文件是Java语言的执行代码文件。为了保证类文件的平台无关性,Java虚拟机规范中对类文件的格式也作了详细的说明。其具体细节请参考Sun公司的Java虚拟机规范。

Java虚拟机的寄存器用于保存机器的运行状态,与微处理器中的某些专用寄存器类似。Java虚拟机的寄存器有四种:

  1. pc: Java程序计数器;
  2. optop: 指向操作数栈顶端的指针;
  3. frame: 指向当前执行方法的执行环境的指针;。
  4. vars: 指向当前执行方法的局部变量区第一个变量的指针。

在上述体系结构图中,我们所说的是第一种,即程序计数器,每个线程一旦被创建就拥有了自己的程序计数器。当线程执行Java方法的时候,它包含该线程正在被执行的指令的地址。但是若线程执行的是一个本地的方法,那么程序计数器的值就不会被定义。

Java虚拟机的栈有三个区域:局部变量区、运行环境区、操作数区。

局部变量区

每个Java方法使用一个固定大小的局部变量集。它们按照与vars寄存器的字偏移量来寻址。局部变量都是32位的。长整数和双精度浮点数占据了两个局部变量的空间,却按照第一个局部变量的索引来寻址。(例如,一个具有索引n的局部变量,如果是一个双精度浮点数,那么它实际占据了索引n和n+1所代表的存储空间)虚拟机规范并不要求在局部变量中的64位的值是64位对齐的。虚拟机提供了把局部变量中的值装载到操作数栈的指令,也提供了把操作数栈中的值写入局部变量的指令。

运行环境区

在运行环境中包含的信息用于动态链接,正常的方法返回以及异常捕捉。

动态链接

运行环境包括对指向当前类和当前方法的解释器符号表的指针,用于支持方法代码的动态链接。方法的class文件代码在引用要调用的方法和要访问的变量时使用符号。动态链接把符号形式的方法调用翻译成实际方法调用,装载必要的类以解释还没有定义的符号,并把变量访问翻译成与这些变量运行时的存储结构相应的偏移地址。动态链接方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码。

正常的方法返回

如果当前方法正常地结束了,在执行了一条具有正确类型的返回指令时,调用的方法会得到一个返回值。执行环境在正常返回的情况下用于恢复调用者的寄存器,并把调用者的程序计数器增加一个恰当的数值,以跳过已执行过的方法调用指令,然后在调用者的执行环境中继续执行下去。

异常捕捉

异常情况在Java中被称作Error(错误)或Exception(异常),是Throwable类的子类,在程序中的原因是:①动态链接错,如无法找到所需的class文件。②运行时错,如对一个空指针的引用。程序使用了throw语句。

当异常发生时,Java虚拟机采取如下措施:

  • 检查与当前方法相联系的catch子句表。每个catch子句包含其有效指令范围,能够处理的异常类型,以及处理异常的代码块地址。
  • 与异常相匹配的catch子句应该符合下面的条件:造成异常的指令在其指令范围之内,发生的异常类型是其能处理的异常类型的子类型。如果找到了匹配的catch子句,那么系统转移到指定的异常处理块处执行;如果没有找到异常处理块,重复寻找匹配的catch子句的过程,直到当前方法的所有嵌套的catch子句都被检查过。
  • 由于虚拟机从第一个匹配的catch子句处继续执行,所以catch子句表中的顺序是很重要的。因为Java代码是结构化的,因此总可以把某个方法的所有的异常处理器都按序排列到一个表中,对任意可能的程序计数器的值,都可以用线性的顺序找到合适的异常处理块,以处理在该程序计数器值下发生的异常情况。
  • 如果找不到匹配的catch子句,那么当前方法得到一个"未截获异常"的结果并返回到当前方法的调用者,好像异常刚刚在其调用者中发生一样。如果在调用者中仍然没有找到相应的异常处理块,那么这种错误将被传播下去。如果错误被传播到最顶层,那么系统将调用一个缺省的异常处理块。

操作数栈区

机器指令只从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。选择栈结构的原因是:在只有少量寄存器或非通用寄存器的机器(如Intel486)上,也能够高效地模拟虚拟机的行为。操作数栈是32位的。它用于给方法传递参数,并从方法接收结果,也用于支持操作的参数,并保存操作的结果。例如,iadd指令将两个整数相加。相加的两个整数应该是操作数栈顶的两个字。这两个字是由先前的指令压进堆栈的。这两个整数将从堆栈弹出、相加,并把结果压回到操作数栈中。

每个原始数据类型都有专门的指令对它们进行必须的操作。每个操作数在栈中需要一个存储位置,除了long和double型,它们需要两个位置。操作数只能被适用于其类型的操作符所操作。例如,压入两个int类型的数,如果把它们当作是一个long类型的数则是非法的。在Sun的虚拟机实现中,这个限制由字节码验证器强制实行。但是,有少数操作(操作符dupe和swap),用于对运行时数据区进行操作时是不考虑类型的。

本地方法栈,当一个线程调用本地方法时,它就不再受到虚拟机关于结构和安全限制方面的约束,它既可以访问虚拟机的运行期数据区,也可以使用本地处理器以及任何类型的栈。例如,本地栈是一个C语言的栈,那么当C程序调用C函数时,函数的参数以某种顺序被压入栈,结果则返回给调用函数。在实现Java虚拟机时,本地方法接口使用的是C语言的模型栈,那么它的本地方法栈的调度与使用则完全与C语言的栈相同。

3  Java虚拟机的运行过程

上面对虚拟机的各个部分进行了比较详细的说明,下面通过一个具体的例子来分析它的运行过程。

虚拟机通过调用某个指定类的方法main启动,传递给main一个字符串数组参数,使指定的类被装载,同时链接该类所使用的其它的类型,并且初始化它们。例如对于程序:

class HelloApp 
{
	public static void main(String[] args) 
	{
		System.out.println("Hello World!"); 
		for (int i = 0; i < args.length; i++ )
		{
			System.out.println(args[i]);
		}
	}
}

编译后在命令行模式下键入: java HelloApp run virtual machine

将通过调用HelloApp的方法main来启动java虚拟机,传递给main一个包含三个字符串"run"、"virtual"、"machine"的数组。现在我们略述虚拟机在执行HelloApp时可能采取的步骤。

开始试图执行类HelloApp的main方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二进制代表,于是虚拟机使用ClassLoader试图寻找这样的二进制代表。如果这个进程失败,则抛出一个异常。类被装载后同时在main方法被调用之前,必须对类HelloApp与其它类型进行链接然后初始化。链接包含三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及把这些域初始化为标准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的静态初始化函数和静态域的初始化构造方法的执行。一个类在初始化之前它的父类必须被初始化。整个过程如下:

图4:虚拟机的运行过程

4  结束语

本文通过对JVM的体系结构的深入研究以及一个Java程序执行时虚拟机的运行过程的详细分析,意在剖析清楚Java虚拟机的机理。


- 作者: tonnyshen 2005年01月25日, 星期二 11:23  回复(0) |  引用(0) 加入博采