[3] 内存分配策略与引用
[3] 内存分配策略与引用

1. 内存溢出与内存泄漏

1.1 内存溢出(OutOfMemory,OOM)

  • JVM 的堆内存空间设置不够大
  • 代码创建了大量的大对象,并且长时间不能被垃圾收集器回收(存在被引用)

1.2 内存泄漏

只有对象不再被程序用到了,但是垃圾收集器又不能回收它们的情况,才叫内存泄漏(Memory Leak)
宽泛意义上的内存泄漏是指在实际情况中:有一些疏忽导致对象的生命周期变的很长,甚至发生 OOM。
📔
例如:单例的生命周期和程序是一样长,如果在单例程序中,持有对外部对象的引用的话,那么这个外部对象是不能被回收的,会导致内存泄漏。
📔
例如:一些提供 close() 方法的资源未关闭导致内存泄漏,如数据库连接、网络连接、 I/O 等。

2. 内存分配策略

对象的内存分配,就是在堆上分配(也可能经过 JIT 编译后被拆散为标量类型并间接在栈上分配),对象主要分配在新生代的 Eden 区上,少数情况下可能直接分配在老年代,分配规则不固定,取决于当前使用的垃圾收集器组合以及相关的参数配置。
以下列举几条最普遍的内存分配规则。

2.1 对象优先在 Eden 分配

大多数情况下(栈上分配情况除外),对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,JVM 将发起一次 Minor GC。

2.2 大对象直接进入老年代

大对象是指需要大量连续内存空间的 Java 对象,如很长的字符串或数据。
一个大对象能够存入 Eden 区的概率比较小,发生分配担保的概率比较大,而分配担保需要涉及大量的复制,就会造成效率低下。
JVM 提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制(HotSpot VM 新生代采用复制算法回收垃圾)。

2.3 长期存活的对象将进入老年代

JVM 在对象头中给每个对象定义了一个对象年龄计数器。当新生代发生一次 Minor GC 后,存活下来的对象年龄 + 1,当年龄超过一定值时,就将超过该值的所有对象转移到老年代中去。
使用 -XX:MaxTenuringThreshold 设置新生代的最大年龄(默认 15),只要超过该参数的新生代对象都会被转移到老年代中去。

2.4 动态对象年龄判定

如果当前新生代的 Survivor 中,相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄 >= 该年龄的对象就可以直接进入老年代,无须等到 -XX:MaxTenuringThreshold 中要求的年龄。

2.5 空间分配担保

为对象分配内存空间时,如果 Eden + Survivor 中空闲区域无法装下该对象,会触发 MinorGC 进行垃圾收集。但如果 Minor GC 过后依然有超过 10% 的对象存活,这样存活的对象直接通过分配担保机制进入老年代,然后再将新对象存入 Eden 区。
通过清除老年代中废弃数据来扩大老年代空闲空间,以便给新生代作担保。这个过程就是分配担保

JDK 6 Update 24 之前的分配担保规则
在发生 Minor GC 之前,JVM 会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间, 如果这个条件成立,Minor GC 可以确保是安全的。
如果不成立,则 JVM 会查看 -XX:HandlePromotionFailure 值是否设置为允许担保失败, 如果是,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小, 如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的; 如果小于,或者 -XX:HandlePromotionFailure 设置不允许冒险,那此时也要改为进行一次 Full GC。

JDK 6 Update 24 之后的分配担保规则
只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。

3. 引用的种类

判定对象是否存活与「引用」有关。在 JDK 1.2 以前,Java 中的引用定义很传统,一个对象只有被引用或者没有被引用两种状态,我们希望能描述这一类对象:
  • 当内存空间还足够时,则保留在内存中。
  • 如果内存空间在进行 GC 后还是非常紧张,则可以抛弃这些对象。
很多系统的缓存功能都符合这样的应用场景。在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为了以下几种类型。不同的引用类型,主要体现的是对象不同的可达性状态 reachable 和 GC 的影响。

3.1 强引用(Strong Reference)

类似 Object obj = new Object() 这类的引用,就是强引用,强引用可以直接访问目标对象。只要强引用存在,垃圾收集器永远不会回收被引用的对象。但是,如果我们错误地保持了强引用,比如:赋值给了 static 变量,那么对象在很长一段时间内不会被回收,强引用是造成 Java 内存泄漏的主要原因之一。

3.2 软引用(Soft Reference)

软引用是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象,并可选的把引用存放到一个引用队列。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

3.3 弱引用(Weak Reference)

弱引用强度比软引用更弱一些。当 JVM 进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。

3.4 虚引用(Phantom Reference)

虚引用也称「幽灵引用」或者「幻影引用」,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响。它仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制。

3.5 终结器引用(Finalizer Reference

用以实现对象的 finalize() 方法,所以被称为终结器引用。它无需手动编码,其内部配合引用队列使用。GC 时,终结器引用入队,由 Finalizer 线程通过终结器引用找到被引用对象并调用其 finalize() 方法,当第二次 GC 时才能回收被引用对象。