在《Java虚拟机规范》的规定里,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError异常的可能。
注意:不同虚拟机,甚至版本不一样,相关的配置可能也会不一样。这里使用 OracleJDK8里的Hotspot虚拟机来做实验。
一、Java堆溢出
1、什么是堆Dump
堆Dump是反应Java堆使用情况的内存镜像,其中主要包括系统信息、虚拟机属性、完整的线程Dump、所有类和对象的状态等。 一般在内存不足、GC异常等情况下,我们就会怀疑有内存泄露。这时我们就可以制作堆Dump来查看具体情况。
2、若出现 java.lang.OutOfMemoryError: GC overhead limit exceeded
官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。
JVM加一个参数:-XX:-UseGCOverheadLimit
3、内存泄漏与内存溢出区别
1)内存泄漏(Memory Leak)
内存泄漏是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。即内存泄漏的堆积最终会导致内存溢出。
2)内存溢出(Memory Overflow)
内存溢出指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。
4、Java堆溢出实例
配置参数说明:
-Xms参数:设置堆的最小值
-Xmx参数:设置对的最大值
-XX: +HeapDumpOnOutOfMemoryError:可以让虚拟机在出现内存溢出异常的时候Dump出当前的内存堆转储快照以便进行事后分析
-XX:HeapDumpPath=${目录}参数:表示生成DUMP文件的路径,也可以指定文件名称,例如:-XX:HeapDumpPath=目录/java_heapdump.hprof。
-XX:-UseGCOverheadLimit参数:禁用限制GC的运行时间检查(生产环境不要动它)。
注意:如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就是内存溢出,我们应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否有向上调整的合理空间。
实例一:内存泄漏导致内存溢出
//-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/E/JVM/heap_dump.hprof -XX:-UseGCOverheadLimit
public class HeapDemo {
public static void main(String[] args) {
// 把对象放到集合中,避免GC回收掉
Set<Object> set = new HashSet<>();
while (true) {
set.add(new HeapTest());
}
}
static class HeapTest{}
}
二、栈溢出
-Xoss参数(设置本地方法栈大小)虽然存在,但实际上是没有任何效果的。栈的容量大小只能由-Xss 参数设置。
在《Java虚拟机规范》中,对虚拟机栈和本地方法栈的内存区域规定了两类异常状况:
1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
2. 如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。
注意:《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,HotSpot虚拟机的选择是不支持动态扩展,所以:
在HotSpot虚拟机上是不会由于虚拟机栈无法扩展而导致OutOfMemoryError异常。只要线程申请栈空间成功了就不会有OOM,但是如果申请时就失败,依然会出现OOM异常。
在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。
1、栈溢出实例
配置参数说明:
-Xss 参数:设置栈的容量大小
-Xoss参数:设置本地方法栈大小(实际上是没有任何效果的)。
实例一:单线程,减少栈容量空间(通过循环方法不停地创建栈帧,栈帧里的局部变量表里放基本类型)或者定义很多局部变量,把栈帧里的局部变量表撑大。
//-Xss128k
public class StackDemo {
private int var = 1;
public void stackLeak(){
var++;
stackLeak();
}
public static void main(String[] args) {
StackDemo stackDemo = new StackDemo();
stackDemo.stackLeak();
}
}
三、方法区和运行时常量池溢出
由于运行时常量池是方法区的一部分(一块内存区域),所以这两个区域的溢出测试可以放到一起进行。
String类的 intern()方法:当调用 intern 方法时,如果常量池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到常量池中,并返回此 String 对象的引用。
1、方法区溢出实例
在JDK 6或更早之前的HotSpot虚拟机中,String常量池都是分配在永久代中,我们可以通过-XX:PermSize和-XX: MaxPermSize限制永久代的大小,即可间接限制其中String常量池的容量。
-XX:PermSize参数:设置Perm(俗称方法区/永久代)的最小值
-XX: MaxPermSize参数:设置Perm(俗称方法区/永久代)的最大值
在JDK 8中在永久代移除了,使用元空间来代替。字符串常量池和运行时常量池放在了Java堆里。元空间里只存储类和类加载器的元数据信息了。
-XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
-XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。
-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。
实例一:使用JDK 6
使用JDK 6用 -XX:PermSize和 -XX: MaxPermSize限制永久代的大小。
//-XX:PermSize=5M -XX:MaxPermSize=5M
public class MethodDemo {
public static void main(String[] args) {
// 使用Set保持着常量池引用,避免Full GC回收常量池行为
Set<String> set = new HashSet<String>();
int i = 0;
while (true) {
System.out.println(i);
set.add(String.valueOf(i++).intern());
}
}
}
如果使用JDK 7或更高版本的JDK来运行实例一,结果是不会重现JDK 6中的溢出异常,程序会执行下去。
实例二:使用JDK 7+
使用JDK 7或更高版本的JDK用 -XX:MetaspaceSize=N -XX:MaxMetaspaceSize=N参数限制元空间容量执行上面代码。
//-XX:MetaspaceSize=5M -XX:MaxMetaspaceSize=5M
public class MethodDemo {
public static void main(String[] args) {
// 使用Set保持着常量池引用,避免Full GC回收常量池行为
Set<String> set = new HashSet<String>();
int i = 0;
while (true) {
System.out.println(i);
set.add(String.valueOf(i++).intern());
}
}
}
由于字符串常量池放在了Java堆里,程序会执行下去。
实例三:使用JDK 7+
使用JDK 7或更高版本的JDK用-Xms -Xmx参数限制Java堆容量执行上面代码。
//-Xms5M -Xmx5M -XX:-UseGCOverheadLimit
public class MethodDemo {
public static void main(String[] args) {
// 使用Set保持着常量池引用,避免Full GC回收常量池行为
Set<String> set = new HashSet<String>();
int i = 0;
while (true) {
System.out.println(i);
set.add(String.valueOf(i++).intern());
}
}
}
2、运行时常量池溢出实例
方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
静态常量池主要用于存放两大类常量:字面量和符号引用量。如类名、访问修饰符、常量池、字段描述、方法描述等。
运行时常量池将class文件中的常量池载入到运行时常量池中(内存中),并保存在方法区中。
所以,对于这部分区域的实例,基本的思路是运行时产生大量的类去填满方法区,直到溢出为止。
实例四:使用 CGLIB工具类来操作字节码生成新的类。来填满元空间,直到溢出为止。
//-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
public class CGlibDemo {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloWorld.class);
enhancer.setUseCache(false); //关掉缓存
//不改变HelloWorld源码的基础上,在hello方法前后做增强
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("hello方法执行前...");
Object rs = methodProxy.invokeSuper(o, args);
System.out.println("hello方法执行后...");
return rs;
}
});
//动态创建代理类
HelloWorld helloWorld = (HelloWorld) enhancer.create();
helloWorld.hello();
}
}
}
class HelloWorld {
public void hello() {
System.out.println("hello world");
}
}
四、直接内存溢出
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
Java和C++语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++中那样可以自己申请内存和释放内存。但是Java中的Unsafe类为我们提供了类似C++手动管理内存的能力。sun.misc.Unsafe类,一般应用开发者不会用到这个类。
因为虽然使用DirectByteBuffer分配内存也会抛出内存溢出异常,但它抛出异常时并没有真正向操作系统申请分配内存,而是通过计算得知内存无法分配就会在代码里手动抛出溢出异常,真正申请分配内存的方法是Unsafe:allocateMemory()。
参数说明:
-XX:MaxDirectMemorySize参数:设置直接内存的容量大小
JVM堆内存大小可以通过-Xmx来设置,同样的direct ByteBuffer可以通过-XX:MaxDirectMemorySize来设置,此参数的含义是当Direct ByteBuffer分配的堆外内存到达指定大小后,即触发Full GC。注意该值是有上限的,默认是64M,最大为sun.misc.VM.maxDirectMemory(),在程序中中可以获得-XX:MaxDirectMemorySize的设置的值。
实例一:使用unsafe分配本机内存
//-Xmx20M -XX:MaxDirectMemorySize=10M
public class DirectMemoryDemo {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
//通过反射获得某个类的所有声明的字段,即包括public、private和proteced,但是不包括父类的申明字段。
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
// 设置可访问权限,可以获取此类的私有成员变量的value
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while(true){
unsafe.allocateMemory(_1MB);
}
}
}
五、JVM -XX: 参数介绍(转)
1、功能开关参数
参数 | 默认值或限制 | 说明 |
参数 | 默认值 | 功能 |
-XX:-AllowUserSignalHandlers | 限于Linux和Solaris,默认不启用 | 允许为java进程安装信号处理器,信号处理参见类:sun.misc.Signal, sun.misc.SignalHandler |
-XX:+DisableExplicitGC | 默认启用 | 禁止在运行期显式地调用System.gc() |
-XX:+FailOverToOldVerifier | Java6新引入选项,默认启用 | 如果新的Class校验器检查失败,则使用老的校验器(失败原因:因为JDK6最高向下兼容到JDK1.2,而JDK1.2的class info 与JDK6的info存在较大的差异,所以新校验器可能会出现校验失败的情况) |
-XX:+HandlePromotionFailure | java5以前是默认不启用,java6默认启用 | 关闭新生代收集担保 |
-XX:+MaxFDLimit | 限于Solaris,默认启用 | 设置java进程可用文件描述符为操作系统允许的最大值。 |
-XX:PreBlockSpin=10 | -XX:+UseSpinning 必须先启用,对于java6来说已经默认启用了,这里默认自旋10次 | 控制多线程自旋锁优化的自旋次数 |
-XX:-RelaxAccessControlCheck | 默认不启用 | 在Class校验器中,放松对访问控制的检查,作用与reflection里的setAccessible类似 |
-XX:+ScavengeBeforeFullGC | 默认启用 | 在Full GC前触发一次Minor GC |
-XX:+UseAltSigs | 限于Solaris,默认启用 | 为了防止与其他发送信号的应用程序冲突,允许使用候补信号替代 SIGUSR1和SIGUSR2 |
-XX:+UseBoundThreads | 限于Solaris, 默认启用 | 绑定所有的用户线程到内核线程, 减少线程进入饥饿状态(得不到任何cpu time)的次数 |
-XX:-UseConcMarkSweepGC | 默认不启用 | 启用CMS低停顿垃圾收集器,减少FGC的暂停时间 |
-XX:+UseGCOverheadLimit | 默认启用 | 限制GC的运行时间。如果GC耗时过长,就抛OOM |
-XX:+UseLWPSynchronization | 限于solaris,默认启用 | 使用轻量级进程(内核线程)替换线程同步 |
-XX:-UseParallelGC | -server时启用,其他情况下,默认不启用 | 策略为新生代使用并行清除,年老代使用单线程Mark-Sweep-Compact的垃圾收集器 |
-XX:-UseParallelOldGC | 默认不启用 | 策略为老年代和新生代都使用并行清除的垃圾收集器 |
-XX:-UseSerialGC | -client时启用,其他情况下,默认不启用 | 使用串行垃圾收集器 |
-XX:-UseSpinning | java1.4.2和1.5需要手动启用, java6默认已启用 | 启用多线程自旋锁优化 |
-XX:+UseTLAB | 1.4.2以前和使用-client选项时,默认不启用,其余版本默认启用 | 启用线程本地缓存区 |
-XX:+UseSplitVerifier | java5默认不启用, java6默认启用 | 使用新的Class类型校验器 |
-XX:+UseThreadPriorities | 默认启用 | 使用本地线程的优先级 |
-XX:+UseVMInterruptibleIO | 限于solaris,默认启用 | 在solaris中,允许运行时中断线程 |
2、性能参数
参数 | 默认值或限制 | 说明 |
-XX:+AggressiveOpts | JDK 5 update 6后引入,但需要手动启用, JDK6默认启用 | 启用JVM开发团队最新的调优成果。例如编译优化,偏向锁,并行年老代收集等 |
-XX:CompileThreshold=10000 | 1000 | 通过JIT编译器,将方法编译成机器码的触发阀值,可以理解为调用方法的次数,例如调1000次,将方法编译为机器码 |
-XX:LargePageSizeInBytes=4m | 默认4m, amd64位:2m | 设置堆内存的内存页大小 |
-XX:MaxHeapFreeRatio=70 | 70 | GC后,如果发现空闲堆内存占到整个预估上限值的70%,则收缩预估上限值 |
-XX:MaxNewSize=size | 1.3.1 Sparc: 32m, 1.3.1 x86: 2.5m | 新生代占整个堆内存的最大值 |
-XX:MaxPermSize=64m | 5.0以后: 64 bit VMs会增大预设值的30%, 1.4 amd64: 96m, 1.3.1 -client: 32m, 其他默认 64m | Perm(俗称方法区)占整个堆内存的最大值 |
-XX:MinHeapFreeRatio=40 | 40 | GC后,如果发现空闲堆内存占到整个预估上限值的40%,则增大上限值 |
-XX:NewRatio=2 | Sparc -client: 8, x86 -server: 8, x86 -client: 12, -client: 4 (1.3), 8 (1.3.1+), x86: 12, 其他默认 2 | 新生代和年老代的堆内存占用比例, 例如2表示新生代占年老代的1/2,占整个堆内存的1/3 |
-XX:NewSize=2.125m | 5.0以后: 64 bit Vms 会增大预设值的30%, x86: 1m, x86, 5.0以后: 640k, 其他默认 2.125m | 新生代预估上限的默认值 |
-XX:ReservedCodeCacheSize=32m | Solaris 64-bit, amd64, -server x86: 48m, 1.5.0_06之前, Solaris 64-bit amd64: 1024m, 其他默认 32m | 设置代码缓存的最大值,编译时用 |
-XX:SurvivorRatio=8 | Solaris amd64: 6, Sparc in 1.3.1: 25, Solaris platforms 5.0以前: 32, 其他默认 8 | Eden与Survivor的占用比例。例如8表示,一个survivor区占用 1/8 的Eden内存,即1/10的新生代内存,为什么不是1/9? 因为我们的新生代有2个survivor,即S0和S1。所以survivor总共是占用新生代内存的 2/10,Eden与新生代的占比则为 8/10 |
-XX:TargetSurvivorRatio=50 | 50 | 实际使用的survivor空间大小占比。默认是50%,最高90% |
-XX:ThreadStackSize=512 | Sparc: 512, Solaris x86: 320 (5.0以前 256), Sparc 64 bit: 1024, Linux amd64: 1024 (5.0 以前 0), 其他默认 512. | 线程堆栈大小 |
-XX:+UseBiasedLocking | JDK 5 update 6后引入,但需要手动启用, JDK6默认启用 | 启用偏向锁 |
-XX:+UseFastAccessorMethods | 默认启用 | 优化原始类型的getter方法性能(get/set:Primitive Type) |
-XX:-UseISM | 默认启用 | 启用solaris的ISM |
-XX:+UseLargePages | JDK 5 update 5后引入,但需要手动启用, JDK6默认启用 | 启用大内存分页 |
-XX:+UseMPSS | 1.4.1 之前: 不启用, 其余版本默认启用 | 启用solaris的MPSS,不能与ISM同时使用 |
-XX:+UseStringCache | 默认开启 | 启用缓存常用的字符串。 |
-XX:AllocatePrefetchLines=1 | 1 | Number of cache lines to load after the last object allocation using prefetch instructions generated in JIT compiled code. Default values are 1 if the last allocated object was an instance and 3 if it was an array. |
-XX:AllocatePrefetchStyle=1 | 1 | Generated code style for prefetch instructions. 0 – no prefetch instructions are generate*d*, 1 – execute prefetch instructions after each allocation, 2 – use TLAB allocation watermark pointer to gate when prefetch instructions are executed. |
-XX:+UseCompressedStrings | Java 6 update 21有一选项 | 其中,对于不需要16位字符的字符串,可以使用byte[] 而非char[]。对于许多应用,这可以节省内存,但速度较慢(5%-10%) |
-XX:+OptimizeStringConcat | 在Java 6更新20中引入 | 优化字符串连接操作在可能的情况下 |
3、调试参数
参数 | 默认值或限制 | 说明 |
-XX:-CITime | 打印发费在JIT编译上的时间 | |
-XX:ErrorFile=./hs_err_pid<pid>.log | JDK6中引入 | 错误文件 |
-XX:-ExtendedDTraceProbes | JDK6中引入仅在Solaris | 启用性能的影响DTrace探测器 |
-XX:HeapDumpPath=./java_pid<pid>.hprof | 1.4.2 update 12, 5.0 update 7 | 指定HeapDump的文件路径或目录 |
-XX:-HeapDumpOnOutOfMemoryError | 1.4.2 update 12, 5.0 update 7 | 当抛出OOM时进行HeapDump |
-XX:OnError=”<cmd args>;<cmd args>” | 1.4.2 update 9 | 当发生错误时执行用户指定的命令 |
-XX:OnOutOfMemoryError=”<cmd args>; <cmd args>” | 1.4.2 update 12, 6 | 当发生OOM时执行用户指定的命令 |
-XX:-PrintClassHistogram | 1.4.2 | 当Ctrl+Break发生时打印Class实例信息,与jmap -histo相同 |
-XX:-PrintConcurrentLocks | 6 | 当Ctrl+Break发生时打印java.util.concurrent的锁信息, 与jstack -l相同 |
-XX:-PrintCommandLineFlags | 5 | 打印命令行上的标记 |
-XX:-PrintCompilation | 当方法被编译时打印信息 | |
-XX:-PrintGC | 当GC发生时打印信息 | |
-XX:-PrintGCDetails | 1.4.0 | 打印GC详细信息 |
-XX:-PrintGCTimeStamps | 1.4.0 | 打印GC用时 |
-XX:-PrintTenuringDistribution | 打印Tenuring年龄信息 | |
-XX:-TraceClassLoading | 跟踪类加载 | |
-XX:-TraceClassLoadingPreorder | 1.4.2 | 跟踪所有加载的引用类 |
-XX:-TraceClassResolution | 1.4.2 | 跟踪常量池的变化 |
-XX:-TraceClassUnloading | 跟踪类的卸载 | |
-XX:-TraceLoaderConstraints | 6 | Trace recording of loader constraints |
-XX:+PerfSaveDataToFile | 退出时保存jvmstat二进制文件 | |
-XX:ParallelGCThreads= | 设置新生代与老年代并行垃圾回收器的线程数 | |
-XX:+UseCompressedOops | Enables the use of compressed pointers (object references represented as 32 bit offsets instead of 64-bit pointers) for optimized 64-bit performance with Java heap sizes less than 32gb. | |
-XX:+AlwaysPreTouch | Pre-touch the Java heap during JVM initialization. Every page of the heap is thus demand-zeroed during initialization rather than incrementally during application execution. | |
-XX:AllocatePrefetchDistance= | Sets the prefetch distance for object allocation. Memory about to be written with the value of new objects is prefetched into cache at this distance (in bytes) beyond the address of the last allocated object. Each Java thread has its own allocation point. The default value varies with the platform on which the JVM is running. | |
-XX:InlineSmallCode= | 当编译的代码小于指定的值时,内联编译的代码 | |
-XX:MaxInlineSize=35 | 内联方法的最大字节数 | |
-XX:FreqInlineSize= | 内联频繁执行的方法的最大字节码大小 | |
-XX:LoopUnrollLimit= | Unroll loop bodies with server compiler intermediate representation node count less than this value. The limit used by the server compiler is a function of this value, not the actual value. The default value varies with the platform on which the JVM is running. | |
-XX:InitialTenuringThreshold=7 | 设置初始的对象在新生代中最大存活次数 | |
-XX:MaxTenuringThreshold= | 设置对象在新生代中最大的存活次数,最大值15,并行回收机制默认为15,CMS默认为4 |