重点知识学习(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 对 象 , 常用异常对象:(
NullPointerException
、OutofMemoryError
),系统类加载器。
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的执行引擎,垃圾回收概述]的主要内容,如果未能解决你的问题,请参考以下文章