(obj);
虚引用
其引用的对象被回收后 其会被先放入队列中 随后被回收(未被强引用引用)
终结器引用
用来追踪并把实现 finalize() 的对象放入终结器队列,等其 finalize 方法执行完后再由 GC 释放其内存 上述五种引用我们来关联垃圾回收验证其内存溢出问题:
验证强引用-软引用内存溢出问题:
public class Ying_yong {static final int _4MB = 4 * 1024 * 1024;public static void main(String[] args) {//强引用ArrayList<byte[]> list = new ArrayList<>();for (int i = 0; i < 5; i++) {list.add(new byte[_4MB]);}
// soft();}//软引用 内存不足后 触发GC回收掉了public static void soft() {ArrayList<SoftReference<byte[]>> list = new ArrayList<>();for (int i = 0; i < 5; i++) {SoftReference<byte[]> sf = new SoftReference<>(new byte[_4MB]);list.add(sf);}for (SoftReference<byte[]> softReference : list) {System.out.println(softReference.get());}}
}
注:我们设置堆大小20M
首先运行强引用 内存溢出 垃圾回收未触发
运行软引用 触发了GC垃圾回收 并打印出了东西 前面几个对象已经被回收掉了
2.垃圾回收算法
标记存活的对象(即GC Roots搜索到的对象) 清除掉未被标记(即可回收)的对象
速度效率很快 但是缺点在于内存空间造成不连续大量碎片
这个很简单理解 GC Roots标记存活对象 没被标记的即可回收的对象 被回收
所有存活对象向一侧移动 然后移动的时候清理掉可回收的对象
速度效率慢 但是整体紧凑 不会出现大量碎片空间
分成两块内存大小一样 然后将第一块标记好的存活对象复制到第二块
清除掉第一块只剩下的可回收对象 交换以此反复
缺点就是占用双倍内存空间 也不会造成内存碎片
至于from区和to区是举例用的 后面分代回收会着重讲
接下来是重点即分代回收
首先分为新生代(年轻代)和老年代
新生代存放新创建的对象 采用复制算法 Minor GC
老年代存放多次使用存活时间长的对象 采用标记-清除/整理 算法 Full GC
分代回收的过程在复制算法讲过就是如下:
首先将eden和from区可存活的对象放入to区 年龄+1
若对象寿命达到阈值则移到老年代
然后清空eden和from区可回收的垃圾
交换from和to区 以此反复
大部分时间只会进行 Minor GC 只有内存极端不足或老年代对象占用过多时才触发 Full GC
Minor GC 通过复制算法快速清理大部分垃圾,避免了 Full GC 的开销
Full GC 只在必要时才触发,采用 mark‑compact(标记‑压缩)以解决老年代碎片问题
我们当然需要代码实际中来验证:
public class Test_GC_options {static final int _7MB = 7 * 1024 * 1024;static final int _10MB = 10 * 1024 * 1024;public static void main(String[] args) {}
}
加入参数 打印参数观察
堆大小20M 新生代10M 单线程垃圾收集器 打印GC基础信息
-Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
不加入任何对象 运行
我们拿出来打印信息Heap堆def new generation新生代 我们设置的10M total 9216K, used 2352K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)eden space 8192K, 28% used [0x00000000fec00000, 0x00000000fee4c010, 0x00000000ff400000)from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)tenured generation 老年代 10M total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace 元空间 used 579K, committed 768K, reserved 1114112K
class space used 36K, committed 128K, reserved 1048576K
我们发现eden区已经用了28%
这个时候我们强引用加入存活对象 看变化
ArrayList<byte[]> list = new ArrayList<>();list.add(new byte[_7MB]);
加入7M后 因为eden区不够了 放入了from区 实际上是放到了to区 然后from区和to区互换了
这个时候我们在加入10M 这个时候新生代肯定顶不住了
list.add(new byte[_10MB]);
首先肯定溢出了 总共才20M 然后我们发现新生代不够了 就放入了老年代
至此验证完毕
3.GC参数和垃圾收集器
关于GC参数大概就是jvm的设置参数
我记得的:
-Xms 初始堆大小 -Xmx堆最大大小
两个设为相同 避免运行时扩容带来的停顿
-Xmn 新生代大小
-XX:+PrintGCDetails -verbose:gc 打印详细GC信息 基础GC打印
-XX:+UseSerialGC 单线程垃圾收集器
更多参考:https://pdai.tech/md/java/jvm/java-jvm-param.html
1.串行垃圾回收器Serial(复制算法) Serial Old(标记-整理算法)
-XX:+UseSerialGC
只使用一个线程进行垃圾回收工作 前者工作于新生代 后者老年代
2.吞吐量优先垃圾回收器
-XX:+UseParallelGC 大多数jdk8+版本默认并行GC
并行多线程工作
3.响应时间优先处理器 CMS 收集器
XX:+UseConcMarkSweepGC 采用标记-清除算法 JDK18+已移除
四个阶段
初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象 速度很快 需要停顿
并发标记: 进行 GC Roots Tracing 的过程 它在整个回收过程中耗时最长 不需要停顿
重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录 需要停顿
并发清除: 不需要停顿
这里还需要介绍一个G1(Garbage-First):
首先我们需要区别一个东西 一中总结的GC(1)表示的这次GC的序号
而不是接下来我们要说的G1
核心就是:把堆划分为若干相等大小的Region
(整体标记-整理算法实现 局部(两个Region之间)复制算法实现)
关于每个Region区 过程类似分代回收但也有区别
1.先在Young Region对象分配 达到阈值分配到老年代 触发Minor GC
2.若老年代也达到阈值就触发并发标记
3.有条件性的垃圾回收
另外插一嘴:大小达到甚至超过分区大小一半的对象称为巨型对象
GC会把它直接放进老年代 可减少(新生代处理时)停顿
4.GC调优
我就说俩问题 具体调优不是一成不变的 会根据环境和机器的情况调整
【新生代是否越大越好吗
不一定 可能会导致新生代的Minor GC停顿时间变长
若晋升率过高可能触发更多的Full GC
Full GC和Minor GC频繁
空间内存不足导致 解决方案均衡新生代和老年代大小比例】
更多调优参考:https://pdai.tech/md/java/jvm/java-jvm-oom.html