【来自B站宋红康老师的视频:https://www.bilibili.com/video/BV1BJ41177cp?p=134】
【本篇博客主要介绍:第14章(垃圾回收概述)、第15章(垃圾回收相关算法)】
【城墙内(C++程序员)的人想出去(跳出内存管理),城墙外(Java程序员)的想进来(跳进内存管理)】
【深入理解程序、 jvm实例、进程、线程的关系:https://blog.csdn.net/jiabeis/article/details/82905891】
【对象本身已经不再使用了,但是由于在虚拟机栈中仍然有一些引用在指向着它,所以它没有办法被垃圾回收器回收,因此就说产生了内存泄漏。如果垃圾对象永远无法清除,那么,随着应用系统运行时间的不断增长,垃圾对象所消耗的总内存会持续上升,直到出现内存溢出,造成应用系统崩溃。】
【Java拥有自动内存管理的能力:自动分配内存 + 自动回收内存。程序员基本上不用参与其中,但要理解JVM内部结果,不然JVM出现问题之后,怎么去排查。】
【方法区:就类似于一个接口。JDK7及之前的JDK版本,它的落地实现是永久代;JDK8及之后的JDK版本,它的落地实现时元空间。】
【这是很重要的一章!!!GC:garbage collection。GC:garbage collector。】
【GC的两个过程:垃圾的标记阶段,垃圾的清除阶段】【标记阶段的算法之间作比较,清除阶段的算法之间作比较】
【因为只有堆和方法区才有可能存在垃圾(JVM的其他内存区域,比如 程序计数器、虚拟机栈、本地方法栈 都不会存在垃圾对象),所以会存在GC行为的只有堆和方法区。java虚拟机规范并不要求所有的JVM都回收方法区。频繁收集新生代,较少收集老年代,基本不动方法区。方法区,就类似于一个接口,JDK7及之前的JDK版本,它的落地实现是永久代;JDK8及之后的JDK版本,它的落地实现时元空间。】
【在JVM中究竟如何标记一个对象是不是已经死亡了呢?简答:当一个java对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。判断java对象存活的方式一般分为两种:引用计数算法和可达性分析算法。】
【怎么判断一个人的价值大不大?看一下这个人被需要的程度。你是唯一的存在,别人替代不了你,这个时候你的价值就很大】
【无法解决循环引用,是引用计数算法的致命缺陷!】【问:对象头存放了哪些信息?】【时间复杂度+空间复杂度】
【循环引用;内存泄漏、内存溢出:https://blog.csdn.net/cmm0401/article/details/109283464、举一个内存泄漏的例子。】
注意:下面这个例子是用来证明java中是不是使用“引用计数算法”来进行垃圾回收?
【为什么能被回收呢?如果java使用的是引用计数算法,则不能被回收;现在通过实验测试发现的确被回收了,说明我们使用反证法反正出来: java使用的不是引用计数算法。】
【强引用、弱引用、软引用、虚引用】
【关于可达性分析的重要的文字描述!需要记住!】
【面试题:哪些对象可以作为GC Roots?】
【JDK中的finalize() 方法可以被任何一个对象重写,如果不重写,则该方法的方法体是空的】
【面试题:虚拟机中的java对象一般可以处于三种状态: 可触及状态、不可触及状态、可复活状态,并简述它们的含义。】
【测试finalize( ) 方法:发现在finalize( )方法中可以救活对象。】
package com.atguigu.java;
//测试Object类中finalize()方法,即对象的finalization机制。
public class CanReliveObj {
public static CanReliveObj obj;//类变量,属于 GC Root
//测试场景1:finalize()方法注释掉。测试场景2:finalize()方法不注释掉。
//注意:此方法只能被调用一次
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用当前类重写的finalize()方法");
obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象this建立了联系,所以它就成为了可触及对象,不能被回收了
}
public static void main(String[] args) {
try {
obj = new CanReliveObj();
// 对象第一次成功拯救自己
obj = null;
System.gc();//调用垃圾回收器
System.out.println("第1次 gc");
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("第2次 gc");
// 下面这段代码与上面的完全相同,但是这次自救却失败了
obj = null;
System.gc();
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
【MAT下载的站点:http://www.eclipse.org/mat】
【eclipse关于GCRoots的说明:http://127.0.0.1:62808/help/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fwelcome.html】
package com.atguigu.java;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Scanner;
//测试dump堆内存文件:30多行代码,竟然有1700个GCRoots
public class GCRootsTest {
public static void main(String[] args) {
List<Object> numList = new ArrayList<>();
Date birth = new Date();
for (int i = 0; i < 100; i++) {
numList.add(String.valueOf(i));
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("数据添加完毕,请操作:");
new Scanner(System.in).next();
numList = null;
birth = null;
System.out.println("numList、birth已置空(堆中的对象会因此发生改变),请操作:");
new Scanner(System.in).next();
System.out.println("结束");
}
}
【object4这个对象我已经不用了,但通过GC过程之后它就是被回收不了,这就造成了内存泄漏,如果有很多对象都存在内存泄漏,那么累积起来就会造成内存溢出。另:GC过程为啥会回收不了object4对象呢?是因为object4这个对象直接或者间接的被引用着,它存在着引用链,所以被回收不了。另:通常情况下,我们排查问题时,会专门看某一个对象到GCRoots的引用链。】
package com.atguigu.java;
import java.util.ArrayList;
//-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
public class HeapOOM {
byte[] buffer = new byte[1 * 1024 * 1024]; //1MB的数字
public static void main(String[] args) {
ArrayList<HeapOOM> list = new ArrayList<>();
int count = 0;
try{
while(true){
list.add(new HeapOOM());
count++;
}
}catch (Throwable e){
System.out.println("count = " + count);
e.printStackTrace();
}
}
}
【前面讲解了标记阶段以及标记阶段使用的标记算法,接下来讲解清除阶段以及清除阶段使用的算法。面试经常问!】
【垃圾清除阶段的三大基本算法:(1)标记-清除算法(2)复制算法(3)标记-压缩算法。】
【标记的是可达对象,而不是标记垃圾对象。而且,在标记的时候,会把“可达对象的标识”记录在可达对象的对象头中。】
【标记-清除算法的过程:(1) 标记阶段:需要从GCRoots开始遍历(第一次遍历),标记出所有被引用的对象。(2) 清除阶段:需要对堆内存从头到尾进行线性遍历(第二次遍历),如果发现某个对象在其Header中没有被标记为可达对象,则将其回收。】
【注意:再次分配新对象内存时,可以使用“空闲列表”方式。对象的实例化见博客:https://blog.csdn.net/cmm0401/article/details/108886227】
【复制算法的过程:需要从GCRoots开始遍历(全程只做一次遍历),凡是能够遍历到的对象都是可达对象,都是存活的,于是按照顺序逐一把存活对象从区域A复制到“之前是完全空白的区域B”,并且,在复制可达对象至区域B的时候,是按照顺序连续存储在区域B的,保证了区域B的内存连续性 。当把区域A中可达对象全部复制到区域B之后,区域A对于我们来说已经无用了,所以直接清空区域A即可。那么,接下来JVM分配新的对象内存的时候直接从区域B开始分配就行了。等到下次垃圾清除的时候,整个过程反过来即可,所以,区域A和区域B是交换着使用,内存利用率为50%,相对来说比较低(可以说是:以空间换时间)。这里要注意:从GCRoots遍历可达对象的时候,一旦遍历到了,不需要给它再做标记了,直接把它复制到区域B即可。注意:再次分配新对象内存时,可以使用“指针碰撞”方式。】
【类似于新生代中的S0和S1区域:https://blog.csdn.net/cmm0401/article/details/108886227。新生代中的S0区域和S1区域使用的垃圾回收算法,实际上就是复制算法,记住!!!】
【上面截图中的“特别的”,写的有问题,这里更正一下:如果系统中的可达对象非常非常多,此时复制算法的效率并不会很高。这是为什么呢?因为如果系统中的可达对象非常非常多,那么复制算法需要从区域A复制到区域B的存活对象的数量就会非常非常多,所以,此时的复制算法效率不会很高。因此,复制算法比较适用于 需要复制的存活对象数量不会特别多才行。】
【最糟糕的情况是:垃圾回收的一圈,发现系统中什么垃圾都没被回收掉,而且系统中的全部对象都平白无故的复制了一份过去,栈的局部变量表中维护的“对用引用地址”也都平白无故的全部更新了一遍,这不是无用的操作吗,效率很低。】
【所以,复制算法比较适用于 需要复制的存活对象数量不会特别多才行(也即:少部分对象存活、大部分对象死亡的场景)。思考一下发现,新生代中的对象大部分都是朝生夕死(70%-99%),恰好适合于复制算法!!!赞!!!另:老年代就不适合用复制算法,因为老年代中的对象大部分都是存活的!!!】
【标记-压缩算法的过程:(1) 标记阶段:需要从GCRoots开始遍历(第一次遍历),标记出所有被引用的对象,也即标记出哪些是可达对象。(2) 压缩阶段:把可达对象按照顺序依次移动过去,并从“内存最开始的位置”顺序存储。之后,再把边界之外的所有空闲空间都清理掉即可。由此可见,标记-压缩算法过程执行完之后,存活对象都是按照顺序依次排列在内存的一端,另外一端都是空闲区域,实现了内存的无碎片化。注意:再次分配新对象内存时,可以使用“指针碰撞”方式。】
【对象的实例化见博客:https://blog.csdn.net/cmm0401/article/details/108886227】
【综合来看,发现“复制算法”(应用在新生代垃圾回收)与“标记-压缩算法”(应用在老年代垃圾回收)还是不错的。】
【目前,垃圾回收算法更多关注的低延迟!!!】