Java的JVM

219 阅读27分钟

0 Java中的事物及使用

0.1 什么是事务?

事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起,并用形如begin transaction和end transaction语句(或函数调用)来界定。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。

0.2 为什么要事务?

事务是为解决数据安全操作提出的,事务控制实际上就是控制数据的安全访问。

用一个简单例子说明:银行转帐业务,账户A要将自己账户上的1000元转到B账户下面,A账户余额首先要减去1000元,然后B账户要增加1000元。假如在中间网络出现了问题,A账户减去1000元已经结束,B因为网络中断而操作失败,那么整个业务失败,必须做出控制,要求A账户转帐业务撤销。这才能保证业务的正确性,完成这个操走就需要事务,将A账户资金减少和B账户资金增加放到同一个事务里,要么全部执行成功,要么全部撤销,这样就保证了数据的安全性。

0.3 事务的4个特性(ACID):

  1. 原子性(atomicity):事务是数据库的逻辑工作单位,而且是必须是原子工作单位,对于其数据修改,要么全部执行,要么全部不执行。

  2. 一致性(consistency):事务在完成时,必须是所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。(实例:转账,两个账户余额相加,值不变。)

  3. 隔离性(isolation):一个事务的执行不能被其他事务所影响。

  4. 持久性(durability):一个事务一旦提交,事物的操作便永久性的保存在DB中。即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

0.4 Java有几种类型的事务?

Java事务的类型有三种:JDBC事务、JTA(Java Transaction API)事务、容器事务。

0.4.1 JDBC事务

在JDBC中处理事务,都是通过Connection完成的。同一事务中所有的操作,都在使用同一个Connection对象。JDBC事务默认是开启的,并且是默认提交。

JDBC Connection 接口提供了两种事务模式:自动提交和手工提交

JDBC中的事务java.sql.Connection 的三个方法与事务有关:

setAutoCommit(boolean):设置是否为自动提交事务,如果true(默认值为true)表示自动提交,也就是每条执行的SQL语句都是一个单独的事务,如果设置为false,需要手动提交事务。

commit():提交结束事务。

rollback():回滚结束事务。

传统JDBC操作流程:

  • 1).获取JDBC连接
  • 2).声明SQL
  • 3).预编译SQL
  • 4).执行SQL
  • 5).处理结果集
  • 6).释放结果集
  • 7).释放Statement
  • 8).提交事务
  • 9).处理异常并回滚事务
  • 10).释放JDBC连接

JDBC优缺点:

  • 1.冗长、重复
  • 2.显示事务控制
  • 3.每个步骤不可获取
  • 4.显示处理受检查异常

JDBC为使用Java进行数据库的事务操作提供了最基本的支持。通过JDBC事务,我们可以将多个SQL语句放到同一个事务中,保证其ACID特性。JDBC事务的主要优点就是API比较简单,可以实现最基本的事务操作,性能也相对较好。但是,JDBC事务有一个局限:一个 JDBC 事务不能跨越多个数据库!所以,如果涉及到多数据库的操作或者分布式场景,JDBC事务就无能为力了。

0.4.2 JTA事务

JTA(Java Transaction API)提供了跨数据库连接(或其他JTA资源)的事务管理能力。JTA事务管理则由JTA容器实现,J2ee框架中事务管理器与应用程序,资源管理器,以及应用服务器之间的事务通讯。

1)JTA的构成

a、高层应用事务界定接口,供事务客户界定事务边界的

b、X/Open XA协议(资源之间的一种标准化的接口)的标准Java映射,它可以使事务性的资源管理器参与由外部事务管理器控制的事务中

c、高层事务管理器接口,允许应用程序服务器为其管理的应用程序界定事务的边界

2)JTA的主要接口位于javax.transaction包中

  • a、UserTransaction接口:让应用程序得以控制事务的开始、挂起、提交、回滚等。由Java客户端程序或EJB调用。
  • b、TransactionManager 接口:用于应用服务器管理事务状态
  • c、Transaction接口:用于执行相关事务操作
  • d、XAResource接口:用于在分布式事务环境下,协调事务管理器和资源管理器的工作
  • e、Xid接口:为事务标识符的Java映射 注:前3个接口位于Java EE版的类库 javaee.jar 中,Java SE中没有提供!UserTransaction是编程常用的接口,JTA只提供了接口,没有具体的实现。

JTS(Java Transaction Service)是服务OTS的JTA的实现。简单的说JTS实现了JTA接口,并且符合OTS的规范。 JTA的事务周期可横跨多个JDBC Connection生命周期,对众多Connection进行调度,实现其事务性要求。 JTA可以处理任何提供符合XA接口的资源。包括:JDBC连接,数据库,JMS,商业对象等等。

3)JTA编程的基本步骤

  • a、首先配置JTA ,建立相应的数据源
  • b、建立事务:通过创建UserTransaction类的实例来开始一个事务。代码如下: Context ctx = new InitialContext(p) ; UserTransaction trans = (UserTransaction) ctx.lookup("javax. Transaction.UserTransaction")
  • c、开始事务:代码为 trans.begin() ;
  • d、找出数据源:从Weblogic Server上找到数据源,代码如下:
  • DataSource ds = (DataSource) ctx.lookup(“mysqldb") ;
  • e、建立数据库连接:Connection mycon = ds.getConnection() ;
  • f、执行SQL操作:stmt.executeUpdate(sqlS);
  • g、完成事务:trans.commit(); / trans.rollback();
  • h、关闭连接:mycon.close() ;

JTA的优缺点:

JTA的优点很明显,就是提供了分布式事务的解决方案,严格的ACID。但是,标准的JTA方式的事务管理在日常开发中并不常用。

JTA的缺点是实现复杂,通常情况下,JTA UserTransaction需要从JNDI获取。这意味着,如果我们使用JTA,就需要同时使用JTA和JNDI。

JTA本身就是个笨重的API,通常JTA只能在应用服务器环境下使用,因此使用JTA会限制代码的复用性。

0.4.3 Spring 容器事务

Spring事务管理的实现有许多细节,如果对整个接口框架有个大体了解会非常有利于我们理解事务,下面通过讲解Spring的事务接口来了解Spring实现事务的具体策略。

Spring事务管理涉及的接口及其联系:

image.png

Spring并不直接管理事务,而是提供了多种事务管理器,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。 Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

Public interface PlatformTransactionManager{  
       // 由TransactionDefinition得到TransactionStatus对象
       TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
    // 提交
       Void commit(TransactionStatus status) throws TransactionException;  
       // 回滚
       Void rollback(TransactionStatus status) throws TransactionException;  
}
 

0.4.4 Spring JDBC事务

如果应用程序中直接使用JDBC来进行持久化,DataSourceTransactionManager会为你处理事务边界。为了使用 DataSourceTransactionManager,你需要使用如下的XML将其装配到应用程序的上下文定义中:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
     <property name="dataSource" ref="dataSource" />
</bean>

实际上,DataSourceTransactionManager是通过调用java.sql.Connection来管理事务。通过调用连接的commit()方法来提交事务,同样,事务失败则通过调用rollback()方法进行回滚。

0.4.5 Hibernate事务

如果应用程序的持久化是通过Hibernate实现的,那么你需要使用HibernateTransactionManager。对于Hibernate3,需要在Spring上下文定义中添加如下的声明:

<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
     <property name="sessionFactory" ref="sessionFactory" />
</bean>
 
sessionFactory属性需要装配一个Hibernate的session工厂,HibernateTransactionManager的实现细节是它将事务管理的职责委托给org.hibernate.Transaction对象,而后者是从Hibernate Session中获取到的。当事务成功完成时,HibernateTransactionManager将会调用Transaction对象的commit()方法,反之,将会调用rollback()方法。

0.4.6 Java持久化API事务(JPA)

Hibernate多年来一直是事实上的Java持久化标准,但是现在Java持久化API作为真正的Java持久化标准进入大家的视野。如果你计划使用JPA的话,那你需要使用Spring的JpaTransactionManager来处理事务。你需要在Spring中这样配置JpaTransactionManager:

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
     <property name="sessionFactory" ref="sessionFactory" />
</bean>

JpaTransactionManager只需要装配一个JPA实体管理工厂(javax.persistence.EntityManagerFactory接口的任意实现)。JpaTransactionManager将与由工厂所产生的JPA EntityManager合作来构建事务。

基本的事务属性的定义:

事务管理器接口PlatformTransactionManager通过getTransaction(TransactionDefinition definition)方法来得到事务,这个方法里面的参数是TransactionDefinition类,这个类就定义了一些基本的事务属性。

事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。

事务属性包含了5个方面:

传播行为、隔离规则、回滚规则、事务超时、是否只读

public interface TransactionDefinition {
    int getPropagationBehavior(); // 返回事务的传播行为
    int getIsolationLevel(); // 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据
    int getTimeout();  // 返回事务必须在多少秒内完成
    boolean isReadOnly(); // 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的
}

7种传播行为:

PROPAGATION_REQUIRED:如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。

PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。

PROPAGATION_MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。

PROPAGATION_REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起。

PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

虽然有7种,但是常用的就第一种REQUIRED和第四种REQUIRES_NEW

五个隔离级别:

ISOLATION_DEFAULT:这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.

另外四个与JDBC的隔离级别相对应;

ISOLATION_READ_UNCOMMITTED:这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。

这种隔离级别会产生脏读,不可重复读和幻像读。

ISOLATION_READ_COMMITTED:保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。

这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。

ISOLATION_REPEATABLE_READ:这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。

它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。

ISOLATION_SERIALIZABLE:这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。

除了防止脏读,不可重复读外,还避免了幻像读。

事务的属性可同通过注解方式或配置文件配置:

注解方式:

@Transactional只能被应用到public方法上,对于其它非public的方法,如果标记了@Transactional也不会报错,但方法没有事务功能. 默认情况下,一个有事务方法, 遇到RuntimeException 时会回滚 . 遇到 受检查的异常 是不会回滚 的. 要想所有异常都回滚,要加上 @Transactional( rollbackFor={Exception.class,其它异常})

@Transactional(
    readOnly = false, //读写事务
    timeout = -1 ,     //事务的超时时间,-1为无限制
    noRollbackFor = ArithmeticException.class, //遇到指定的异常不回滚
    isolation = Isolation.DEFAULT, //事务的隔离级别,此处使用后端数据库的默认隔离级别
    propagation = Propagation.REQUIRED //事务的传播行为
)

配置文件( aop拦截器方式):

<tx:advice id="advice" transaction-manager="txManager">
          <tx:attributes>
            <!-- tx:method的属性:
                * name 是必须的,表示与事务属性关联的方法名(业务方法名),对切入点进行细化。通配符 
                     (*)可以用来指定一批关联到相同的事务属性的方法。
                      如:'get*''handle*''on*Event'等等.
                * propagation:不是必须的,默认值是REQUIRED表示事务传播行为,
                  包括REQUIRED,SUPPORTS,MANDATORY,REQUIRES_NEW,NOT_SUPPORTED,NEVER,NESTED
                * isolation:不是必须的 默认值DEFAULT ,表示事务隔离级别(数据库的隔离级别)
                * timeout:不是必须的 默认值-1(永不超时),表示事务超时的时间(以秒为单位)
                * read-only:不是必须的 默认值false不是只读的表示事务是否只读?
                * rollback-for: 不是必须的表示将被触发进行回滚的 Exception(s);以逗号分开。
                   如:'com.foo.MyBusinessException,ServletException'
                * no-rollback-for:不是必须的表示不被触发进行回滚的 Exception(s),以逗号分开。                                        
                   如:'com.foo.MyBusinessException,ServletException'   
                任何 RuntimeException 将触发事务回滚,但是任何 checked Exception 将不触发事务回滚                     
            -->
             <tx:method name="save*" propagation="REQUIRED" isolation="DEFAULT" read-only="false"/>
             <tx:method name="update*" propagation="REQUIRED" isolation="DEFAULT" read-only="false"/>
             <tx:method name="delete*" propagation="REQUIRED" isolation="DEFAULT" read-only="false"  rollback-for="Exception"/>
             <!-- 其他的方法之只读的 -->
             <tx:method name="*" read-only="true"/>
          </tx:attributes>
</tx:advice>

1 JVM

1.1 基本概念

JVM 是可运行 Java 代码的a假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、 一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接 的交互。

clipboard.png

1.2 运行过程

我们都知道 Java 源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件, 而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。 也就是如下:

  • Java 源文件—->编译器—->字节码文件
  • 字节码文件—->JVM—->机器码 每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够 跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会 存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不 能共享。

2 线程

2.1 线程

这里所说的线程指程序执行过程中的一个线程实体。JVM 允许一个应用并发执行多个线程。 Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓 冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。 Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可 用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。当线程结束时, 会释放原生线程和 Java 线程的所有资源。

2.2 java内存区域

image.png

image.png

应用程序的内存由三块组成,堆,非堆,直接内存。

  • 堆:new的对象
  • 非堆:方法、栈、静态变量
  • 直接内存:开发在代码里面进行申请的内存,它不在堆里面,也不在非堆里面。若代码中未申请,将不会被直接使用。直接内存若没有指定其参数,直接内存大小和堆内存大小保持一致。

3 JVM图解

Java 中,栈的大小通过-Xss 来设置,当栈中存储数据比较多时,需要适当调大返个值,否则会出现 java.lang.StackOverflowError 异常。常见的出现返个异常的是无法迒回的递归,因为此时栈中保存 的信息都是方法迒回的记彔点。

如何解决同时存在的对象创建和对象回收问题?

垃圾回收线程是回收内存的,而程序运行线程则是消耗(戒分配)内存的,一个回收内存,一个分配 内存,从返点看,两者是矛盾癿。因此,在现有癿垃圾回收方式中,要迕行垃圾回收前,一般都需要 暂停整个应用(卲:暂停内存癿分配),然后迕行垃圾回收,回收完成后再继续应用。返种实现方式 是最直接,而且最有效癿解决二者矛盾癿方式。

但是这种方式有一个很明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增 大,对应应用暂停的时间也会相应的增大。一些对相应时间要求很高癿应用,比如最大暂停时间要求 是几百毫秒,那举当堆空间大亍几个 G 时,就很有可能超过返个限制,在返种情冴下,垃圾回收将 会成为系统运行的一个瓶颈。为解决返种矛盾,有了并发垃圾回收算法,使用返种算法,垃圾回收线 程不程序运行线程同时运行。在返种方式下,解决了暂停的问题,但是因为需要在新生成对象的同时 又要回收对象,算法复杂性会大大增加,系统的处理能力也会相应降低,同时,“碎片”问题将会比 较难解决。

3.1 虚拟机

image.png

如图所示:虚拟机中共划分为三个代:年轻代(Young Generation)、老年代(Old Generation)和持久 代(Permanent Generation)。其中【持久代】主要存放的是 Java 类的类信息,不垃圾收集要收集的 Java对象关系不大。【年轻代】和【老年代】的划分是对垃圾收集影响比较大的。

3.2 年轻代(Young Generation)

  • 定义:所有新生成的对象首先都是放在年轻代。
  • 目标:尽可能快速的收集掉那些生命周期短的对象,朝生夕死(80%以上)。
  • 分区:一个Eden(伊甸园)区,两个 Survivor(幸存)区(分别叫from和to),默认比例为8:1
  • GC概念:Minor GC发生在Eden区;Young GC发生在Eden、S0、S1区;Major GC发生在Old区;对新生代、老年代、永久代的全体内存空间的垃圾回收,所以称之为 Full GC。。

image.png

一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次 Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

在 GC开始的时候,对象只会存在于Eden区和名为Survivor-From区,Survivor-To区是空的。 紧接着进行Young GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次Young GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次Young GC前的“From”,新的“From”就是上次 Young GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

3.2.1 年轻代的JVM参数

1)-XX:NewSize和-XX:MaxNewSize

用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

2)-XX:SurvivorRatio

用于设置Eden和其中一个Survivor的比值,这个值也比较重要。

3)-XX:+PrintTenuringDistribution

这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。

4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold

用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

3.3 年老代(Old Generation)

在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代 中存放的都是一些生命周期较长的对象。

老年代的对象比较稳定,所以 Major GC 不会频繁执行。在进行 Major GC 前一般都先进行 了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足 够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

Major GC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。Major GC 的耗时比较长,因为要扫描再回收。Major GC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

3.4 持久代(Permanent Generation)

用于存放静态文件,如今 Java 类、方法等。

持久代对垃圾回收没有显著影响,但是有些应用可能态生成者调用一些 class,例如 Hibernate 等,在返种时候需要设置一个比较大的持久代空间来存放返回运行过程中新增的类。持久代大小通过 **(-XX:MaxPermSize=)**进行设置。

3.4.1 持久代JVM参数

  • 它可以通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize来进行调整
  • 当到达XX:MetaspaceSize所指定的阈值后会开始进行清理该区域
  • 如果本地空间的内存用尽了会收到java.lang.OutOfMemoryError: Metadata space的错误信息。
  • 和持久代相关的JVM参数-XX:PermSize及-XX:MaxPermSize将会被忽略掉。

3.5 Full GC(垃圾回收)

有如下原因可能导致 Full GC:

  • 老年代(Tenured)被写满
  • 持久代(Perm)被写满
  • System.gc()被调用
  • 上一次 GC 之后 Heap 的各域分配策略动态变化

4 JVM参数设置、分析

不管是YGC还是Full GC,GC过程中都会对导致程序运行中中断,正确的选择不同的GC策略,调整JVM、GC的参数,可以极大的减少由于GC工作,而导致的程序运行中断方面的问题,进而适当的提高Java程序的工作效率。但是调整GC是以个极为复杂的过程,由于各个程序具备不同的特点,如:web和GUI程序就有很大区别(Web可以适当的停顿,但GUI停顿是客户无法接受的),而且由于跑在各个机器上的配置不同(主要cup个数,内存不同),所以使用的GC种类也会不同(如何选择见GC种类及如何选择)。本文将注重介绍JVM、GC的一些重要参数的设置来提高系统的性能。

JVM内存组成及GC相关内容请见之前的文章:JVM内存组成GC策略&内存申请

4.1 JVM参数的含义 实例见实例分析

image.png

image.png

image.png

4.2 并行收集器相关参数

image.png

4.3 CMS相关参数

image.png

4.4 辅助信息

image.png

4.5 GC性能方面的考虑

对于GC的性能主要有2个方面的指标:吞吐量throughput(工作时间不算gc的时间占总的时间比)和暂停pause(gc发生时app对外显示的无法响应)。

4.5.1 Total Heap

默认情况下,vm会增加/减少heap大小以维持free space在整个vm中占的比例,这个比例由MinHeapFreeRatio和MaxHeapFreeRatio指定。

一般而言,server端的app会有以下规则:

  • 对vm分配尽可能多的memory;
  • 将Xms和Xmx设为一样的值。如果虚拟机启动时设置使用的内存比较小,这个时候又需要初始化很多对象,虚拟机就必须重复地增加内存。
  • 处理器核数增加,内存也跟着增大。

4.5.2 The Young Generation

另外一个对于app流畅性运行影响的因素是young generation的大小。young generation越大,minor collection越少;但是在固定heap size情况下,更大的young generation就意味着小的tenured generation,就意味着更多的major collection(major collection会引发minor collection)。

NewRatio反映的是young和tenured generation的大小比例。NewSize和MaxNewSize反映的是young generation大小的下限和上限,将这两个值设为一样就固定了young generation的大小(同Xms和Xmx设为一样)。

如果希望,SurvivorRatio也可以优化survivor的大小,不过这对于性能的影响不是很大。SurvivorRatio是eden和survior大小比例。

一般而言,server端的app会有以下规则:

  • 首先决定能分配给vm的最大的heap size,然后设定最佳的young generation的大小;
  • 如果heap size固定后,增加young generation的大小意味着减小tenured generation大小。让tenured generation在任何时候够大,能够容纳所有live的data(留10%-20%的空余)。

4.6 经验&&规则

4.6.1 年轻代大小选择

    • 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择).在此种情况下,年轻代收集发生的频率也是最小的.同时,减少到达年老代的对象.
    • 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度.因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用.
    • 避免设置过小.当新生代设置过小时会导致:1.YGC次数更加频繁 2.可能导致YGC对象直接进入旧生代,如果此时旧生代满了,会触发FGC.

4.6.2 年老代大小选择

    1. 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数.如果堆设置小了,可以会造成内存碎 片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间.最优化的方案,一般需要参考以下数据获得:

并发垃圾收集信息、持久代并发收集次数、传统GC信息、花在年轻代和年老代回收上的时间比例。

    1. 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代.原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象.
    1. 较小堆引起的碎片问题

因为年老代的并发收集器使用标记,清除算法,所以不会对堆进行压缩.当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象.但是,当堆空间较小时,运行一段时间以后,就会出现"碎片",如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记,清除方式进行回收.如果出现"碎片",可能需要进行如下配置:

-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩.

-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

    1. 用64位操作系统,Linux下64位的jdk比32位jdk要慢一些,但是吃得内存更多,吞吐量更大
    1. XMX和XMS设置一样大,MaxPermSize和MinPermSize设置一样大,这样可以减轻伸缩堆大小带来的压力
    1. 使用CMS的好处是用尽量少的新生代,经验值是128M-256M, 然后老生代利用CMS并行收集, 这样能保证系统低延迟的吞吐效率。 实际上cms的收集停顿时间非常的短,2G的内存, 大约20-80ms的应用程序停顿时间
    1. 系统停顿的时候可能是GC的问题也可能是程序的问题,多用jmap和jstack查看,或者killall -3 java,然后查看java控制台日志,能看出很多问题。(相关工具的使用方法将在后面的blog中介绍)
    1. 仔细了解自己的应用,如果用了缓存,那么年老代应该大一些,缓存的HashMap不应该无限制长,建议采用LRU算法的Map做缓存,LRUMap的最大长度也要根据实际情况设定。
    1. 采用并发回收时,年轻代小一点,年老代要大,因为年老大用的是并发回收,即使时间长点也不会影响其他程序继续运行,网站不会停顿
    1. JVM参数的设置(特别是 –Xmx –Xms –Xmn -XX:SurvivorRatio -XX:MaxTenuringThreshold等参数的设置没有一个固定的公式,需要根据PV old区实际数据 YGC次数等多方面来衡量。为了避免promotion faild可能会导致xmn设置偏小,也意味着YGC的次数会增多,处理并发访问的能力下降等问题。每个参数的调整都需要经过详细的性能测试,才能找到特定应用的最佳配置。

4.7 promotion failed:

垃圾回收时promotion failed是个很头痛的问题,一般可能是两种原因产生,第一个原因是救助空间不够,救助空间里的对象还不应该被移动到年老代,但年轻代又有很多对象需要放入救助空间;第二个原因是年老代没有足够的空间接纳来自年轻代的对象;这两种情况都会转向Full GC,网站停顿时间较长。

解决方方案一:

第一个原因我的最终解决办法是去掉救助空间,设置-XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0即可,第二个原因我的解决办法是设置CMSInitiatingOccupancyFraction为某个值(假设70),这样年老代空间到70%时就开始执行CMS,年老代有足够的空间接纳来自年轻代的对象。

解决方案一的改进方案:

又有改进了,上面方法不太好,因为没有用到救助空间,所以年老代容易满,CMS执行会比较频繁。我改善了一下,还是用救助空间,但是把救助空间加大,这样也不会有promotion failed。具体操作上,32位Linux和64位Linux好像不一样,64位系统似乎只要配置MaxTenuringThreshold参数,CMS还是有暂停。为了解决暂停问题和promotion failed问题,最后我设置-XX:SurvivorRatio=1 ,并把MaxTenuringThreshold去掉,这样即没有暂停又不会有promotoin failed,而且更重要的是,年老代和永久代上升非常慢(因为好多对象到不了年老代就被回收了),所以CMS执行频率非常低,好几个小时才执行一次,这样,服务器都不用重启了。

-Xmx4000M -Xms4000M -Xmn600M -XX:PermSize=500M -XX:MaxPermSize=500M -Xss256K -XX:+DisableExplicitGC -XX:SurvivorRatio=1 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSClassUnloadingEnabled -XX:LargePageSizeInBytes=128M -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:log/gc.log

4.8 CMSInitiatingOccupancyFraction值与Xmn的关系公式

上面介绍了promontion faild产生的原因是EDEN空间不足的情况下将EDEN与From survivor中的存活对象存入To survivor区时,To survivor区的空间不足,再次晋升到old gen区,而old gen区内存也不够的情况下产生了promontion faild从而导致full gc.那可以推断出:eden+from survivor < old gen区剩余内存时,不会出现promontion faild的情况,即:

(Xmx-Xmn)*(1-CMSInitiatingOccupancyFraction/100)>=(Xmn-Xmn/(SurvivorRatior+2)) 进而推断出:

CMSInitiatingOccupancyFraction <=((Xmx-Xmn)-(Xmn-Xmn/(SurvivorRatior+2)))/(Xmx-Xmn)*100

例如:

当xmx=128 xmn=36 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((128.0-36)-(36-36/(1+2)))/(128-36)*100 =73.913 当xmx=128 xmn=24 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((128.0-24)-(24-24/(1+2)))/(128-24)*100=84.615… 当xmx=3000 xmn=600 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((3000.0-600)-(600-600/(1+2)))/(3000-600)*100=83.33 CMSInitiatingOccupancyFraction低于70% 需要调整xmn或SurvivorRatior值。

网上一童鞋推断出的公式是::(Xmx-Xmn)*(100-CMSInitiatingOccupancyFraction)/100>=Xmn 这个公式个人认为不是很严谨,在内存小的时候会影响xmn的计算。

关于实际环境的GC参数配置见:实例分析监测工具见JVM监测

参考:

JAVA HOTSPOT VM(www.helloying.com/blog/archiv…

JVM 几个重要的参数 (校长)

java jvm 参数 -Xms -Xmx -Xmn -Xss 调优总结

Java HotSpot VM Options

bbs.weblogicfans.net/archiver/ti…

Frequently Asked Questions About the Java HotSpot VM

Java SE HotSpot at a Glance

Java性能调优笔记(内附测试例子 很有用)

说说MaxTenuringThreshold这个参数