一、JVM 内存分配与回收
1. 对象优先在 Eden 区分配
- 大多数情况下,对象在新生代中 Eden区 分配。
当 Eden区 没有足够空间进行分配时,虚拟机将发起一次 MinorGC。
- 发生在 新生代 的 垃圾收集动作。
- MinorGC 非常频繁,回收速度一般也比较快。
- 一般回收 年轻代、老年代、方法区 的垃圾。
- MajorGC 的速度一般会比 MinorGC 的慢 10倍 以上。
-XX:+PrintGCDetails
public class GCTest {
@SuppressWarnings("all")
public static void main(String[] args) {
byte[] allocation1 = new byte[1024 * 60000];
}
}
- 只给 allocation1 分配内存,几乎已经被分配完 Eden区 内存(即使程序什么也不做,新生代也会使用至少 几M 内存)。
- 假如再给 allocation2 分配内存的时候,Eden区 内存几乎已经被分配完了。
- 当 Eden区 没有足够空间进行分配时,虚拟机将发起一次 MinorGC。
GC 期间虚拟机又发现 allocation1 无法存入 Survior 空间,所以只好把新生代的对象提前转移到老年代中去,老年代上的空间足够存放 allocation1,所以不会出现 FullGC。 - 执行 MinorGC 后,后面分配的对象如果能够存在 Eden区 的话,还是会在 Eden区 分配内存。
2. 大对象直接进入老年代
- 大对象 就是 需要大量连续内存空间 的对象(比如:字符串、数组)。
JVM 参数(-XX:PretenureSizeThreshold=0
)可以设置大对象的大小。
- 默认值是 0,意味着 任何对象 都会先在 新生代 分配内存。
- 如果对象超过设置的大小,会直接进入 老年代,不会进入 年轻代。
- 这个参数只在 Serial 和 ParNew 两个收集器下有效。
- 避免为 大对象分配内存 时的 复制操作,降低效率
配置参数 大对象临界值。
-XX:PretenureSizeThreshold=1000000
-XX:+UseSerialGC
3. 长期存活的对象将进入老年代
- 既然 虚拟机 采用了 分代收集 的思想来管理内存。
那么 内存回收 时就必须能识别,哪些对象应放在新生代,哪些对象应放在老年代中。
- 为了做到这一点,虚拟机 给每个对象一个 对象年龄(Age)计数器。
- 如果对象在 Eden区 出生,并经过第一次 MinorGC 后仍然能够存活。
并且能被 Survivor区 容纳的话,将被移动到 Survivor区 空间中,并将 对象年龄 设为 1 岁。 - 对象在 Survivor区 中每熬过一次 MinorGC,对象年龄 就增加 1 岁。
当它的年龄增加到一定程度(默认 15 岁),就会被晋升到 老年代 中。
- 对象晋升到 老年代 的 年龄阈值
配置参数(-XX:MaxTenuringThreshold=15
)。
-XX:MaxTenuringThreshold=10
4. 对象动态年龄判断
- 当前放对象的 Survivor区(放对象的那块 Survivor区)。
一批对象的总大小 大于 这块 Survivor区域 内存大小 的 50%。 - 那么此时 大于等于 这批对象年龄最大值 的 其他对象,就可以 直接进入老年代 了。
- 例如:Survivor区 里现在有一批对象(年龄1、年龄2、年龄n)的多个年龄对象。
- 当对象大小总和 超过了 Survivor区域 的 50%。
- 此时就会把 年龄n 以上的对象都放入 老年代。
- 这个规则是 希望那些可能 长期存活的对象,尽早进入 老年代。
- 对象 动态年龄 判断机制,一般是在 MinorGC 之后触发的
5. MinorGC 后存活的对象 Survivor 区放不下
- 这种情况会把 存活的对象,部分挪到 老年代,部分可能还会放在 Survivor区
6. 老年代空间分配担保机制
- 年轻代 每次 MinorGC 之前,JVM 都会计算下 老年代剩余可用空间。
- 如果这个 可用空间 小于 年轻代里现有的所有对象大小之和(包括垃圾对象)。
- 就会查看(
-XX:-HandlePromotionFailure
)参数是否设置了(JDK-8 默认就设置了)。 - 如果有这个参数,就会看看 老年代的可用内存大小,是否大于之前每一次 MinorGC 后进入老年代的对象的 平均大小。
- 如果上一步结果是 小于 或者 之前说的参数没有设置。
那么就会触发一次 FullGC
,对 老年代 和 年轻代 一起回收一次垃圾。 - 如果回收完,还是没有 足够空间 存放新的对象,就会发生
"OOM"
。
- 剩余存活的 需要挪动到 老年代 的对象大小,还是大于 老年代可用空间。
- 那么也会触发
FullGC
。 FullGC
完之后,如果还是没用空间放 MinorGC
之后的存活对象,则也会发生 "OOM"
。
![在这里插入图片描述](https://img-blog.csdnimg.cn/801f63b87bc9497a8a931d26fcc0b2fb.png)
7. Eden区 与 Survivor区 默认 8:1:1
- 大量的对象 被分配在 Eden区,Eden区 满了后会触发
MinorGC
。
- 可能会有
99%
以上的对象,成为垃圾被回收掉。 - 剩余存活的对象,会被挪到为空的 那块 Survivor区。
- 下一次 Eden区 满了后,又会触发 MinorGC。
- 把 Eden区 和 Survivor区 垃圾对象回收。
- 把剩余存活的对象 一次性挪动到 另外一块为空的 Survivor区。
- 因为 新生代的对象 都是 朝生夕死的,存活时间很短。
- 所以 JVM 默认的
8:1:1
的比例是很合适的。 - 让 Eden区 尽量的大,Survivor区 够用即可。
- JVM 默认有这个参数(
-XX:+UseAdaptiveSizePolicy
),会导致这个比例自动变化。 - 如果不想这个比例有变化,可以设置参数(
-XX:-UseAdaptiveSizePolicy
)。
二、如何判断对象可以被回收
- 堆中几乎放着所有的对象实例。
- 对堆垃圾回收前的第一步,就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
1. 引用计数法
- 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加
1
。 - 当引用失效,计数器就减
1
。 - 任何时候计数器为
0
的对象,就是不可能再被使用的。
- 但是目前主流的虚拟机中,并没有选择这个算法来管理内存。
- 其最主要的原因是,它很难解决对象之间 相互循环引用的问题。
- 除了对象
testA
和 testN
相互引用着对方之外,这两个对象之间再无任何引用。 - 但是他们因为互相引用对方,导致它们的引用计数器都不为
0
。 - 于是引用计数算法,无法通知
GC
回收器回收他们。
public class ReferenceCountingGCTest {
private ReferenceCountingGCTest instance = null;
public static void main(String[] args) {
ReferenceCountingGCTest testA = new ReferenceCountingGCTest();
ReferenceCountingGCTest testB = new ReferenceCountingGCTest();
testA.instance = testB;
testB.instance = testA;
testA = null;
testB = null;
}
}
2. 可达性分析算法
- 就是通过一系列的称为
"GCRoots"
的对象作为起点。 - 从这些节点开始向下搜索引用的对象,找到的对象都标记为 非垃圾对象。
- 其余未标记的对象都是 垃圾对象。
"GCRoots"
根节点。
线程栈的本地变量、静态变量、本地方法栈的变量 等等。
![在这里插入图片描述](https://img-blog.csdnimg.cn/613f8c9974db41ffb843291ed3df2126.png)
3. 常见引用类型
- 强引用。
- 软引用。
- 弱引用。
- 虚引用。
3.1 强引用
public static Person person = new Person();
3.2 软引用
- 软引用:将对象用
SoftReference
软引用类型的对象 包裹。
- 正常情况不会被回收。
- 但是 GC 做完后,发现 释放不出空间 存放新的对象。
- 则会把这些 软引用 的对象回收掉。
- 软引用,在实际中有重要的应用。
例如:浏览器的后退按钮。
- 按后退时,这个后退时显示的网页内容,是重新进行请求还是从缓存中取出呢?
这就要看具体的实现策略了。 - 如果一个网页在浏览结束时,就进行内容的回收。
则按后退查看前面浏览过的页面时,需要重新构建。 - 如果将浏览过的网页存储到内存中,会造成内存的大量浪费,甚至会造成内存溢出。
public static SoftReference<Person> personSoftReference = new SoftReference<>(new Person());
3.3 弱引用
- 弱引用:将对象用
WeakReference
弱引用类型的对象 包裹。 - 弱引用 跟没引用差不多,GC 会直接回收掉,很少用。
public static WeakReference<Person> personWeakReference = new WeakReference<>(new Person());
3.4 虚引用
- 虚引用:也称为 幽灵引用 或者 幻影引用。
- 它是 最弱 的一种引用关系,几乎不用。
4. finalize()
方法最终判定对象是否存活
- 即使在可达性分析算法中不可达的对象,也并非是 “非死不可” 的。
- 这时候它们暂时处于 “缓刑阶段”。
- 要真正宣告一个对象死亡,至少要经历再次标记过程。
- 标记 的前提是 对象在进行可达性分析后,发现没有与
GCRoots
相连接的引用链。
- 筛选的条件是 此对象 是否有必要执行
finalize()
方法。 - 当对象没有覆盖
finalize()
方法,对象将直接被回收。
- 如果 这个对象 覆盖了
finalize()
方法。
finalize()
方法是对象脱逃死亡命运的 最后一次机会。 - 如果对象要在
finalize()
中成功拯救自己。
只要重新 与 引用链上 的任何的一个对象,建立关联即可。 - 譬如:把自己赋值给某个 类变量 或 对象的成员变量。
那在第二次标记时,它将移除出 “即将回收” 的集合。 - 如果对象这时候还没逃脱,那基本上它就真的被回收了。
@Test
public void test2() throws InterruptedException {
ArrayList<Object> list = new ArrayList<>();
long i = 0;
long j = 0;
while (true) {
list.add(new Person(String.valueOf(i++), 18));
new Person(String.valueOf(j--), 20);
Thread.sleep(10);
}
}
5. 如何判断一个类是无用的类
- 类需要同时满足下面三个条件,才能算是 “无用的类”。
- 该类 所有的实例 都已经被回收,也就是 Java 堆中不存在该类的 任何实例。
- 加载该类的
ClassLoader
已经被回收。 - 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。