内存溢出
内存溢出,OutOfMemory,简称 OOM,通常出现在某一块内存空间耗尽的时候。
堆溢出
示例如下:
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]);
}
}
}
这个 list 对象总是持有 byte 数组的强引用,导致 byte 数据无法回收。运行以上代码,会立刻抛出错误:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Main.main(Main.java:9)
直接内存溢出
示例如下:
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
while (true) {
list.add(ByteBuffer.allocateDirect(1024 * 1024));
}
}
}
不停申请直接内存,list 对象持有直接内存的强引用,gc 时不会被回收,运行会抛出错误:
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:694)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at Main.main(Main.java:10)
默认下最大直接内存(-XX:MaxDirectMemorySize)等于最大堆大小(-Xmx)。
过多线程溢出
示例如下:
public class Main {
public static void main(String[] args) {
while (true) {
new Thread(() -> {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
每个线程的开启都要占用系统内存,因此创建线程数量太多,可能导致 OOM:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
at java.lang.Thread.start0(Native Method)
at java.lang.Thread.start(Thread.java:717)
at Main.main(Main.java:15)
永久区溢出
由于笔者 JDK 1.8 缘故,这里演示元数据区溢出,本质一样。结合第三方依赖 cglib 无限产生类:
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
public class Main {
public static void main(String[] args) {
int count = 0;
try {
while (true) {
Enhancer enhancer = new Enhancer();
// 指定不适用缓存
enhancer.setUseCache(false);
enhancer.setSuperclass(Main.class);
enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> null);
enhancer.create();
count++;
}
} catch (Exception e) {
System.out.println(count);
e.printStackTrace();
}
}
}
运行一段时间,抛出异常:
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:467)
at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:339)
at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:117)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:294)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
at Main.main(Main.java:15)
GC 效率低下
虚拟机会检查下列情况:
- GC 耗时是否超过 98%
- 老年代释放是否小于 2%
- eden 区释放是否小于 2%
- 是否连续最近 5 次同时出现上述情况
如果满足上述条件,虚拟机会抛出异常:
java.lang.OutOfMemoryError: GC overhead limit exceeded#
这个 OOM 只是辅助作用,可以通过 -XX:-UseGCOverheadLimit 禁止产生。
String 在虚拟机中的实现
String 对象的特点
不变性
String 对象一旦生成,则不能再对它进行改变。当一个对象需要多线程共享时,省略同步和锁等待的时间,大幅提高系统性能。
String.substring()、String.concat() 方法并没有修改原始字符串,而是产生了一个新的字符串。如果需要一个可修改的字符串,可以使用 StringBuffer 或者 StringBuilder 对象。
针对常量池的优化
当两个 String 对象拥有相同的值时,它们只因用常量池中的同一个拷贝,节省内存空间。
String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1 == str2); // false
System.out.println(str1 == str2.intern()); // false
System.out.println("abc" == str2.intern()); // true
System.out.println(str1.intern() == str2.intern()); // true
String.intern() 方法返回字符串在常量池中的引用。
类的 final 定义
作为 final 类的 String 对象不可能有任何子类,主要是为了“安全性”和“效率”。
有关 String 的内存泄漏
尽管垃圾回收器已经将内存泄漏的概率大大降低,但是并不意味着没有内存泄漏的可能。
JDK 1.6 版本中,String 对象内部结构如下:

简单翻阅下 JDK 1.6 中 String.substring() 的实现:
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
public String substring(int beginIndex, int endIndex) {
...
return new String(offset + beginIndex, endIndex - beginIndex, value);
}
如果使用 String.substring() 讲一个大字符串切割为小字符串,当大字符串被回收时,小字符串 value 中多余的部分无法被回收,造成内存泄露。
JDK 1.7 大幅调整了 String 的实现,去掉了 offset 和 count,substring 方法不再复用原 String 的 value,而是复制产生新的字符串,避免了内存泄露的发生。
public String(char value[], int offset, int count) {
...
this.value = Arrays.copyOfRange(value, offset, offset + count);
}
public String substring(int beginIndex, int endIndex) {
...
int subLen = endIndex - beginIndex;
return new String(value, beginIndex, subLen);
}
内存泄露,指由于疏忽或者错误未能释放不再使用的内存空间。
有关 String 常量池的位置
虚拟机中,有块称为常量池的区间专门用于存放字符串常量。JDK 1.6 之前,这块区间属于永久区的一部分,JDK 1.7 之后,移到了堆中进行管理。
import java.util.ArrayList;
import java.util.List;
/**
* @author caojiantao
*/
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
while (true){
int i = 0;
list.add(String.valueOf(i++).intern());
}
}
}
String.intern() 方法获得在常量池中的字符串引用,如果常量池中没有该常量字符串,该方法会将字符串加入常量池。
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2245)
at java.util.Arrays.copyOf(Arrays.java:2219)
at java.util.ArrayList.grow(ArrayList.java:242)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
at java.util.ArrayList.add(ArrayList.java:440)
at Main.main(Main.java:13)
虽然 String.intern() 的返回值永远等于该字符串常量,但并不代表相同的字符串 intern() 的返回值都是一样。如果一个字符串在 intern() 调用之后被回收,再此进行一次 intern() 调用,那么该字符串重新被加入常量池,但是引用位置已经不同。
MAT 分析 Java 堆
有点复杂 (・ω・`ll)