前面的话
本文主要内容如下(Java 6):
- Java虚拟机结构
- JVM垃圾回收机制: 包括2种垃圾回收方法, 7种垃圾收集器。
- JVM内存区域配置: 包括堆区域、新域与旧域、永久区域、新域子空间
- JVM性能调优实战:调优配置参考和调优实战
Java虚拟机结构与属性
JVM提供Java运行环境,内存区域保存的是Java类和Java对象,Java的内存区域称作堆(Heap)。
Java虚拟机内存结构
JVM的堆被分为3个域(Generation)分别为新域(Young Generation)、旧域(Tenured Generation)和永久域(Perm Generation),标记为Virtual的部分被保留,在必要时才实际分配出去。

新域(Young Generation)由Eden和两个救助空间Survivor组成。新对象通常创建于Eden中。其中一个救助空间Survivor会随时被清空,并用作另一个救助空间Survivor的目的地。当进行垃圾收集时,所有来自Eden和救助空间Survivor的活动对象都被复制到另一个救助空间Survivor。对象在两个救助空间Survivor之间移动,直到它们足够“老”,能够被移入保存生存期较长对象的旧域(Tenured Generation)中。
永久域(Perm Generation)保存那些在虚拟机的整个生存期都生存的对象。因此,该域(Generation)不需要被垃圾收集程序清空。
JVM配置选项
要对JVM的性能进行调优,其实就是对JVM的内存进行调整和控制,包括堆大小的调整、Generation大小的调整、垃圾收集程序的选择,以及以上各项的组合。
- -X 选项:以-X 开头的选项,这些选项在JDK升级时不会通知更改
- -XX 选项:以-XX 开头的选项,这些选项不够稳定,所以建议少用
- Boolean 值:即布尔型值,它用-XX:+<option>表示打开该属性,用-XX:-<option>表示关闭该选项
- Nemeric 值:即数字型值,形式为-XX:<option>=<number>,其中的数字可以包含表示字节数的字母,k和K表示KB,m和M表示MB,g和G表示GB,如果不加字母即为Byte,例如32k表示32768Bytes。
- String 值:即字符串型值,形式为-XX:<option>=<string>,通常用于指定一个文件名、路径名、命令列表等。
本节列出了这些选项及一些值得注意的例外事项,其中粗体部分的属性是比较常用的属性
一、-X属性列表
| 属性列表及其默认值 | 描述 |
|---|---|
| -Xint | 只在解释模式下运行虚拟机,如果使用这个选项,系统将不编译任何字节码 |
| -Xbatch | 通常JVM会预编译代码,结束后再在JIT中执行,该属性用于禁止预编译,直接在JIT中执行编译和运行 |
| -Xdebug | 启用JVMDI Debug支持,在J2SE 5.0中已经不再支持该属性 |
| -Xbootclasspath:bootclasspath | 指定一个目录及JAR和ZIP档案的列表,作为搜索启动类的范围,列表中各项之间的分隔符为冒号: |
| -Xfuture | 对类文件执行严格的格式检查。这个选项强制Java对是否遵守类文件规范做更加严格的检查,而默认的检查只是基于Java 1.1.x 的标准。你应该使用这个选项来对代码进行测试,以便确保这些代码在未来的Java版本中能够工作,这些未来的版本可能强制进行更为严格的类文件格式检查 |
| -Xnoclassgc | 禁用垃圾回收 |
| -Xincgc | 启用增量垃圾收集器 |
| -Xmn | 为Eden对象设定Java堆的大小,默认值为640KB |
| -Xmsn | 设定Java堆的初始大小,默认大小时2097152(2MB)。这个值必须是1024字节(1KB)的倍数,且比它大 |
| -Xmxn | 设定Java堆的最大大小,默认值为64MB,最大的堆大小达到将近2GB(2048MB) 请注意:很多垃圾收集器的选项依赖于堆大小的设定。请在微调垃圾收集器使用内存空间的方式之前,确认是否已经正确设定了堆的尺寸 |
| -Xprof | 把运行程序详细的行为分析数据发送给标准输出。在产品级的代码中不能使用这个选项 |
| -Xrunhprof[:help] | 启用CPU、内存和监视器记录 |
| -Xrs | 降低JVM的系统信号使用率,JVM能够监视控制台事件,如果监控到CTRL_C_EVENT、CTRL_CLOSE_EVENT、CTRL_LOGOFF_EVENT、CTRL_SHUTDOWN_EVENT时,将停止JVM的运行 |
| -Xssn | 设置线程堆栈大小为n |
二、-XX属性列表
-XX 属性可以分为三种类型:JVM行为控制属性、JVM性能调优属性、JVM Debug属性
1)JVM行为控制属性
| 属性列表及其默认值 | 描述 |
|---|---|
| -XX:-AllowUserSignalHandlers | 仅与Solaris和Linux系统相关,启用信号处理器 |
| -XX:-AltStackSize=16384 | 仅与Solaris和Linux系统相关,指定信号处理器堆栈大小 |
| -XX:-DisableExplicitGC | 忽略代码中对System.gc()的显示调用,虚拟机仍然按照正常的机制进行垃圾收集器。这个选项禁止在代码中强制执行垃圾收集 |
| -XX:+FailOverToOldVerifier | 当新建类型检查失败时,转移到旧对象 |
| -XX:+HandlePromotionFailure | 新域不需要为活动对象增大权限 |
| -XX:+MaxFDLimit | 仅与Solaris系统相关,提高文件描述符的数量到最大 |
| -XX:preBlockSpin=10 | 当使用-XX:+UseSpinning属性时,设置线程同步代码的最大数量 |
| -XX:-RelaxAccessControlCheck | 释放访问控制器的验证检查 |
| -XX:+ScavengeBeforeFullGC | 设置新域的GC为优先 |
| -XX:+UseAltSigs | 仅与Solaris系统相关,使用可选的信号代替SIGUSR1和SIGUSR2 |
| -XX:+UseBoundThreads | 仅与Solaris系统相关,将用户级别与内核线程绑定 |
| -XX:+UseConcMarkSweepGC | 激活标志和清除同时进行的垃圾收集活动,这个选项对多处理器的计算机有效 |
| -XX:+UseGCOverheadLimit | 在OutOfMemory前,限制JVM的GC回收时间 |
| -XX:+UseLWPSynchronization | 仅与Solaris系统相关,使用LWP代替线程同步机制 |
| -XX:-UseParallelGC | 激活并行的垃圾收集活动,这个选项只对多处理器的计算机有效 |
| -XX:-UseParallelOldGC | 使用并行垃圾收集器 |
| -XX:-UseSerialGC | 使用串行垃圾收集器 |
| -XX:-UseSpinning | 在进入操作系统的线程同步前,使用Java监视 |
| -XX:+UseTLAB | 激活线程本地的分配缓存区。使用这个缓存区将使线程任务繁重的应用程序的内存分配更加具有可扩展性,大大提高内存分配的性能 |
| -XX:+UseSplitVerifier | 使用对堆栈映射表的属性实行新对象检查 |
| -XX:+UseThreadPriorities | 使用本地线程优先级 |
| -XX:+UseVMInterruptibleIO | 仅与Solaris系统相关,遇到系统IO操作的EINTR错误时终止线程 |
2)JVM性能调优属性
如下图所示的属性用于JVM的性能进行调优,比如永久域大小、新域大小和上限等。


3)JVM Debug属性
如下图所示的属性用于进行JVM的调试输出,便于进行Debug调试。

Java垃圾回收机制
- 垃圾回收的2种方法:引用计数、对象引用遍历。
- 垃圾收集器的7中类型:标记——清除收集器、标记——压缩收集器、复制收集器、增量收集器、分代收集器、并发收集器、并行收集器。
垃圾回收的2种方法
垃圾收集机制(GC),是JVM用于释放哪些不再使用的对象所占用内存的程序与算法。并不是所有的JVM都有GC,因此GC没有一个规范的规定,它没有被Java语言制定成规范。通常大多数JVM都有GC,并且它们都使用类似的算法管理内存和执行收集操作。
垃圾收集的目的在于清除不再使用的对象。因此,首先必须知道哪些对象不再使用,GC将会收集不再引用的对象。为了标志对象是否被引用,通常采用两种方法:引用计数和对象引用树。
1) 引用计数

引用计数存储对每个对象的所有引用数,也就是说,当应用程序引用某一个对象时,JVM增加引用数。当某对象的引用数为0时,便可以进行垃圾收集。
如图2-2所示,GC对每一个类的示例对象进行引用计数,当增加引用时计数加1,当减少引用时计数减1,从图中可以看出对象C的计数为0,那么C对象就可以被垃圾回收了。
2) 对象引用树
引用计数是早期的JVM所使用的技术,目前大多数JVM都采用对象引用树的方式。对象引用遍历树是将对象引用关系构建成一颗树,从一组根对象开始,沿着整个对象树上的每条链接,递归确定可达(reachable)对象。如果某对象是不能从根对象的一个(至少一个)可达,则将它作为垃圾收集起来。在对象遍历阶段,GC必须记住哪些对象可以到达,以便删除不可到达的对象,这称为标记(marking)对象。

如图2-3所示,对象A被对A1引用,引用对象B被B1和B2引用,对象Z被对象Z1和Z2引用,但对象C没有被其它对象引用,即是不可到达对象。
然后,JVM要删除不可达的对象。删除时,有些JVM只是简单地扫描堆栈,删除未标记的对象,并释放它们的内存以生成新的对象,这叫作清除(sweeping)。这种方法的问题在于内存会分为很多很多小段,而它们都不足以用于新的对象,但是组合起来却很大。因此许多JVM可以重新组织内存中的对象,并进行压缩(compact),形成可利用的空间。
为了进行内存空间的重新组织,JVM需要停止其它的活动。这种方法意味着所有与应用程序相关的工作停止,只有GC运行。这也导致了在响应期间增加了许多混杂请求,也有更复杂的GC不断增加或同时运行以减少或者清除应用程序的中断,这也造成了程序运行的复杂性。有的GC使用单线程完成这项工作,有的则采用多线程以增加效率。
垃圾收集器的7个类型
- 标记——清除收集器
- 标记——压缩收集器
- 复制收集器
- 增量收集器
- 分代收集器
- 并发收集器
- 并行收集器
1)标记——清除收集器
标记——清除收集器是对遍历对象图进行遍历,并标记可到达的对象,然后扫描栈以寻找未标记对象并释放它们的内存。这种收集器一般使用单线程工作并停止其它操作。
2)标记——压缩收集器
也叫标记——清除——压缩收集器,它与标记——清除收集器有相同的标记阶段。在第二阶段,则把标记对象复制到堆栈的新域中以便压缩堆栈。这种收集器也会停止其它操作。
3)复制收集器
它将堆栈分为两个域,称为半空间,它每次仅使用一个半空间。GC运行时,JVM生成的新对象放在一个半空间中,将可到达对象复制到另一个半空间中,从而压缩了堆栈。这种方法适用于短生存期的对象,持续复制长生存周期的对象则导致效率降低。
4)增量收集器
增量收集器则把堆栈分为多个域,每次仅从一个域收集垃圾。这会造成较小的应用程序中断。
5)分代收集器
这种收集器把堆栈分为两个或多个域,用以存放不同寿命的对象。JVM生成的新对象一般放在其中的某个域中。过一段时间,继续存在的对象将获得使用期并转入更长寿的域中。分代收集器对不同的域使用不同的算法以优化性能。
6)并发收集器
顾名思义,并发就是同时发生,即它是与应用程序是同时运行的。该收集器在某一时刻(比如压缩时)一般都不得不停止其它操作以完成特定的任务,但是因为其它应用程序可进行其它的后台操作,所以,中断其它处理的实际时间大大降低。
7)并行收集器
并行收集器使用某种传统的算法,并使用多线程并行地执行它们的工作。在多CPU机器上使用多线程技术可以显著地提高Java应用程序的可扩展性。
并发和并行收集器,只是针对整个整个应用程序或硬件CPU的,所以对内存的操作有更特殊的机制
JVM内存区域配置
在充分理解垃圾收集算法执行过程后,才能有效地优化GC的性能。有些垃圾收集专用于特殊的应用程序。比如,实时应用程序主要是为了避免垃圾收集中断,而大多数OLTP应用程序则注重整体效率。理解了应用程序的工作负荷和JVM支持的垃圾收集算法,便可以进行优化配置GC。
Sun的JVM使用的是分代收集器,它把堆分为3个主要的域:新域、旧域和永久域。JVM生成的所有新对象放在新域中,一旦对象经历了一定数量的垃圾收集循环后,便获得使用期并进入旧域。在永久域中JVM则存储类和方法。就配置而言,永久域是一个独立域并且不认为是堆的一部分。
JVM的配置选项中有一些专门用于对这些域进行配置的属性,首先我们通过如下图展示各部分属性的相应配置对象。
配置堆区域
JVM中通常所说的堆(Heap),实质上是新域和旧域的和,它代表了两个区域的内存大小。
一、设置堆的初始大小(Xms,其中s为start)
java -Xms128m // 将堆的初始大小设置为128MB
二、设置堆的最大大小(Xmx,其中x为max)
java -Xmx128m // 将堆的最大大小设置为128MB
TIPS:
1) 初始大小必须小于最大大小
2) 通常可以将初始大小设置为最大大小,这样就可以避免程序动态增加堆的大小。
配置新域和旧域
有3种方法用来设置新域和旧域的大小:
1) 设置新域大小(Xmn,其中n为new)
java -Xms256m -Xmx256m -Xmn64m
// 将新域的初始值和最大值设置为64MB
2) 设置新域的初始值和最大值
可以使用-XX:NewSize 和 -XX:MaxNewsize 设置新域的初始值和最大值
java -Xms256m -Xmx256m -XX:NewSize 64m -XX:MaxNewSize=64m
3)设置新域和旧域的比例
下面的命令把整个堆设置成128MB,新域比例设置为3,即新域和旧域比例为1:3,新域为堆的1/4即32MB。
java -Xms128m -Xmx128m -XX:NewRatio=3
设置了新域大小,旧域大小即为堆的大小减去新域的大小,因此对旧域不需要进行设置,JVM也因此没有提供对旧域的设置属性。
配置永久区域
1) 永久域大小
永久域默认大小为4MB。运行程序时,JVM会调整永久域大小以满足需要。每次调整时,JVM会对堆进行一次完全的垃圾收集。使用-XX:MaxPermSize标志来增加永久域的大小。
java -XX:MaxPermSize=64MB
2) 永久域初始大小
当JVM加载类时,永久域中的对象急剧增加,从而使JVM不断调整永久域大小。为了避免调整,可使用-XX:PermSize标志设置初始值。
下面把永久域初始值设置成32MB,最大值设置成64MB:
java -Xms512m -Xmx512m -Xmn128m -XX:PermSize=32m -XX:MaxPermSize=64m
配置新域子空间
在默认状态下,JVM在新域中使用复制收集器,对旧域使用标记——清除——压缩收集器。在新域中使用复制收集器有很多意义,因为应用程序生成的大部分对象是生命周期较短的。新域一般分为3个部分:
- 第一部分为Eden,用于生成新的对象;
- 另两个部分称为救助空间(Survivor):from救助空间 和 to救助空间;
当Eden被充满时,收集器停止执行应用程序,把所有可到达的对象复制到当前的from救助空间;
当from救助空间被充满时,收集器则把可到达的对象复制到当前的to救助空间。
维持活动的对象在救助空间中不断复制,知道它们获得使用期并转入旧域。
使用-XX:SurvivorRatio可控制新域子空间的大小。
同NewRatio一样,SurvivorRatio规定某救助空间与Eden空间的比值。比如,以下命令把新域设置成64MB,Eden占32MB,每个救助空间各占16MB:
java -Xms256m -Xmx256m Xmn64m -XX:SurvivorRatio=2
通常所有过渡对象在移出Eden空间时将被收集。如果能够这样的话,并且移出Eden空间的对象是长寿命的,那么理论上可以立即把它们移进旧域,避免在救助空间中被反复复制。
但是应用程序不能适应这种理想状态,因为它们中有一小部分中长寿命的对象。最好将这些中长寿命的对象放在新域中,因为复制小部分的对象比压缩旧域廉价。
为控制新域中对象的复制,可用-XX:TargetSurvivorRatio 控制救助空间的比例。该值时设置救助空间的使用比例,是一个百分比,默认值是50。
如救助空间为1M,该值为50表示可用500K。当较大的堆栈使用较低的SurvivorRatio 时,应增加该值到80~90,以更好利用救助空间。用-XX:maxtenuring threshold 可控制上限。
为防止所有的复制全部发生,以及希望对象从Eden扩展到旧域,可以把MaxTenuringThreshold 设置为0。设置完成后,实际上就不再使用救助空间了,因此应把SurvivorRatio设成最大值以最大化Eden空间,设置如下:
java -XX:MaxTenuringThreshold=0 -XX:SurvivroRatio=50000
JVM性能调优实战
通过JVM内存区域的分配即学会了如何使用JVM属性来设置JVM内存大小。那么设置为多少才算是最优的呢?
调优配置参考
JVM的堆大小决定了JVM花费在收集垃圾的时间和频度。
你的应用建立和释放对象的速度决定了垃圾收集的频度。
因此,在编程时,应注意使用对象的缓存,而不是创建新的对象。
对象生存的时间越长,需要收集时间也越长,收集也会变慢。一次完全的垃圾收集应该不超过3~5秒,如果系统花费很多的时间收集垃圾,请减小堆大小。
一般来说,你应该使用物理内存的80% 作为堆大小。对于1GB内存、单CPU的机器来说,下面是一组参考配置:
-Xms800m -Xmx800m // 一般设为同样大小
-Xmn200m // 将NewSize与MaxNewSize设为一致
-XX:PermSize=128m // 设置永久域初始大小
-XX:MaxPermSize=128m // 设置永久域最大大小
-XX:NewSize=200m // 此值设大可调大新对象区减少Full GC次数
-XX:MaxNewSize=200m // 新域最大值
-XX:NewRatio=3 // 设置了该值后可不设NewSize
-XX:SurivivorRatio=4 // 设置救助空间大小
-XX:userParNewGC // 可用来设置并行收集
-XX:ParallelGCThreads // 可用增加并行度
-XX:UseParallelGC // 设置后可以使用并行清除收集器
-XX:UseAdaptiveSizePolicy // 上面一个联合使用效果更好,利用它可以自动优化新域大小及救助空间比值
通常当我们遇到java.lang.OutOfMemoryError:Java heap space的错误,这个问题的根源是JVM虚拟机的默认Heap大小是64MB,可以通过设置其最大和最小值来实现。解决办法有如下:
1) 可以在Windows中更改系统环境变量
加上JAVA_OPTS=-Xms800m -Xmx800m
2) 如果用的Tomcat,在Windows下可以在Tomcat主目录\bin\catalina.bat 中加上:
set JAVA_OPTS='Xms800m -Xmx800m'
3) 如果是Linux系统,在Tomcat主目录的/bin/catalina.sh 的前面加上:
set JAVA_OPTS='Xms800m -Xmx800m'
调优实战
为了能够将JVM GC的调优应用在具体的实践当中,下面通过5个例子来说明GC的调优。
堆区域设置
堆即Heap Size,堆的大小是新域和旧域之和。设置JVM堆,即使在Java程序运行过程中,设置JVM可以使用的内存空间的大小。
JVM在启动时会自动设置堆的值,其初始空间(-Xms)是物理内存的1/64,最大空间(-Xmx)是物理内存的1/4。可以利用JVM提供的-Xmn、-Xms、-Xmx等选项进行设置。
在JVM中如果98%的时间是用于GC且可用的Heap Size不足2%的时候将抛出java.lang.OutOfMemoryError:Java heap space,此时需要调大最大堆空间。
Heap Size最大不要超过可用物理内存的80%,一般要将-Xms 和 -Xmx选项设置为相同,而-Xmn 为1/4的-Xmx值。
新域设置
一般的新域的大小是整个Heap Size的1/4。
新域的Minor收集率一般在70%以上。
当然在实际应用场景中要根据具体情况进行调整。
新域对应用响应速度的影响
新域的Minor收集占用的时间计算如下:应用线程被中断的总时长/(应用执行总时间+应用线程被中断的总时长)。
对于互联网应用系统的响应稍微慢一些是可以被接受的,但是对于GUI类型的应用响应速度慢将会给用户带来非常糟糕的体验。
新域对应用响应时间的选择
在GUI应用的Heap Size设置为32MB的时候系统等待时间约为0.02秒左右,而设置为64MB的时候等待时间则变成0秒左右。但是32MB的时候系统的Major收集间隔为0.45秒左右,而Heap Size 增加到64MB的时候为1.06秒。
那么应用在运行的时候是选择32MB还是64MB呢?如果应用是Web类型(即要求有较大吞吐量)的应用则使用64MB(即Heap Size 大一些)比较好;对于要求实时响应较高的场合(如GUI型应用)则使用32MB较好一些。
Heap Size 的 -Xms -Xmn 设置不要超出物理内存的大小,否则会提示
Error occurred during initialization of VM Could not reserve enough space for object heap。
使用-XX:+UserParNewGC 属性设置并行收集器
使用-XX:+UserParNewGC 选项的minor 收集的时间要比不使用的短。
如果要进一步深入研究JVM设置,可以参考Sun的网站:http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html(现在已经移入Oracle旗下,可以去Oracle官网查找)
总结
- JVM 内存组成:新域、旧域、永久域;新域由Eden和两个Survivor组成;永久域保存那些在虚拟机整个生存周期都生存的对象(类、方法等),因此该域不需被垃圾收集。
- JVM 配置选项有-X 选项和-XX 选项,可设置3中类型值:Boolean、Numeric、String。
- 垃圾回收的2种方法:引用计数、对象引用遍历。
- 垃圾收集器的7种类型:……
- 设置JVM内存区域的方法
- -Xms
- -Xmx
- -Xmn
- -XX:NewSize
- -XX:MaxNewSize
- -XX:NewRatio
- -XX:MaxPermSize
- -XX:PermSize
- -XX:SurivorRation
