重点知识学习(4.3)--[JVM的执行引擎,垃圾回收概述]

Posted 小智RE0

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重点知识学习(4.3)--[JVM的执行引擎,垃圾回收概述]相关的知识,希望对你有一定的参考价值。

1.执行引擎

Java虚拟机的核心部分之一;jvm将字节码加载到内存中;

注意:字节码并不能够直接运行在操作系统之上, 字节码 不是 机器码 ; 字节码仅包含被 JVM 所识别的字节码指令、符号表,以及其他辅助信息.

在前端编译时,将.java文件转换为.class字节码文件;
后端编译时,.class字节码文件转换为机器码;


执行引擎机制:
解释器: 将字节码逐行解释执行;翻译为对应平台的本地机器指令执行

JIT编译器(即时编译器): 将字节码整体编译为机器码执行 .


Java 是半编译半解释型语言特色

逐行解释执行时的效率低;
JIT编译器将字节码翻译为机器码文件时; 针对使用频率较高的热点代码进行编译,缓存起来, 执行效率得到提高;但是编译是需要消耗时间的.

所以jvm启动后,可考虑先通过解释器 解释执行代码 , 之后再使用编译器编译执行.
所以说Java是半编译半解释的语言.


JIT 编译器执行效率高为什么还需要解释器?

程序启动后,解释器可以快速响应,减少了编译的时间;

编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间,但编译
为本地代码后,执行效率高。就需要采用解释器与即时编译器并存的架构来换取
一个平衡点


2.垃圾回收初概述

上一节在学习到堆内存时,就出现了垃圾回收这个名词;

早期的垃圾回收:

在早期的 C/C++时代,垃圾回收基本上是手工进行的;
就需要开发人员手动清除;
使用 new关键字进行内存申请,并使用 delete 关键字进行内存释放.

例如:

MibBridge *pBridge= new cmBaseGroupBridge();
//如果注册失败,使用 Delete 释放该对象所占内存区域
if(pBridge->Register(kDestroy)!=NO ERROR)
delete pBridge;

早期手动垃圾回收的好处是:可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏.


  • 注意,Java不是唯一拥有垃圾回收特性的语言;
  • C++语言没有垃圾收集技术,需要程序员手动的收集;但是Java语言是自动回收机制;
  • 回收的主要区域–>频繁回收年轻代,较少回收老年代,基本不回收永久代(方法区); 注意:栈,本地方法栈,程序计数器没有垃圾回收.

首先,这里要回收的 垃圾是指 : 在运行程序中没有任何引用指向的对象
那么为啥要进行垃圾回收呢? 在内存有限的情况下,如果不及时处理垃圾,其他的新对象没有可用空间;可能导致内存溢出;

除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便 JVM 将整理出的内存分配给新的对象。

例如数组结构: 需要连续空间.

那么自动内存管理有什么好处呢? 无需开发人员手动参与内存的分配与回收,可 降低内存泄漏和 内存溢出的风险 ; 更专注于业务开发, 而且如今的项目中 , 没有 GC就不能保证应用程序的正常进行.

虽然说自动内存管理较为优秀; 但是若仅仅依赖于它;可能就会弱化 Java 开发人员在程序出现内存溢出时定位问题和解决问题的能力;
那么就需要足够了解 JVM 的自动内存分配和内存回收原理;.当需要排查各种内存溢出内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,就必须对这些“自动化”的技术实施必要的监控和调节。

堆空间作为垃圾回收的重点区域;垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收.

内存溢出与内存泄漏

内存溢出:内存可用量不足;
内存泄漏:有些对象已经在程序不被使用,但是垃圾回收机制并不能判定其为垃圾对象,不能将其回收掉,这样的对象越积越多, 长久也会导致内存不够用;
例如: 与数据库连接完之后,需要关闭连接的通道,但是没有关闭.


2.1 垃圾标记阶段算法

注意标记阶段的主要目的:主要是为了判断对象是否存活;而不是清除垃;

在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。

当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。

在标记阶段主要会采用两种方式:引用计数算法 ; 可达性分析算法.

(1)引用计数算法

对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。

实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。

对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1;
当引用失效时,引用计数器就减 1。只要对象 A 的引用计数器的值为 0,即表示 对象 A 不可能再被使用,可进行回收

严重的缺陷:

  • 需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
  • 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
  • 引用计数器有一个严重的问题,即无法处理循环引用的情况。
  • Java 的垃圾回收器中并未使用引用计数算法.

可能会导致循环引用问题. 好几个对象之间相互引用,但是没有其他引用指向他们,此时垃圾回收不能回收他们,但是也没有引用指向. 这就造成了内存泄漏

(2)可达性分析算法(根搜索算法、追踪性垃圾收集)

  • 目前所使用的垃圾标记算法
  • 可达性分析算法同样具备实现简单和执行高效等特点,可以有效地解决循环引用问题防止内存泄漏的发生
  • GCRoots根集合就是一组必须活跃的引用.
  • 以根对象(GCRoots)为起始点,从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
  • 内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
  • 如果目标对象没有任何引用链相连,则是不可达的,表示该对象己经死亡可标记垃圾对象
  • 只有能够被根对象集合直接或者间接连接的对象才是存活对象。

GC Roots 可以是哪些元素?

  • 虚拟机栈中引用的对象:(各个线程被调用的方法中使用到的参数、局部变量).
  • 本地方法栈内 JNI(通常说的本地方法)引用的对象
  • 方法区中类静态属性引用的对象,比如:Java 类的引用类型静态变量
  • 方法区中常量引用的对象,比如:字符串常量池(StringTable)里的引用
  • 所有被同步锁 synchronized 持有的对象
  • Java 虚拟机内部的引用。
  • 基 本 数 据 类 型 对 应 的 Class 对 象 , 常用异常对象:( NullPointerExceptionOutofMemoryError),系统类加载器。

finalization 机制

使用finalize()方法后,对象可能起死回生;

提到finalize()方法;就会想到 final ,finally,与finalize的区别这个经典题目


对象销毁前可回调方法finalize();对象终止(finalization)机制允许开发人员提供对象被销毁之前进行自定义处理逻辑。

  • 垃圾回收此对象之前,总会先调用此对象finalize()方法,一个对象的 finalize()方法只被调用一次
  • finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。
  • 在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。

不要自己显示的去调用finalize()方法,在里面写代码一定要慎重;应该交给垃圾回收机制调用。

  • finalize()方法的执行时间是没有保障的,它完全由 GC 线程决定,极端情况下,若不发生 GC,则 finalize()方法将没有执行机会
  • 一个糟糕的 finalize()会严重影响 GC 的性能。比如 finalize 是个死循环。

虚拟机中的对象一般处于三种可能的状态

  • 可触及的: 从根节点开始,可以到达这个对象 。 (还未被标记为垃圾)
  • 可复活的: 对象的所有引用都被释放,但是对象有可能在 finalize()时复活。 确定为垃圾了,但没有调用finalize()方法.
  • 不可触及的: 对象的 finalize()被调用,并且没有复活,那么就会进入不可触及 状态。不可触及的对象不可能被复活,因为 finalize()只会被调用一次.

执行案例

public class CanReliveObj 

    public static CanReliveObj obj;//类变量,属于 GC Root

    //此方法只能被调用一次
    @Override
    protected void finalize() throws Throwable 
        //super.finalize();
        System.out.println("调用当前类重写的finalize()方法");
        obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
    

    public static void main(String[] args) 
        try 
            obj = new CanReliveObj();
            // 对象第一次成功拯救自己
            obj = null;
            System.gc();//调用垃圾回收器,触发FULL 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();
        
    

结果

1次 gc
调用当前类重写的finalize()方法
obj is still alive
第2次 gc
obj is dead

过程:
判定一个对象 objA 是否可回收,至少要经历两次标记过程:

  • 如果对象 objA 到 GC Roots 没有引用链,则进行第一次标记。
  • 进行筛选,判断此对象是否有必要执行 finalize()方法

(1)若objA没有重写finalize方法;或者 finalize()方法已经被虚拟机调用过了;那么giant对象直接就进入不可触状态;

(2)如果对象 objA 重写了 finalize()方法,且还未执行过,那么 objA 会被插入到 F-Queue 队列中,由一个虚拟机自动创建的、低优先级的 Finalizer 线程触发其 finalize()方法执行。即可触及->可复活态;

(3)finalize()方法是对象逃脱死亡的最后机会,稍后 GC 会对 F-Queue 队列中的对象进行第二次标记。如果 objA 在 finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA 会被移出“即将回收”集合。对象会再次出现没有引用存在的情况。


以上是关于重点知识学习(4.3)--[JVM的执行引擎,垃圾回收概述]的主要内容,如果未能解决你的问题,请参考以下文章

JVM之内存与垃圾回收篇执行引擎

JVM学习--垃圾回收机制

JVM(基础知识,垃圾回收)

JVM 基本结构

JVM整体结构-java栈详解

Java面试问题笔记——JVM