羊蹓狼 发表于 2024-11-5 07:24:04

一文夯实垃圾网络的理论基础

如何判断一个引用是否存活

引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时间计数器为 0 的对象就是不可能再被使用的。
长处:可即刻采取垃圾,当对象计数为0时,会立刻采取;
弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放。JVM不用这种算法
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404251647806.gif
可达性分析算法

通过 GC Root 对象为起点,从这些节点向下搜索,搜索所走过的路径叫引用链,当一个对象到 GC Root没有任何的引用链相连时,说明这个对象是不可用的。
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404251647808.gif

[*]JVM中的垃圾采取器通过可达性分析来探索全部存活的对象
[*]扫描堆中的对象,看可否沿着GC Root对象为起点的引用链找到该对象,如果找不到,则表示可以采取
GC Root的对象有哪些?


[*]虚拟机栈(栈帧中的本地变量表)中引用的对象,例如各个线程被调用的方法栈用到的参数、局部变量或者临时变量等。 
[*]方法区中类静态属性引用的对象或者说Java类中的引用范例的静态变量。
[*]方法区中常量引用的对象或者运行时常量池中的引用范例变量。
[*]本地方法栈中JNI(即一样平常说的Native方法)引用的对象
[*]JVM内部的内存数据结构的一些引用、同步的监控对象(被修饰同步锁)。
方法区的采取

因为方法区主要存放永久代对象,而永久代对象的采取率比新生代低很多,因此在方法区上举行采取性价比不高。
主要是对常量池的采取和对类的卸载。
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都必要虚拟机具备类卸载功能,以保证不会出现内存溢出。
类的卸载条件很多,必要满意以下三个条件,并且满意了也不肯定会被卸载:

[*]该类全部的实例都已经被采取,也就是堆中不存在该类的任何实例。
[*]加载该类的 ClassLoader 已经被采取。
[*]该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
可以通过 -Xnoclassgc 参数来控制是否对类举行卸载。
finalize()

finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。(Java 9中已弃用)
当一个对象可被采取时,如果必要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。自救只能举行一次,如果采取的对象之前调用了 finalize() 方法自救,背面采取时不会调用 finalize() 方法。
引用范例

四个引用的特点:

[*]强引用:gc时不会采取
[*]软引用:只有在内存不够用时,gc才会采取
[*]弱引用:只要gc就会采取
[*]虚引用:是否采取都找不到引用的对象,仅用于管理直接内存
强引用

平常常见的
Object object = new Object();只要一个对象有强引用,垃圾采取器就不会举行采取。即便内存不够了,抛出OutOfMemoryError异常也不会采取。因此强引用是造成java内存走漏的主要缘故原由之一。 对于一个平凡的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相 应(强)引用赋值为 null,就是可以被垃圾网络的了,详细采取时机还是要看垃圾网络策略。
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404251647804.gif
/**
* 一个对象
* 重写finalize方法,可以知道已经被回收的状态
*/
public class OneObject {
    @Override
    protected void finalize() throws Throwable {
      System.out.println("啊哦~OneObject被回收了");
    }
}

/**
* 强引用例子
*/
public class ShowStrongReference {
    public static void main(String[] args) {
      // 直接new一个对象,就是强引用
      OneObject oneObject = new OneObject();
      System.out.println("输出对象地址:" + oneObject);
      System.gc();
      System.out.println("第一次gc后输出对象地址:" + oneObject);
      oneObject = null;
      System.gc();
      System.out.println("置为null后gc输出对象地址:" + oneObject);
    }
}

//输出:
输出对象地址:com.esparks.pandora.learning.references.OneObject@72ea2f77
第一次gc后输出对象地址:com.esparks.pandora.learning.references.OneObject@72ea2f77
置为null后gc输出对象地址:null
啊哦~OneObject被回收了软引用

特点:软引用通过java.lang.SoftReference类实现。只有在内存不够用时,gc才会采取
软引用的生命周期比强引用短一些。只有当 JVM 认为内存不敷时,才会去试图采取软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 之前,清算软引用指向的对象。软引用可以和一个引用队列(ReferenceQueue)团结使用,如果软引用所引用的对象被垃圾采取器采取,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被采取。如果队列为空,将返回一个null;否则该方法返回队列中前面的一个Reference对象。
SoftReference<OneObject> oneObjectSr = new SoftReference<>(new OneObject());https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404251647817.gif
当内存充足的时间,垃圾采取器不会举行采取。当内存不够时,就会采取只存在软引用的对象释放内存。
常用于本地缓存处理。
/** * 软引用 * 内存不够了就会采取 * 注意,运行时必要保证heap大小为35m,即小于实行中全部对象的大小,才能触发gc * -Xmx35m * */public class ShowSoftReference {    public static void main(String[] args) {      // 我们必要通过SoftReference来创建软引用      SoftReference<OneObject> oneObjectSr = new SoftReference<>(new OneObject());      // 我们这里创建一个大小为20m的数组      SoftReference arraySr = new SoftReference(new byte);      System.out.println("软引用对象oneObjectSr的地点:" + oneObjectSr);      System.out.println("通过oneObjectSr关联的oneObject对象的地点:" + oneObjectSr.get());      System.out.println("数组的地点:" + arraySr);      System.out.println("通过arraySr关联的byte数组的地点:" + arraySr.get());      System.gc();      System.out.println("正常gc一次之后,oneObject对象并没有采取。地点" + oneObjectSr.get());      // 再创建另一个大小为20m的数组,这样heap就不够大了,从而系统自动gc。如果依旧不够,会把已有的软引用关联的对象都采取掉。      System.out.println("创建另一个大小为20m的数组otherArray");      byte[] otherArray = new byte;      System.out.println("otherArray的地点:" + otherArray);      // gc后,软引用对象还在,但是通过软引用对象创建的对象就被采取了      System.out.println("现在软引用对象oneObjectSr的地点:" + oneObjectSr);      System.out.println("通过oneObjectSr关联的oneObject对象的地点:" + oneObjectSr.get());      System.out.println("现在数组的地点:" + arraySr);      System.out.println("现在arraySr中关联的byte数组的地点:" + arraySr.get());    }}执行代码,可以看到以下输出:
软引用对象oneObjectSr的地址:java.lang.ref.SoftReference@4f8e5cde
通过oneObjectSr关联的oneObject对象的地址:test.niuke.Test1$OneObject@504bae78
数组的地址:java.lang.ref.SoftReference@3b764bce
通过arraySr关联的byte数组的地址:[B@759ebb3d
正常gc一次之后,oneObject对象并没有回收。地址test.niuke.Test1$OneObject@504bae78
创建另一个大小为20m的数组otherArray
otherArray的地址:[B@484b61fc
现在软引用对象oneObjectSr的地址:java.lang.ref.SoftReference@4f8e5cde
通过oneObjectSr关联的oneObject对象的地址:null
现在数组的地址:java.lang.ref.SoftReference@3b764bce
现在arraySr中关联的byte数组的地址:null弱引用

特点:弱引用通过WeakReference类实现。只要gc就会采取
弱引用的生命周期比软引用短。在垃圾采取器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间充足与否,都会采取它的内存。由于垃圾采取器是一个优先级很低的线程,因此不肯定会很快采取弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)团结使用,如果弱引用所引用的对象被垃圾采取,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
WeakReference<OneObject> oneObjectWr = new WeakReference<>(new OneObject());https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404251647819.gif
只要发生gc,就会采取只存在弱引用的对象。
常用于Threadlocal。
/**
* 弱引用
* 只要gc就会回收
*/
public class ShowWeakReference {
    public static void main(String[] args) {
      // 我们需要通过WeakReference来创建弱引用
      WeakReference<OneObject> objectWr = new WeakReference<>(new OneObject());
      System.out.println("弱引用objectWr的地址:" + objectWr);
      System.out.println("弱引用objectWr关联的oneObject对象的地址:" + objectWr.get());

      System.gc();

      // gc后,弱引用对象还在,但是通过弱引用对象创建的对象就被回收了
      System.out.println("gc后,弱引用objectWr的地址:" + objectWr);
      System.out.println("gc后,弱引用objectWr关联的oneObject对象的地址:" + objectWr.get());
    }
}执行代码,可以看到以下输出:
弱引用objectWr的地址:java.lang.ref.WeakReference@72ea2f77
弱引用objectWr关联的oneObject对象的地址:com.esparks.pandora.learning.references.OneObject@33c7353a
gc后,弱引用objectWr的地址:java.lang.ref.WeakReference@72ea2f77
gc后,弱引用objectWr关联的oneObject对象的地址:null
啊哦~OneObject被回收了虚引用

特点:虚引用也叫幻象引用,通过PhantomReference类来实现。是否采取都找不到引用的对象,仅用于管理直接内存
无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时间都可能被垃圾采取器采取。虚引用必须和引用队列 (ReferenceQueue)团结使用。当垃圾采取器准备采取一个对象时,如果发现它还有虚引用,就会在采取对象的内存之前,把这个虚引用加入到与之关联的引用队列中。 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾采取。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被采取之前采取一些程序行动。
private ReferenceQueue<OneObject> queue = new ReferenceQueue<>();
PhantomReference<OneObject> oneObjectPr = new PhantomReference<>(new OneObject(), queue);无论是否gc,其实都获取不到通过PhantomReference创建的对象。
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404251647823.gif
其仅用于管理直接内存,起到通知的作用。
这里补充一下背景。因为垃圾采取器只能管理JVM内部的内存,无法直接管理系统内存的。对于一些存放在系统内存中的数据,JVM会创建一个引用(类似于指针)指向这部分内存。
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404251647416.gif
当这个引用在采取的时间,就必要通过虚引用来管理指向的系统内存。这里还必要依赖一个队列来实现。当触发gc对一个虚引用对象采取时,会将虚引用放入创建时指定的ReferenceQueue中。之后单独对这个队列举行轮询,并做额外处理。
https://seven97-blog.oss-cn-hangzhou.aliyuncs.com/imgs/202404251647438.gif
/** * 虚引用 * 只用于管理直接内存,起到通知的作用 */public class ShowPhantomReference {    /**   * 虚引用必要的队列   */    private static final ReferenceQueue QUEUE = new ReferenceQueue();    public static void main(String[] args) {      // 我们必要通过 PhantomReference来创建虚引用      PhantomReference objectPr = new PhantomReference(new OneObject(), QUEUE);      System.out.println("虚引用objectPr的地点:" + objectPr);      System.out.println("虚引用objectPr关联的oneObject对象的地点:" + objectPr.get());      // 触发gc,然后检查队列中是否有虚引用      while (true) {            System.gc();            Reference
页: [1]
查看完整版本: 一文夯实垃圾网络的理论基础