摘要: Spring3 and EJB Integration(I)EJB Client I used EJB client to call EJB stateless bean, I found it is waste time to lookup the stateless bean every time. So I tried to use spring to get singleton stub ...
阅读全文
引言
现在很多的企业都在使用开源框架开发自己的企业级应用,如 Struts、Spring 和 Hibernate 等。起初由于受到资金和规模等的限制,大部分应用都部署在 Tomcat 或 Jboss 等开源应用服务器上。但随着业务不断发展,对应用部署的安全和性能要求也越来越高,企业希望将现有的开源应用从开源服务器迁移到商业应用服务器之上,比如:WebSphere Application Server ( 以下简称为 WAS),通过 WAS 增强应用整体性能,并实现更加可靠的管理。本文将通过实例向大家介绍如何将开源应用从 Tomcat 迁移到 WAS,并帮助大家解决一些可能遇到的普遍问题。
基于 Eclipse 开发的 Struts、Spring 和 Hibernate 开源应用和开发环境的特点
随着 Java 技术的逐渐成熟与完善,作为建立企业级应用的标准平台,J2EE 平台得到了长足的发展。借助于 J2EE 规范中包含的多项技术:Enterprise JavaBean (EJB)、Java Servlets (Servlet)、Java Server Pages (JSP)、Java Message Service (JMS) 等,大量的应用系统被开发出来。但是,在传统 J2EE 应用的开发过程中也出现了一些问题,比如在存储和读取过程中使用大量 SQL 和 JDBC 操作,会降低编程的效率以及系统的可维护性;过去传统的 J2EE 应用多采用基于 EJB 的重量级框架 ( 比如:EJB 2.1),这样做的问题在于使用 EJB 容器进行开发和调试需要耗费大量时间并且耦合度非常高,不利于扩展。
在摸索过程中,各种开源框架孕育而生。开源框架以其免费、开源和简单等特点逐渐成为开发人员的最爱,现在仍然有很多的企业使用开源框架开发自己的应用程序。在开源框架中使用最多的就是 Struts、Spring 和 Hibernate 整合框架 ( 以下简称 SSH 框架)。
典型的 J2EE 三层结构,分为表现层、中间层(业务逻辑层)和数据服务层。三层体系将业务规则、数据访问及合法性校验等工作放在中间层处理。客户端不直接与数据库交互,而是通过组件与中间层建立连接,再由中间层与数据库交互。下面就介绍以下 SSH 框架在 J2EE 三层结构中的作用:
- Struts 是一个在 JSP Model2 基础上实现的 MVC 框架,主要分为模型 (Model) 、视图 (Viewer) 和控制器 (Controller) 三部分,其主要的设计理念是通过控制器将表现逻辑和业务逻辑解耦,以提高系统的可维护性、可扩展性和可重用性。
- Spring 是一个解决了许多 J2EE 开发中常见问题并能够替代 EJB 技术的强大的轻量级框架。这里所说的轻量级指的是 Spring 框架本身,而不是指 Spring 只能用于轻量级的应用开发。Spring 的轻盈体现在其框架本身的基础结构以及对其他应用工具的支持和装配能力。与传统 EJB ( 比如 EJB 2.1) 相比,Spring 可使程序研发人员把各个技术层次之间的风险降低。当然,随着 Java EE 5 及 Java EE 6 中新 EJB 规范的出现,如:EJB 3.0, EJB 3.1,EJB 的开发变得越来越简单。用户可以根据自己的需求和能力,选择合适的框架。想了解更多关于 Java EE 5 和 Java EE 6 中的内容,请参考参考资源 [4] 和 [5]。
- Hibernate 是一个数据持久层框架,是一种实现对象和关系之间映射 (O/R Mapping) 的工具,它对 JDBC 进行了轻量级的对象封装,使程序员可以使用对象编程思想来操作数据库。它不仅提供了从 Java 类到数据表的映射,也提供了数据查询和恢复机制。相对于使用 JDBC 和 SQL 来操作数据库,使用 Hibernate 能大大的提高开发效率。
SSH 框架虽然非常强大,但也有一些缺点,比如 : 相比 Servlet+JDBC 开发方式,复杂度增加了不少 ; 而且开源框架开发和部署的灵活性,使得其使用方式不是很符合现有的 J2EE 规范,从而导致从 Tomcat 或其他开源服务器上迁移到 WAS 会出现很多问题和异常。并且,因为默认的 Eclipse 或 MyEclipse 工具缺少 WAS 的运行时插件,使得开发的开源应用程序无法直接从 IDE 里部署到 WAS。接下来,我们会分步介绍从 Tomcat 迁移到 WAS 可能出现的问题,虽然不能涵盖迁移过程中的所有问题,但希望能够抛砖引玉,尽量解决一些普遍存在的问题。
以下使用的实例是利用 Struts2+Spring2+Hibernate3 开发的模拟医院管理应用。其中功能模块包括前台的显示模块、登录模块、后台的文章和药品管理模块、用户管理模块等基本模块;数据库包括药品、文章、学生、教师和看病等数据表。我们利用 Struts 实现 MVC 模型处理前台的各种请求;利用 Hibernate 将数据持久化并简化对数据的查询;利用 Spring 进行依赖注入控制整个业务逻辑层。图 1 为应用的部分包和配置文件结构
图 1. 应用部分包结构和相关配置文件
配置好 MYSQL,将应用部署到 Tomcat 正常显示页面如下:
图 2. 应用主页
这里需要注意的是,由于开源框架的开发和目录结构不规范,导致在 WAS 中部署 WAR 文件失败。您可能会看到诸如“EAR 文件可能已损坏或不完整。确保对于 WebSphere Application Server,该应用程序处于兼容的 Java 2 Platform, Enterprise Edition (J2EE) 级别。”这样的错误。
图 3. WAS 中应用部署错误
遇到上述错误的原因,可能是因为 WAR 文件中包含 EXE 文件或者 WAR 文件结构不规范,去掉这些文件或调整文件结构即可解决该错误。
迁移之前的准备工作
迁移之前的准备工作非常关键。我们首先要确保应用可以在 Tomcat 成功运行,当然我们还需要确认以下几个方面:
- Tomcat 启动正常
- Struts、Spring 和 Hibernate 所需要的 lib 包都包含在应用的 WAR 或者 EAR 包中
- 应用在 Tomcat 上部署并且启动成功
- 应用的 Struts 功能启动成功,并能正常处理请求
- 应用的数据库连接正常,Hibernate 映射成功并能正常实现数据持久化
- 应用的 Spring 功能成功,并能将所需要的资源注入到应用中
- 测试应用的其他功能确保应用整体运行正常
同时查看 Tomcat、Eclipse 的日志,确保应用没有编译异常或错误。当然我们利用 Eclipse 或者 MyEclipse 开发应用时可能会用到 WAS 的插件,我们在部署之前一定要确保系统中只有一个 WAS 实例在运行。如果其他 WAS 实例运行,可能会出现端口冲突等错误,这时 WAS 会提示一些错误:
清单 1. WAS 端口冲突错误
org.omg.CORBA.INTERNAL: CREATE_LISTENER_FAILED_4 vmcid: 0x49421000 minor code:
Caused by: org.omg.CORBA.INTERNAL: CREATE_LISTENER_FAILED_4 vmcid: 0x49421000 minor
code: 56 completed: No
at com.ibm.ws.orbimpl.transport.WSTransport.createListener(WSTransport.java:719)
at com.ibm.ws.orbimpl.transport.WSTransport.initTransports(WSTransport.java:591)
at com.ibm.rmi.iiop.TransportManager.initTransports(TransportManager.java:155)
at com.ibm.rmi.corba.ORB.set_parameters(ORB.java:1212)
at com.ibm.CORBA.iiop.ORB.set_parameters(ORB.java:1662)
at org.omg.CORBA.ORB.init(ORB.java:364)
at com.ibm.ws.orb.GlobalORBFactory.init(GlobalORBFactory.java:86)
at com.ibm.ejs.oa.EJSORBImpl.initializeORB(EJSORBImpl.java:179)
at com.ibm.ejs.oa.EJSServerORBImpl.<init>(EJSServerORBImpl.java:102)
at com.ibm.ejs.oa.EJSORB.init(EJSORB.java:55)
at com.ibm.ws.runtime.component.ORBImpl.start(ORBImpl.java:379)
... 26 more
|
WAS 在启动的时候抛出以上异常,这主要是因为端口冲突,我们可以查看系统中是否有已经启动的 WAS 或者别的程序正在占用此端口。也可以通过修改概要文件 config\cells\cellname\nodes\nodename 目录下的 serverindex.xml 文件中的端口解决。
应用和环境都没问题了,我们就可以着手迁移应用了。
部署中 Struts 框架可能遇到的问题所遇到的问题
Struts 框架最早是作为 Apache Jakarta 项目的组成部分问世运作,它继承了 MVC 的各项特性,并根据 J2EE 的特点,做了相应的变化与扩展。Struts 框架很好的结合了 Jsp,Java Servlet,Java Bean,Taglib 等技术。
不论是 Struts1 还是 Struts2,很多部署问题都跟 lib 包冲突有关,所以在部署应用的时候要尽量查看一下应用 WAR 文件本身是否存在相互冲突的 jar 文件,WAR 文件和 WAS 所带的包是否存在相互冲突。我们总结了一些 Struts 包冲突的问题供大家参考:
启动应用的时候报 Unable to load bean typecom.opensymphony.xwork2.ObjectFactory classorg.apache.struts2.impl.StrutsObjectFactory 错误。这种错误多是由于 WAS 中存在相同 sturts2-core jar 文件与应用 WAR 或者 EAR 文件中 struts 包冲突。建议删除 WAR 包中的 jar 文件,即可解决此问题。
还有一种情况是在 Tomcat 下项目运行没有任何问题,但把 WAR 包安装在 WAS 中只能访问 HTML 页面了,其余的 Struts2 的请求和 JSP 页面都不能访问,提示您无权查看此页面,查看 WAS 日志文件中发现,启动时有类似错误:
清单 2. WAS 包冲突错误
[11-8-18 15:17:41:079 CST] 00000010 webapp E com.ibm.ws.webcontainer.webapp.WebApp
initializeExtensionProcessors SRVE0280E:
扩展处理器无法在工厂
[com.ibm.ws.jsp.webcontainerext.ws.WASJSPExtensionFactory@2bec2bec]
中进行初始化:java.lang.ClassCastException:
com.sun.faces.application.WebappLifecycleListener
incompatible with java.util.EventListener
……
[10-8-18 15:17:41:562 CST] 00000010 config I Initializing
Sun's JavaServer Faces implementation (1.2_07-b03-FCS) for context '/cc'
[10-8-18 15:17:44:579 CST] 00000010 webapp W com.ibm.ws.webcontainer.webapp
.WebApp initializeTargetMappings SRVE0269W: 找不到用于处理 JSP 的扩展处理器。
|
解决方法有两种:
- 在应用程序服务器 -> [ 选择所使用的服务器 ] -> Web 容器设置 -> Web 容器 -> 定制属性,增加名称为"com.ibm.ws.webcontainer.invokefilterscompatibility"的定制属性,值设为 true。
- 或者检查 WAR 文件的 lib 库中是否存 jsf-api.jar,jsf-impl.jar,jstl-1.2.jar 三个 jar 文件。这是因为在使用 MyEclipse 开发时,MyEclipse 会自动将这三个 jar 文件加入到 lib 库中,但 jsf-impl.jar 包中的 com.sun.faces.application.WebappLifecycleListener 与 java.util.EventListener 不兼容导致应用无法访问,打开 WAR 包的 lib 目录,删除这三个 lib 包即可解决问题。
部署中 Spring 框架可能遇到的问题所遇到的问题
Spring 框架支持轻量级的企业应用开发。作为开源项目,Spring 框架得到了广泛的支持。目前大多数 SSH 架构的开源应用以 Tomcat 作为开发以及测试的 Web 容器。 WAS 也同样支持基于 Spring 框架的项目开发和部署,除了支持 Spring 框架本身的资源管理以及支持事务的特性外,WAS 也可以依靠自身的容器管理、事务支持等带来更加可靠的运行时环境。本文该部分将初步介绍基于 Spring 框架的开源应用从 Tomcat 上移植到 WAS 上需要注意的方面。
使用 Spring 开发的应用可以同时使用 Struts 和 Hibernate。整合了 Struts、Spring 和 Hibernate 进行开发的应用只要保证需要的 JAR 包全部打包在应用中,便可以正确的部署到 WAS 上。其中,WAS 不提供 Tomcat 缺省提供的一些开源 JAR 包,需要将这些包包含在应用中。此外,WAS 提供了很多 J2EE 相关特性,如果 Spring 要使用这些特性,则需要对 Spring 做相关配置。
数据访问
Spring 整合 Hibernate 时,数据源配置信息应该定义在 applicationContext.xml 文件中。清单 3 给出了一个典型的配置数据源信息的示例。您可以将这段代码不做任何修改,加入到 applicationContext.xml 文件中,并放到打包之后的应用中,WAS 会自动识别并利用 Spring 框架完成数据源的配置。
清单 3. applicationContext.xml 文件中的数据源配置
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.ibm.db2.jcc.DB2Driver"></property>
<property name="url" value="jdbc:db2://localhost:50000/MYTEST"></property>
<property name="username" value="db2admin"></property>
<property name="password" value="password"></property>
</bean>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource">
<ref bean="dataSource" />
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.DB2Dialect</prop>
</props>
</property>
<property name="mappingResources">
<list><value>com/ibm/user/Person.hbm.xml</value></list>
</property>
</bean>
|
您也可以使用 WAS 中已经配置好的数据源,在 Spring 的配置文件 applicationContext.xml 文件中声明数据源的代理 Bean,将 WAS 的数据源等资源通过该 Bean 委托给 Spring 框架进行调用。清单 4 给出了一个使用该方式进行配置的 applicationContext.xml 文件的片段。应用在运行时会使用该 Bean 找到相应的数据源完成与数据库的交互。
清单 4. applicationContext.xml 文件中配置 WAS 数据源
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName"><value> java:comp/env/jdbc/SSHTestDB</value></property>
</bean>
|
事务管理
Spring 框架同 WAS 一样支持两种事务管理的方式,分别为编程式和声明式。大多数的用户会选择声明式的事务管理方式,这种方式也是推荐使用的。
通常情况下 Spring 事务管理的一个核心是 PlatformTransactionManager 接口,使用声明方式的事务管理的类均实现该接口,如对数据源进行事务管理的 DataSourceTransactionManager,对 Hibernate 进行事务管理的 HibernateTransactionManager 等。用户可以选择继续使用这些事务管理方法,在 applicationContext.xml 文件中做相应的配置,如清单 5 所示,然后打包部署到 WAS 上完成应用的安装和配置。
清单 5. Spring 框架的事务管理配置
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="txManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
|
其中 ref=”dataSource” 对应于清单 3 中的 Bean id=”dataSource”;ref=”sessionFactory”对应于清单 3 中的 bean id="sessionFactory"。
WAS 支持 JTA,而 Spring 也提供了 JtaTransactionManager。因此也可以将 Spring 中的事务管理交给 WAS 来做。Spring 2.5 之后提供的特定于 WAS 的事务管理的实现类为 WebSphereUowTransactionManager,您可以在 applicationContext.xml 文件中进行相应的配置,将事务的管理交由 WAS 来做。如清单 6 所示。其中配置的 bean id="transactionManager" 并不需要知道自己为哪些资源负责,因为它使用了 WAS 容器的全局事务管理体系。
清单 6. Spring 配置 WAS 的事务管理
<bean id="txManager"
class="org.springframework.transaction.jta.WebSphereUowTransactionManager">
</bean>
<bean id="transactionManager"
class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager">
<ref local="txManager"/>
</property>
</bean>
|
部署中 Hibernate 框架可能遇到的问题所遇到的问题
Hibernate 做为数据持久层框架非常灵活,易于上手,并且便于与其它开源框架整合,从而使它成为开源解决方案中数据持久层框架的不二选择。Hibernate 从 3.2 开始,就开始兼容 JPA。Hibernate3.2 获得了 Sun TCK 的 JPA (Java Persistence API) 兼容认证。这使它的应用范围更加广泛。这里介绍将使用 Hibernate 做为持久层的应用移植到 WAS 上时需要注意的方面。
在项目初期,由于定位或需求的原因,很多 Hibernate 应用都使用 Tomcat 或 Jboss 做为应用服务器。随着项目规模越来越大,对应用的可靠性和安全要求越来越高,就会考虑向商业应用服务器的迁移。总的来说,对一个可以正常运行在 Tomcat 或 Jboss 上的应用来说,移植到 WAS 上非常简单,并不需要做太多改动,只需要将应用所依赖的 Hibernate 相关 jar 包都打包在应用中,再更具情况对配置文件做轻微调整即可,不用修改任何 Java 源代码。
保持原有连接方式
很多 Hibernate 应用采用 JDBC 的链接方式,即在配置文件 hibernate.cfg.xml 中配置 connection.url 属性,指定数据库链接信息。例如:<property name="connection.url">jdbc:db2://db2url:port/dbname</property>
这种应用程序往往还要使用第三方提供的数据库连接池,例如 C3P0 等。如果在移植到 WAS 之后仍然想保持现有连接形式和数据库连接池不变,则不需要对配置文件做任何修改,只需要将第三方提供的数据库连接池所依赖的 jar 包文件一同打包到 WAS 应用中即可。例如将 C3P0 数据库连接池 jar 文件 c3p0-0.9.1.jar 打包到应用程序 lib 目录下。
使用 WebSphere Application Server 数据源
WAS 数据源有着众多企业级优势,很多用户移植后都希望能使用到 WAS 做为企业级应用服务器数据层的强大功能,对 Hibernate 的移植也不例外。其实将 Hibernate 应用的数据源移植到 WAS 非常简单,只需要在 hibernate.cfg.xml 配置文件中加入数据源属性 connection.datasource 和 JNDI 提供商信息即可,无需修改任何源代码。这里需要注意,一旦配置好 WAS 的数据源,WAS 将接管与数据库通信的工作,如果在您以前的应用中使用了第三方数据库连接池,将会产生冲突。解决方法也很简单,只要将 hibernate.cfg.xml 配置文件中的关于第三方数据库连接池的信息注释或删除即可。
总结起来可分为三步:
- 将数据库连接信息
<property name="connection.url">jdbc:db2://db2url:port/dbname</property>
替换为 WAS 数据源信息
<property name="connection.datasource">jdbc/hibernate</property>
- 加入 jndi.class 属性
<property name="jndi.class">com.ibm.websphere.naming.WsnInitialContextFactory</property>
- 注释或删除原有数据库连接池相关属性
总结
现在的开源框架越来越庞大,同一框架不同版本的区别也很大。这篇文章虽不能覆盖开源框架迁移到 WAS 的所有问题,但总结的都是一些比较普遍的问题,力求让用户快速地发现和解决部署和迁移过程中的问题。在迁移过程中,应用的代码基本不需要修改,只要配置和部署得当,从 Tomcat 将开源应用迁移到 WAS 并不是一件难事。
摘要: Spring配置文件中关于事务配置总是由三个组成部分,分别是DataSource、TransactionManager和代理机制这三部分,无论哪种配置方式,一般变化的只是代理机制这部分。 DataSource、TransactionManager这两部分只是会根据数据访问方式有所变化,比如使用Hibernate进行数据访问时,DataSource实际为SessionFactory,Transact...
阅读全文
从J2SE 5.0开始,HotSpot JVM共包含四种垃圾收集器,它们全部基于分代算法。
一、代的划分
HotSpot JVM中内存被划分为三代:年幼代(young generation)、年长代(old generation)和永久代(permanent generation)。从逻辑上讲,年幼代和年长代共同构成了Java堆,而永久代则被称为方法区(method area)。除了一些大对象可能在年长区中直接分配外,大部分对象都在年幼区中创建;而年长区除了那些直接创建的大对象外,大部分对象都是在年幼区中历经几次垃圾收集而幸免于难后被提升过来的。永久代中则保存着已载入类型的相关信息,包含了类、方法和其他一些内部使用的元数据,所有这些信息同样以对象的形式来组织和表示,虽然这些对象并不是Java对象,但是它们却象Java对象一样可以被同样的垃圾收集器所收集;另外,java.lang.String类所管理的内在化的字符串缓存池也在该代中分配;虽然名字叫做“永久”代,但其中的对象并不是永久的,只是沿用了历史名称而已。
年幼代由一个伊甸区(Eden Space)和两个更小的生还区(Survivor Space)组成,如下图所示(该图为缩略图,请点击察看原图)。大部分对象在伊甸区中创建,少数大对象可能在年长代中直接创建。生还区中保存的对象是历经一次或多次对年幼代的垃圾收集而未死的幸存者,并且在被认为已足够成熟而提升到年长代之前,它们仍有在稍后的某次垃圾收集过程中牺牲的可能。除非处在垃圾收集过程当中,两个生还区中只有一个用来容纳这些幸存者,另一个则保持为空。
JVM中的垃圾收集" alt="HotSpot JVM中的垃圾收集" src="http://s5.sinaimg.cn/bmiddle/51501580g8169fb66ab44&690"> 二、垃圾收集类型
年幼代填满后,一次只针对该代的收集被执行,这样的收集也被称作“次收集(minor collection)”。当年长代或永久代被填满后,一次针对所有代的完整收集被执行,这样的收集也被称作“主收集(major collection)”。通常来说,在一次主收集过程中,年幼代首先被收集,收集算法采用当前收集器的年幼代收集算法,该算法往往是针对年幼对象的行为特征而专门设计的;然后是对年长代和永久代的收集,收集算法都采用当前收集器的年长代收集算法。对于给定收集器所具体使用的年幼代和年长代收集算法,请参考下文。另外,主收集过程中如果存在压缩,则每代独自进行。
不过,首先收集年幼代的策略在年长代空闲空间太小时会失效,因为年长代已无足够的空间来接纳所有的可能从年幼代提升过来的对象;在这种情况下,除CMS外的所有收集器都会放弃原有的年幼代收集算法,转而统一采用年长代收集算法对所有代进行收集。(CMS收集器之所以例外是因为它的年长代算法不能用来收集年幼代。)
三、快速分配
从下文对垃圾收集器的描述中可以看出,在许多情况下,内存中都有大块的连续空闲空间用以满足对象的分配请求。这种情形下的分配操作使用简单的“bump-the-pointer”技术,效率很高。按照这种技术,JVM内部维护一个指针(allocatedTail),它始终指向先前已分配对象的尾部,当新的对象分配请求到来时,只需检查代中剩余空间(从allocatedTail到代尾geneTail)是否足以容纳该对象,并在“是”的情况下更新allocatedTail指针并初始化对象。下面的伪代码具体展示了从连续内存块中分配对象时分配操作的简洁性和高效性:
void * malloc(int n){
if( geneTail - allocatedTail < n ) doGarbageCollection();
void * wasAllocatedTail = allocatedTail;
allocatedTail += n;
return wasAllocatedTail;
}
对于多线程应用,分配操作必须是线程安全的。如果使用全局锁为此提供保证,则分配操作必定成为一个性能瓶颈。基于此,HotSport JVM采用了一种被称为“线程局部分配缓冲区”(Thread-Local Allocation Buffers,TLAB)的技术。该项技术为每个线程提供一个独立的分配缓冲区(伊甸区的一小部分),借此来提高分配操作的吞吐量。因为针对每个TLAB,只有一个线程从中分配对象,故而分配操作可以使用“bump-the-pointer”技术快速完成,而不必使用任何锁机制;只有当线程将其已有TLAB填满并且需要获取一个新的TLAB时,同步才是必须的。同时,为了减少TLAB所带来的空间消耗,还使用了一些其他技术,例如,分配器能够把TLAB的平均大小限制在伊甸区的1%以下。
“bump-the-pointer”和TLAB技术的组合保证了分配操作的高效性,类似new Object()这样的操作在大部分时间内只需要大约10条机器指令即可完成。
四、收集方式
1)串行(serial)和并行(parallel)
串行和并行是从收集任务本身如何被完成的角度来描述收集过程的。采用串行方式收集时,同一时间只有一件事情会发生。例如,即使有多个CPU可用,还是只有一个被用来执行收集任务。当采用并行方式收集时,垃圾收集任务被分成许多子任务,并且那些子任务在不同的CPU上被同时执行。同时操作使收集任务得以更快地完成,代价是增加了任务的复杂性并可能产生内存碎片。2)STW(stop-the-world)和并发(concurrent)
STW和并发是从收集任务是否影响应用程序执行的角度来描述收集过程的。当垃圾收集使用STW方式进行时,在收集期间应用程序将被完全挂起。作为另一种选择,收集任务可以采取并发方式,与应用程序一起同时执行。比较典型的情况是,并发收集器采取并发方式完成大部分工作,但也可能偶尔地不得不切换到STW方式,以完成一些需要应用程序短暂停顿的工作。STW收集比并发收集更为简单,因为在收集期间堆被冻结、对象不会改变;它的缺点是对某些应用程序来说,被暂停过久是不符合要求的。相对地,采用并发方式进行垃圾收集时,停顿时间会更短,但这也要求收集器必须更加小心,因为它正在操作可能被应用程序同时更新的对象。对并发收集器来说,这会增加额外开销从而影响性能,也会对堆空间产生更大的需求。
五、串行收集器(serial collector)
使用串行收集器时,对年幼代和年长代的收集都采用串行、STW方式进行。也就是说收集任务同时只使用一个CPU,而且在收集任务执行期间,应用程序的执行被中止。
1)串行收集器如何收集年幼代?
下图展示了使用串行收集器对年幼代进行收集时的操作细节。伊甸区中的活动对象被拷贝到初始为空的生还区2中;已占用的生还区1中的活动对象如果仍显年轻,则同样被拷贝到生还区2中,否则被直接拷贝到年长代;需要注意的是,生还区2一旦被填满,则所有尚未被拷贝的活动对象,不论其来自伊甸区还是生还区1,也不论其曾经幸免于多少次次收集,统统被拷贝到年长代。按照定义,在活动对象被拷贝之后,伊甸区和生还区1中的对象就全部成为垃圾对象,无须再被检查。(垃圾对象在图中以“X”标记,虽然实际上收集器并不检查和标记这些对象。)
![HotSpot <wbr alt=]()
JVM中的垃圾收集" alt="HotSpot
JVM中的垃圾收集" src="http://s4.sinaimg.cn/middle/51501580g81cfe5708d93&690"> 收集完成后,伊甸区和生还区1变为空闲空间,活动对象保存在生还区2中。此时,两个生还区的角色已经发生了互换。如下图:
JVM中的垃圾收集" alt="HotSpot JVM中的垃圾收集" src="http://s4.sinaimg.cn/middle/51501580g81d362167ce3&690"> 2)串行收集器如何收集年长代?
串行收集器采用标记-清理-压缩算法收集年长代和永久代。在标记阶段,收集器遍历引用树,找出所有活动对象并打上标记;在清理阶段,顺序扫描代空间中所有对象(不论死活),计算出每个活动对象在代空间中的新位置;在压缩阶段,指向活动对象的所有引用被先期更新后,所有活动对象也被逐个滑动到其新的位置。由于所有活动对象都是按照次序朝代空间头部移动的,因此就在代空间尾部自然形成了一个单一而连续的空闲空间。如下图:
JVM中的垃圾收集" alt="HotSpot JVM中的垃圾收集" src="http://s3.sinaimg.cn/middle/51501580g81fd1aa883d2&690">压缩过程使基于年长代或永久代的分配操作可以使用快速的“bump-the-pointer”技术。
3)何时使用串行收集器?
对于运行在客户端级硬件上并且对停顿时间没有特别要求的大多数应用而言,串行收集器都是首选。按照目前的硬件水平,串行收集器可以高效地管理使用64MB堆空间、最长停顿时间不能超过半秒的很多重要应用。
4)串行收集器的选用
在J2SE 5.0版本中,对于非服务器级硬件而言,串行收集器作为缺省的垃圾收集器被自动选用;对于其他硬件平台,则可以通过命令行选项“-XX:+UseSerialGC”进行显示的选用。
六、并行收集器(parallel collector)
目前,许多Java应用的运行平台大都包含很多物理内存和多个CPU。并行收集器,也被称作吞吐量收集器,被开发出来的主要目的就是为了充分利用CPU资源,而不是只让一个CPU去做垃圾收集而其他CPU却被闲置。
1)并行收集器如何收集年幼代?
和串行收集器相比,并行收集器采用了大致相同的年幼代收集算法,只是执行的是其并行版本而已。对年幼代的收集虽然仍基于拷贝技术、采用STW方式进行,但收集工作是并行展开的,使用了多个CPU,这就降低了垃圾收集开销,从而提高了应用程序的吞吐量。下图展示了并行收集器和串行收集器在执行年幼代收集时到底有何不同:
JVM中的垃圾收集" alt="HotSpot JVM中的垃圾收集" src="http://s7.sinaimg.cn/middle/51501580g81e435e495a6&690">
2)并行收集器如何收集年长代?
和串行收集器一样,并行收集器对年长代的收集同样基于标记-清理-压缩算法,同样采用串行、STW方式进行。
3)何时使用并行收集器?
能够得益于并行收集器的应用程序,必定运行在多CPU机器上,并且对停顿时间不能有特别的约束。因为可能持续时间很长的年长代收集虽然稀少,但还是会发生的。适于采用并行收集器的典型应用包括批处理、记帐、工资单和科学计算等等。你可能更倾向于选择并行压缩收集器(见下文)而不是并行收集器,因为前者对所有代(而不只是年幼代)的收集都采用并行方式进行。
4)并行收集器的选用
在J2SE 5.0版本中,对于服务器级硬件而言,并行收集器作为缺省的垃圾收集器被自动选用;对于其他硬件平台,则可以通过命令行选项“-XX:+UseParallelGC”进行显示的选用。
七、并行压缩收集器(parallel compacting collector)
并行压缩收集器在J2SE 5.0 U6中引入,它和并行收集器的区别在于,对年长代的收集它使用了全新的算法。注意:并行压缩收集器终将取代并行收集器。
1)并行压缩收集器如何收集年幼代?
同并行收集器,不再赘述。
2)并行压缩收集器如何收集年长代?
使用并行压缩收集器时,对年长代和永久代的收集都采用带滑动压缩的准并行、STW方式进行。为了满足并行处理的要求,每一个代空间均被逻辑划分为诸多定长区域(fixed-sized region),每个区域的相关信息保存在收集器维护的内部数据结构中。收集过程被分为标记、汇总和压缩三个阶段进行。在标记阶段,根引用集被划分给多个垃圾收集线程,它们同时运行,以并行的方式对活动对象进行追踪和标记;在活动对象被标记的同时,该对象的起始区域的数据也将被同步更新以反映该活动对象的大小和位置信息。
在汇总阶段(summary phase),操作不再基于对象,而是区域。考虑到先前收集过程中的压缩累积效应,每一个代空间中位于左侧的某一部分通常是密集的,主要包含了活动对象。从这样的密集区块中可能回收的空间数量使得它们并不值得被压缩。所以汇总阶段的首要任务就是检查区域的密集度,从最左边一个区域开始,直到找到这样的一个区域,使得在该区域及其右侧所有区域中可被回收的空间数量抵得上对它们进行压缩的成本。该区域左侧的所有区域就被称为密集前置区块,没有对象会被移入其中。该区域及其右侧所有区域会被压缩,以消除所有死区。汇总阶段的下一个任务就是计算并保存每个被压缩区域中活动数据的首字节在压缩后的新位置。需要注意的是:汇总阶段在目前被实现为一个串行阶段,这也是“准”并行方式的由来;并行实现也是可能的,只是与标记和压缩阶段的并行化相比,它对性能的影响不大。
在压缩阶段,垃圾收集线程使用汇总数据确定需要被填充的区域,然后它们就可以独立地把对象拷贝到这些区域中而不再需要额外的同步。这就产生了一个堆,堆空间的一端塞满了活动对象,而另一端则是一个单一而连续的空闲内存块。
3)何时使用并行压缩收集器?
和并行收集器一样,并行压缩收集器同样有益于在多CPU机器上运行的应用程序。除此之外,年长代收集的并行化操作方式还减少了停顿时间,使得并行压缩收集器比并行收集器更为适合那些有停顿时间限制的应用。不过,对于运行在大型共享主机(如SunRays)上的应用来说,并行压缩收集器也许并不太合适,因为任何单一应用都不应长时间独占几个CPU。在这样的机器上,要么考虑通过命令行选项“-XX:ParallelGCThreads=n”减少垃圾收集线程的数目,要么考虑选择一种不同的收集器。
4)并行压缩收集器的选用
并行压缩收集器只能通过命令行选项“-XX:+UseParallelOldGC”进行显示的选用。
八、并发的标记-清理收集器(Concurrent Mark-Sweep(CMS) Collector)
对于许多应用来说,端到端的吞吐量并不象响应时间那么重要。通常来讲,对年幼代的收集并不会引起太长时间的停顿。但是对年长代的收集,虽然不常发生,却可能导致停顿时间过长的状况,在堆空间很大时尤其明显。为了解决这个问题,HotSpot JVM包含了一个名叫“并发的标记-清理(CMS)收集器”的收集器,它也被称为低延迟收集器。
1)CMS收集器如何收集年幼代?
同并行收集器,不再赘述。
2)CMS收集器如何收集年长代?
采用CMS收集器收集年长代时,大部分收集任务与应用程序并发执行。
CMS收集器的收集周期始于初始标记,它采用串行、STW方式进行,用于确定根引用集。随后进入并发标记阶段,完成对所有活动对象的追踪和标记,在JDK6中该阶段已开始采用并行、并发方式进行。由于在并发标记过程中应用程序正在执行并可能更新了一些对象的引用域,因此并发标记过程结束时并非所有活动对象都已确保被标记出来。为了处理这种情况,应用程序再次暂停,收集过程进入再标记阶段;它采用并行、STW方式进行,通过对并发标记过程中被修改对象的再次访问最终完成整个标记过程。因为再标记阶段的停顿时间往往是最长的(超过初始标记停顿甚至次收集停顿),因此再标记过程会尽量采用并行方式进行。
再标记阶段完成后,所有活动对象都已确保被标记,随后进入并发清理阶段,它采用串行、并发方式进行,就地回收所有垃圾对象。下图展示了串行的标记-清理-压缩收集器和CMS收集器在收集年长代时的区别:
JVM中的垃圾收集" alt="HotSpot JVM中的垃圾收集" src="http://s13.sinaimg.cn/middle/51501580g82676852d74c&690"> 因为一些任务(例如再标记过程中对被修改对象的再次访问)增加了收集器的工作量,CMS收集器的总体开销自然会增大。对于大多数试图减少停顿时间的收集器来说,这是一种典型的折衷。
CMS收集器是唯一一个不使用压缩技术的收集器。也就是说,在垃圾对象所占用的空间被释放以后,收集器并不把活动对象全部推挤到代空间的某一端去。见下图:
JVM中的垃圾收集" alt="HotSpot JVM中的垃圾收集" src="http://s3.sinaimg.cn/middle/51501580g82716466bed2&690">这种方式节省了回收时间,但却因为空闲空间不再连续,收集器也就不再可能只使用一个简单指针即可指示出可分配给新对象的下一个空闲空间的位置,相反,它现在需要使用空闲空间列表。也就是说,收集器创建并通过一组列表把内存中尚未分配的区域连接起来,每当有对象需要分配空间时,适当的列表(基于所需内存数量)被搜索,以找到一块足以放下该对象的空闲区域。作为结果,与使用“bump-the-pointer”技术时相比,年长代中的分配操作变得更加昂贵。同时这也给年幼代收集带来了额外的开销,因为在其收集过程中每提升一个对象都会触发一次年长代中的分配操作。
CMS收集器的另一个缺点是和其他收集器相比它需要更大的堆空间。一方面,由于在标记阶段应用程序被允许运行,它就可能继续分配内存,从而可能使年长代空间不断地增长;另一方面,虽然标记阶段完成后所有活动对象都已确保被标记,但是在标记过程中一些对象却可能变为垃圾对象,而且直到下次年长代收集之前它们不会被回收。这样的对象也被称为游浮垃圾。
CMS收集器的最后一个缺点是由于缺乏压缩它可能引发碎片化问题。为了对付碎片化,CMS收集器跟踪对象的流行尺寸,预估未来需求,并为满足需求还可能分割或合并空闲内存块。
不象其他收集器,CMS收集器并不是等到年长代填满后才启动对年长代的收集,而是尝试尽早启动年长代收集,以便在年长代被填满之前收集过程可以完成。否则的话,CMS收集器将重新采用在串行和并行收集器中使用的标记-清理-压缩算法,尽管该算法工作于STW方式,也更加耗时。为避免这种情况的发生,CMS收集器对先前收集所耗时间和代空间充满所耗时间进行统计,并据此确定收集启动时间。另外,当年长代的空间占用率超过启动占用率(initiating occupancy)时,CMS收集器也将启动一次收集。启动占用率的值可以通过命令行选项“-XX:CMSInitiatingOccupancyFraction=n”进行设定,其中 n 表示年长代空间大小的百分比。缺省值为68。
总的来说,与并行收集器相比,CMS收集器(有时甚至显著地)减少了年长代收集的停顿时间,而代价是略有增加的年幼代收集的停顿时间、吞吐量方面的一些损失和额外的堆空间需求。
3)增量模式
CMS收集器可以采用让并发阶段增量完成的模式运行。这种模式通过对并发阶段的周期性暂停把处理能力交还给应用程序,以减少并发阶段持续时间过长所带来的不利影响。收集工作被分成许多小的时间块,它们在年幼代收集的间歇期被调度。当应用程序既需要CMS收集器提供的低停顿时间,又只能在很少的CPU(比如说1到2个)上运行时,这个特性就相当有用。
4)何时使用CMS收集器?
如果应用程序需要更短的垃圾收集停顿时间并且能够承担在运行时和垃圾收集器共享处理器资源,那么就可以使用CMS收集器。(由于其并发性,在垃圾收集过程中CMS收集器将和应用程序抢夺CPU周期。)通常来说,具有较大的长寿数据集并且运行在2个或多个CPU上的应用程序,更容易受益于CMS收集器的使用。一个典型的例子就是Web服务器。对于任何需要低停顿时间的应用程序来说,CMS收集器都值得考虑。对于年长代尺寸适中并且运行在单一处理器上的交互式应用程序来说,使用CMS收集器同样可能取得不错的效果。
5)CMS收集器的选用
CMS收集器只能通过命令行选项“-XX:+UseConcMarkSweepGC”进行显示的选用。如果希望CMS收集器在增量模式下运行,还需要通过命令行选项“-XX:+CMSIncrementalMode”启用该模式。
九、收集器、堆尺寸和虚拟机的自动选择
在J2SE 5.0中,根据应用程序所运行的硬件平台和操作系统,垃圾收集器、堆尺寸和HotSpot虚拟机(客户机或服务器)的缺省值被自动选定。这些自动的选择不仅减少了对命令行选项的使用,而且还更好的满足了不同类型应用程序的需要。
1)服务器级硬件(server-class machine,不使用“机器”这个词是因为读起来太拗口)的定义:
服务器级硬件必须同时满足以下两个条件:
①拥有2个或以上的物理处理器
②拥有2GB或以上的物理内存
该定义适用于所有平台,32位Windows平台除外。
2)服务器级硬件与非服务器级硬件下各项缺省值的比较:
机器类型 |
虚拟机 |
垃圾收集器 |
堆尺寸初始值-Xms |
堆尺寸最大值-Xmx |
服务器级硬件 |
服务器版 |
并行收集器 |
物理内存的1/64,不超过1GB |
物理内存的1/4,不超过1GB |
非服务器级硬件 |
客户机版 |
串行收集器 |
4MB |
64MB |
注意:本节所涉及的堆尺寸指的是Java堆的大小,包括年幼代和年长代,但不包括永久代。