浅析Java虚拟机详解之概述、对象生存法则

 更新时间:2021年4月2日 15:01  点击:2090

Java与C++之间有一堵由内存分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。

垃圾搜集器与内存分配策略.png

一、概述

Java堆和方法区这两个区域有着很显著的不确定性:

1、一个接口的多个实现类需要的内存可能会不一样,一个方法所执行的不同条件分支所需要的内存也可能不一样
2、只有处于运行期间,我们才能知道程序究竟会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的

垃圾收集器所关注的正是这部分的内存该如何管理

2020072320140535.png

二、对象已死?

1、引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加一;当引用失效时,计数器就减一;任何时刻计数器为零的对象是不可能再被使用的。

引用计数器虽然占用了一些额外的内存空间来进行计数,原理简单,判定效率很高;

为什么主流Java虚拟机没有使用引用计数器来管理内存呢

引用计数法看似简单的算法有很多例外情况要考虑,必须配合大量额外处理才能保证正确的工作,比如单纯的引用计数很难解决对象之间互相循环引用的问题

引用计数器的缺陷

/**
 * @Author: yky
 * @CreateTime: 2020-12-13
 * @Description: 引用计数器的缺陷
 */
public class ReferenceCountingGC {
  public Object instance = null;
  private static final int _1MB = 1024 * 1024;
  /**
   * 这个成员变量唯一作用是占内存
   */
  private byte[] bigSize = new byte[2 * _1MB];
  public static void testGC(){
    ReferenceCountingGC objA = new ReferenceCountingGC();
    ReferenceCountingGC objB = new ReferenceCountingGC();
    objA.instance = objB;
    objB.instance = objA;
    //发生GC,objA、objB能否被回收
    System.gc();
  }
}

运行代码收查看日志信息发现,这两个对象均被回收虚拟机并没有因为这两个相互引用就放弃回收他们---->Java虚拟机并不是通过计数算法来判断对象是否存活的;

2、可达性分析算法

该算法的核心思想:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些结点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连(图论话来说从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的)

image

对象obj5obj6obj7虽然有关联,但是他们到GC roots不可达因此他们会被判定为可回收对象

在 Java 语言中,可作为 GC Roots 的对象包括以下几种

  • 虚拟机栈(栈中的本地变量表)中的引用对象,如各线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等;
  • 方法区中的类静态属性引用的对象,如Java类的引用类型静态变量;
  • 方法区中的常量引用的对象,如字符串常量里的引用;
  • 本地方法栈总JNI(Navicat方法)引用的对象;
  • Java虚拟机内部的引用
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等;
  • 根据用户所选的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入;

无论通过哪种算法判断对象是否存活都和“引用”离不开关系。

1)强引用

是指在程序代码之间普遍存在的引用赋值,Object obj = new Object();这种引用关系。
无论什么情况下,只要强引用关系还在,垃圾收集器就不会回收掉被引用的对象;

2)软引用

用来描述一些还有用,但非必须的对象。只要软引用关联着的对象,在系统将要发生内存溢出前,会把这些对象列进回收范围之中进行第二次回收;如果这次的回收还没有足够的空间,才会抛出内存溢出的异常;

JDK1.2后提供SoftReference类实现软引用:

Soft reference objects, which are cleared at the discretion of the garbage
collector in response to memory demand. Soft references are most often used
to implement memory-sensitive caches.

3)弱引用

弱引用也被用来描那些非必须对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止,当垃圾收集器开始工作,无论当前内存是足够,都会回收掉只被弱引用关联的对象;

JDK1.2后WeakReference类用来实现弱引用:

Weak reference objects, which do not prevent their referents from being

made finalizable, finalized, and then reclaimed. Weak references are most

often used to implement canonicalizing mappings.

4)虚引用

也叫“幽灵引用”、“幻影引用”,最弱的一种引用关系

  • 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用获得一个对象的实例;
  • 为一个对象设置虚引用的唯一目的是为了能在这个对象被收集器回收时收到一个系统通知;

PhantomReference类来实现虚引用:

Phantom reference objects, which are enqueued after the collector determines that their referents may otherwise be reclaimed. Phantom references are most often used to schedule post-mortem cleanup actions.

应用需要读取大量本地图片

如果每次读取图片都从硬盘读取,则会严重影响性能;解决方案:【软引用或者弱引用】

Map<String,SoftReference<BitMap>> imp = new HashMap<String,SoftReference<BitMap>>

4、生存还是死亡?

在进行过可达性分析后的对象也不一定是非死不可的,该对象进行可达性分析后,发现没有与GC Roots相连接的引用链

  • 这个对象就会第一次被标记起来;对对象是否必要执行finalize()方法进行判断(已经被虚拟机调用过finalize()方法或者没有覆盖finalize()方法都认为是没有必要执行该finalize()方法)
  • F-Queue队列中存放该对象,优先级较低的Finalizer线程会去执行它;Gc 会对这个队列里面的对象再进行一次标记,如果在finalize方法中,对象没有自己自救的话,它就会被标记回收
  • finalize方法自救自己的办法是:重新与引用链上面的任何一个对象建立连接;如把自己this赋值给某个类或对象的成员变量

/**
1.对象可以在GC时自救
2.自救的办法只有一次,因为一个finalize方法最多只能被调用一次
**/
public class FinalizeEscapeGC {

  public static FinalizeEscapeGC SAVE_HOOK = null;
  public void isAlive(){
    System.out.println("yes,I am still alive :)");
  }

  @Override
  protected void finalize() throws Throwable {
    super.finalize();
    System.out.println("finalize method executed !");
    FinalizeEscapeGC.SAVE_HOOK = this;
  }

  public static void main(String [] args) throws InterruptedException {
    SAVE_HOOK = new FinalizeEscapeGC();
    //对象第一次成功拯救自己
    SAVE_HOOK = null;
    System.gc();
    //因为finalize优先级很低,所以延迟0.5s以等待它;
    Thread.sleep(500);
    if(SAVE_HOOK != null){
      SAVE_HOOK.isAlive();
    }else{
      System.out.println("no, i am dead :(");
    }

    //下面这段代码再执行一遍,验证对象是不是可以成功
    SAVE_HOOK = null;
    System.gc();
    Thread.sleep(500);
    if(SAVE_HOOK != null){
      SAVE_HOOK.isAlive();
    }else{
      System.out.println("no, i am dead :(");
    }
  }

}

结果如下:

finalize method executed !
yes,I am still alive :)
no, i am dead :(

  • 并不鼓励使用这种办法来拯救对象,它的运行代价高昂,不确定性大,无法保证顺序;
  • finalize方法能做的所有工作,try-finally也可以做的更好,更及时,所以希望忘记这个方法的存在;

5、回收方法区

很多人认为方法区(或者HotSpot虚拟机中的元空间或永久代)是没有垃圾收集行为的,《Java虚拟机规范》中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾收集的“性价比”一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集效率远低于此。

方法区的垃圾收集主要回收两部分:废弃的常量和不再使用的类型;

  • 回收废弃常量与回收Java堆中的对象非常类似。以常量池中字面量的回收为例:

假如一个字符串“Java”已经进入了常量池中,但是当前系统没有任何一个字符串对象的值是“Java”,换句话说是没有任何String对象引用常量池中的“Java”常量,也没有其他地方引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“Java”常量就会被系统清理出常量池。

  • 常量池中的其他类(接口)、方法、字段的符号引用也与此类似。

判定一个常量是否是“废弃常量”比较简单。而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是“无用的类”:

  1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“被允许”,而不是和对象一样,不使用了就必然会回收。在大量使用反射、动态代理、CGLib等bytecode框架的场景,以及动态生成JSP和OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证不会被方法区造成过大的内存压力。

到此这篇关于Java虚拟机详解之概述、对象生存法则的文章就介绍到这了,更多相关Java虚拟机内容请搜索猪先飞以前的文章或继续浏览下面的相关文章希望大家以后多多支持猪先飞!

[!--infotagslink--]

相关文章