每个Java程序员迟早都会碰到下面这个错误:
- java.lang.OutOfMemoryError
这个时候一般会建议采用如下方式解决这个错误:
- 增加MaxPermSize值
- 增加最大堆内存到512M(-xmx参数)
这篇文章会具体介绍Java堆空间和参数MaxPermSize的含义。这篇文章涉及下列主题,并采用Hotspot JVM:
- 垃圾回收器(Garbage Collector,GC)
- 哪个JVM?
- JVM命令行选项
垃圾回收器
垃圾回收器负责:
- 分配内存
- 保证所有正在被引用的对象还存在于内存中
- 回收执行代码已经不再引用的对象所占的内存
应用执行时,定位和回收垃圾对象的过程会占用总执行时间的将近25%,这会拖累应用的执行效率。
Hotspot VM提供的垃圾回收器是一个分代垃圾回收器(Generational GC)[9,16,18]-将内存划分为不同的阶段,也就是说,不同的生命周期的对象放置在不同的地址池中。这样的设计是基于弱年代假设(Weak Generational Hypothesis):
1.越早分配的对象越容易失效;
2.老对象很少会引用新对象。
这种分代方式可以减少垃圾回收的停顿时间以及大范围对象的回收成本。Hotspot VM将其堆空间分为三个分代空间:
1. 年轻代(Young Generation)
○ Java应用在分配Java对象时,这些对象会被分配到年轻代堆空间中去
○ 这个空间大多是小对象并且会被频繁回收
○ 由于年轻代堆空间的垃圾回收会很频繁,因此其垃圾回收算法会更加重视回收效率
2. 年老代(Old Generationn)
○ 年轻代堆空间的长期存活对象会转移到(也许是永久性转移)年老代堆空间
○ 这个堆空间通常比年轻代的堆空间大,并且其空间增长速度较缓
○ 由于大部分JVM堆空间都分配给了年老代,因此其垃圾回收算法需要更节省空间,此算法需要能够处理低垃圾密度的堆空间
3. 持久代(Permanent Generation)
○ 存放VM和Java类的元数据(metadata),以及interned字符串和类的静态变量
(Java Interned String分为两类,一类是literal string。就是字符常量。比如通过以下语句得到的字符串:
String literal = "I am a literal string";
另一类是用户调用 java.lang.String.intern() 得到的。 与字符常量不同,该类字符串只能是弱引用的(weakly referenced)。)
次收集(Minor GC)和全收集(Full GC)
当这三个分代的堆空间比较紧张或者没有足够的空间来为新到的请求分配的时候,垃圾回收机制就会起作用。有两种类型的垃圾回收方式:次收集和全收集。当年轻代堆空间满了的时候,会触发次收集将还存活的对象移到年老代堆空间。当年老代堆空间满了的时候,会触发一个覆盖全范围的对象堆的全收集。
次收集
- 当年轻代堆空间紧张时会被触发
- 相对于全收集而言,收集间隔较短
全收集
- 当老年代或者持久代堆空间满了,会触发全收集操作
- 可以使用System.gc()方法来显式的启动全收集
- 全收集一般根据堆大小的不同,需要的时间不尽相同,但一般会比较长。不过,如果全收集时间超过3到5秒钟,那就太长了[1]
全收集通常时间最长,并且是程序无法延迟执行或者无法达到吞吐量目标的主因。GC的目标是去减少程序运行过程中垃圾回收的频率。为了达到这个目的,可以从这两方面入手:
- 从系统方面考虑:
○ 尽量采用大堆,但是不要大到需要系统从磁盘上“换”页。一般而言,可用的RAM(没有被系统进程占用的)的80%都应该分配给JVM。
○ Java堆空间越大,垃圾回收器和java应用在吞吐量(throughput)和延迟执行(latency)方面的效果越好。
- 从应用方面考虑:
○ 减少对象分配(object allocations)操作,或者采用对象保留(object retention)方式有助于减小存活的数据大小,(不理解)这也可以反过来帮助垃圾回收做的更好。
○ 参考这篇文章—Java性能提升窍门[19]
内存溢出错误(OutOfMemoryError)
可怕的内存溢出错误是Java程序员最不愿意看到的。然而这个错误还是会出现,尤其应用中涉及到大量的数据处理时,或应用运行时间过长时。
一个应用所占内存大小包括:
- Java堆大小
- 线程栈
- I/O缓冲区
- 原生库所分配的内存
当一个应用耗尽了内存并且JVM GC也无法回收任何对象空间的时候,就会发生内存溢出错误。但是,内存溢出错误并不一定就意味着内存泄露(memory leak)。也有可能只是一个配置问题,例如设置的堆大小(如果没有设置那就是缺省的堆大小)对于应用来说是不够用的。
JVM命令行参数
无论是客户端应用还是服务器端应用,一旦系统运行缓慢并且垃圾回收所占时间过长,你就会希望通过调整堆大小来改善这一点。不过,为了不影响其他也跑在同一个系统中的应用,不应该将堆大小设置的过大。
GC调优是很重要的。找到最佳的分代堆空间是一个迭代的过程[3,10,12]。这里我们假定你已经为你的应用找到了最佳堆大小。那么你可以采用下面的JVM命令来进行设置:
GC 命令行选项 | 描述 |
-Xms | 设置Java堆大小的初始值/最小值。例如:-Xms512m (请注意这里没有”=”). |
-Xmx | 设置Java堆大小的最大值 |
-Xmn | 设置年轻代对空间的初始值,最小值和最大值。请注意,年老代堆空间大小是依赖于年轻代堆空间大小的 |
-XX:PermSize=<n>[g|m|k] | 设置持久代堆空间的初始值和最小值 |
-XX:MaxPermSize=<n>[g|m|k] | 设置持久代堆空间的最大值 |
最后一点,最早在Java SE 5.0中有对服务器的人机工程学的介绍[13]。这个可以很好的减少服务器端应用的调优时间,尤其是在堆大小测量和复杂GC调优方面。很多情况下,服务器端调优的最好方式就是不去调优。
参考文章
1 Tuning Java Virtual Machines (JVMs)
2 Diagnosing Java.lang.OutOfMemoryError
3 Java Performance by Charlie Hunt and Binu John
5 GCViewer (a free open source tool)
6 Comparison Between Sun JDK And Oracle JRockit
7 Java SE 6 Performance White Paper
8 F&Q about Garbage Collection
10 Memory Management in the Java HotSpot Virtual Machine White Paper
11 Java Hotspot Garbage Collection
12 FAQ about GC in the Hotspot JVM (with good details)
13 Java Heap Sizing: How do I size my Java heap correctly?
15 JRockit JVM Heap Size Options
16 Pick up performance with generational garbage collection
17 Which JVM?
18 Memory Management in the Java HotSpot™ Virtual Machine
英文原文: XML and more,翻译:ImportNew - 郑雯