1.如何判断对象可回收
核心思想:堆内存中对象没有被任何引用。
在c语言中没有自动化垃圾回收机制,需要开发者自己人工清理堆垃圾,在java中开发自动化方式清理堆垃圾。
引用计数法
引用计数法:每次当该对象引用一次的时候,引用次数都会+1,如果引用的次数为0 则认为没有被引用,直接被垃圾给回收清理掉。
最大的缺陷:A如果引用B,B引用A 但是其他对象没有任何的引用A和B,相互存相互依赖,无法被垃圾回收。
Java虚拟机采用可达性分析算法
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
白话文解释:只要对象的引用和GCRoot没有引用关系则可以进行垃圾回收,反之则不能够进行垃圾回收。解决了循环依赖的问题
哪些可以作为GC Roots
1.虚拟机栈(栈帧中的本地变量表)中引用的对象。
2.元空间中类静态属性引用的对象。
3.本地方法栈中JNI(即一般说的Native方法)引用的对象。
可以使用 MemoryAnalyzer 工具进行文件内存分析,下载地址,https://www.eclipse.org/mat/downloads.php
代码测试:
public class Test001 {
public static void main(String[] args) throws IOException {
ArrayList
-- 打镜像文件命令
jmap -dump:format=b,live,file=a.bin 53120
2.java四种引用方法
2.1:强引用
-Xmx8m :设置堆内存
-XX:+PrintGCDetails -verbose:gc :打印 gc 回收信息
强引用:被引用关联的对象永远不会被垃圾收集器回收
Object object=new Object();那object就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
public class Test01 {
public static void main(String[] args) {
// 强引用
UserInfo userInfo1 = new UserInfo("ming");
UserInfo userInfo2 = userInfo1;
userInfo1 = null;
System.out.println(userInfo2);
}
}
控制台输出结果:com.example.javareference.UserInfo@77459877
2.2:软引用(SoftReference)
软引用:软引用关联的对象,在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常
public class Test02 {
public static void main(String[] args) {
soft1();
// soft2();
}
/**
* 软引用:打印已经被清理的对象
*/
private static void soft1() {
ArrayList> objects = new ArrayList<>();
for (int i = 0; i < 10; i++) {
SoftReference softReference = new SoftReference<>(new byte[4 * 1024 * 1024]);
System.out.println(softReference.get());
objects.add(softReference);
}
System.out.println("打印结果:");
objects.forEach((t) -> {
System.out.println(t.get());
});
}
/**
* 软引用:不打印已经被清理的对象
*/
private static void soft2() {
ReferenceQueue referenceQueue = new ReferenceQueue<>();
ArrayList> objects = new ArrayList<>();
for (int i = 0; i < 10; i++) {
SoftReference softReference = new SoftReference<>(new byte[4 * 1024 * 1024], referenceQueue);
objects.add(softReference);
}
Reference poll = referenceQueue.poll();
while (poll != null) {
objects.remove(poll);
poll = referenceQueue.poll();
}
System.out.println("打印结果:");
objects.forEach((t) -> {
System.out.println(t.get());
});
}
}
2.3:弱引用(WeakReference)
无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
public class Test02 {
public static void main(String[] args) {
UserInfo userInfo1 = new UserInfo("ming");
// 弱引用
WeakReference userInfoWeakReference = new WeakReference<>(userInfo1);
userInfo1 = null;
// 手动进行 gc 回收
System.gc();
System.out.println(userInfoWeakReference.get());
}
}
控制台输出结果:null
2.4:虚引用(PhantomReference)
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收
public class Test04 {
public static void main(String[] args) {
UserInfo userInfo1 = new UserInfo("ming");
ReferenceQueue
引用队列(ReferenceQueue)
软弱虚对应引用的指针放入到引用队列中,实现清理。
ReferenceQueue referenceQueue = new ReferenceQueue<>();
ArrayList> objects = new ArrayList<>();
for (int i = 0; i < 10; i++) {
SoftReference softReference = new SoftReference<>(new byte[4 * 1024 * 1024], referenceQueue);
objects.add(softReference);
}

3.垃圾收集器回收算法
垃圾收集器在什么时候触发垃圾收集?
当新生代或者是老年代内存满的情况下,开始触发垃圾收集。
3.1 标记清除算法
根据gcRoot对象的引用链,发现如果该对象没有被引用的情况下,则标记为垃圾,然后在清除。
优点:算法非常简单。
缺点:空间地址不连续、空间利用率不高 容易产生碎片化。
标记清除之前效果图:

绿色代表未被引用的空间,黄色代表被引用的空间。
标记清除之后效果图:

白色代表标记清除的空间,出现了空间不连续的情况,而这些不连续的空间只能存储少于等于自身空间大小的对象,所以空间利用率比较低。
3.2 标记整理算法
根据gcRoot对象的引用链,发现如果该对象没有被引用的情况下,则标记为垃圾。
标记整理与标记清除区别在于:避免标记清除算法产生的碎片问题,清除垃圾过程中,会将可用的对象实现移动,内存空间更加具有连续性。
优点:没有内存的碎片问题,空间地址保持连续。
缺点:整理过程中会产生内存地址移动,在移动过程中其他线程无法访问堆内存,效率偏低。Stop-The-World
效果图如下所示:

3.3 标记复制算法
当我们堆内存触发gc的时候,在from区中将可用对象拷贝到to中,在直接将整个from区清空,依次循环切换。
优点:不会产生内存碎片
缺点:比较占内存空间,有二块空间分为两个区域:from 和to区 相等。
标记复制之前效果图:

标记复制之后,经过清理后效果图:

4.分代算法
对我们堆内存空间实现分代,核心分为新生代、老年代。在整个堆内存中,新生代触发GC回收次数比老年代多。
4.1:新生代
刚创建的对象都存在新生代空间,存放生命周期较短的对象的区域。新生代中分为eden区、from区、to区。默认情况下,新生代中eden区、from区、to区比例为8:1:1.
刚创建的对象存放在eden区,当eden区满的时候,幸存的对象晋升存放到from区或者是to区,from区和to区使用的是标记复制算法。
4.2:老年代
存放生命周期较长的对象的区域。
哪些对象会晋升到老年代中
1:年限达到。当 GC 多次回收的时候,如果一直引用能达到一个阈值的情况下,直接晋升到老年代。阈值可以自己设置
例子:假如有二个对象A、B经过多次的新生代GC回收之后依然还被引用的话,就会晋升到老年代。
2:大对象。如果存放的对象的内存大于新生代空间的内存,则直接存放到老年代中。
例子:假如新生代空间为5M、老年代空间为10M,如果这时候创建的对象为6M,这时候对象则会直接存放到老年代空间中。如果创建的对象为11M,则会报OM(内存溢出)异常。
相同点: 都存储在Java堆上。
不同点:新生代触发的是MinorGC、老年代触发的是FullGC。默认情况下,新生代与老年代存储空间比例为1:2。
4.3:程序发生内存溢出的原因
存放对象的空间大小大于我们老年代的空间大小。
4.4:为什么在分代算法中,新生代有from区和to区且大小一样
因为新生代中 GC 触发非常频繁,为了能够更加高效的清理垃圾,所以采用标记复制算法。
4.5:分代算法中,老年代为什么使用标记整理算法
因为在老年代空间满的情况下会触发 FullGC进行垃圾回收,FullGC 垃圾回收会同时回收新生代、老年代、元空间三个区域。

代码演示:
-Xms18m -Xmx18m -XX:+PrintGCDetails -verbose:gc
设置堆初始空间、最大空间为20M,并在控制台打印出详细的垃圾回收信息。
18M空间:新生代与老年代空间比例为1:2,新生代占6M ,老年代占12M。
1 public class Test05 {
2
3 public static void main(String[] args) {
4 // -Xms18m -Xmx18m -XX:+PrintGCDetails -verbose:gc
5 ArrayList
由上面控制台打印信息看出第11、12、14行进行 GC 垃圾回收在新生代区域(PSYoungGen)。
第13、15行进行 FullGC 垃圾回收,包括 新生代(PSYoungGen)、老年代(ParOldGen)、元空间(Metaspace) 三个区域。
[PSYoungGen: 2617K->504K(5632K)]:表示新生代空间回收之前是2617k,回收之后是504k,5632k是整个新生代空间的大小。等于20行:total 5632K。
19行以下打印的是 新生代、老年代、元空间的内存使用情况。
5.GC日志分析工具
GCViewer、GCEasy、GCHisto、GCLogViewer 、Hpjmeter、garbagecat
https://gceasy.io/