JVM一些参数
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
作者使用了一些参数来限制JVM的各部分的大小,以快速达到测试目的。这里我想说的是,很多同学说没有条件,工作中没有遇到高并发的场景,其实可以自己搭建一个受限JVM+JMeter+其它必要中间件(如MySQL,Redis等)组合来实现这个目的。
———————————————————————————————
Java堆溢出
通过设置-XX: +HeapDumpOnOutOf-MemoryError 可以让JVM在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。
解决思路:
通过内存映像分析工具对Dump出来的堆转储快照信息进行分析。先确认导致OOM的对象,分清楚到底是Memory Leak还是Memory Overflow。如果是内存泄漏,可进一步通过工具查看泄露对象到GC Roots的引用链,找到泄露对象是通过怎样的路径、与哪些GC Roots相关联才导致垃圾收集器无法回收他们,进而分析内存泄漏代码的具体位置。
public class HeapOOEMTest {
static class OOEM {
}
public static void main(String[] args) {
List<OOEM> ooemTest = new ArrayList<>();
while (true) {
ooemTest.add(new OOEM());
}
}
}
———————————————————————————————
Java栈溢出
1.栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError
2.如果虚拟机栈内存允许动态扩展,当扩展容量无法申请到足够内存时,抛出OOME
通过设置-Xss128K的方式设置JVM的栈容量可以比较方便地模拟该状态。不同的操作系统和不同版本的JVM栈容量会有所不同,主要取决于OS的内存分页大小限制。
如果-Xss设置的过小,一些JVM启动时会报错。而在IDEA中,这样的设置可能不会生效。
无论栈帧太大,还是栈容量太小,当新的栈帧内存无法分配时,HotSpot抛出的都是SOFE
如果是建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。这种通过“减少内存”的手段来解决内存溢出的方式,如果没有这方面处理经验,一般比较难以想到,这一点读者需要在开发32位系统的多线程应用时注意。
public class StackSOFETest {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
StackSOFETest stackSOFETest = new StackSOFETest();
try {
stackSOFETest.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + stackSOFETest.stackLength);
throw e;
}
}
}
———————————————————————————————
方法区和运行时常量池溢出
在JDK 6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,我们可以通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量。
public class MethodAreaOOEMTest {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
int i = 0;
while (true) {
set.add(String.valueOf(i++).intern());
}
}
}
在JDK8或者更高版本的JDK结果不同,如果设置了这两个参数,JVM还会给出提示:
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=6M; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=6M; support was removed in 8.0
String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
在JDK 8以后,永久代便完全退出了历史舞台,元空间作为其替代者登场。
HotSpot还是提供了一些参数作为元空间的防御措施,避免因运行时生成大量的动态类而造成的破坏性结果(JDK1.7之前表现为PermGen space OOME)
-XX:MaxMetaspaceSize:设置元空间最大值,默认是-1,即不限制,或者说只受限于本地内存大小。
-XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过-XX:MaxMetaspaceSize(如果设置了的话)的情况下,适当提高该值。(动态的)
-XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:Max-MetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比。
———————————————————————————————
直接内存溢出
直接内存(Direct Memory)的容量大小可通过-XX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java堆最大值(由-Xmx指定)一致。
思路:由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见有什么明显的异常情况,如果读者发现内存溢出之后产生的Dump文件很小,而程序中又直接或间接使用了DirectMemory(典型的间接使用就是NIO),那就可以考虑重点检查一下直接内存方面的原因了。
public class DirectMemoryOOEMTest {
public static void main(String[] args) throws IllegalAccessException {
Field field = Unsafe.class.getDeclaredFields()[0];
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
while (true) {
unsafe.allocateMemory(1024 * 1024);
}
}
}