ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Java虚拟机垃圾回收机制

2021-09-21 15:00:52  阅读:125  来源: 互联网

标签:Java 对象 引用 虚拟机 回收 GC 垃圾 内存


Java虚拟机垃圾回收机制

Java虚拟机在运行过程中时刻监控虚拟机管理的内存使用情况,当发现内存不够用时或者某个适当时机,就对其所管理的内存中的不再使用的对象进行内存回收,以便有更多内存来支持程序的运行。虚拟机是如何判断内存中的对象是否还存活的呢?

如何判断对象是否存活

在Java堆内存里存放着几乎所有的对象,虚拟机在进行垃圾回收之前需要判断哪些对象是存活,哪些是已经死亡的(没有其它对象再引用它)。

引用计数法

给对象添加一个引用计数器,当有对象引用它时计数器加1,当某对象不再引用它时计数器减1.

这种方法虽然可以处理一些问题,但是当两个对象出现彼此引用的情况,而又没有其它对象对它们进行引用,实际上这两个对象在程序逻辑中已经没有使用到它们的地方了,已经可以判定为死亡了,但是引用计数不为0,因而会引至虚拟机无法对这两个对象进行内存回收。

引用计数法,有些语言可能在使用,做为其垃圾回收的机制,但是在主流的Java虚拟机中已经没有它的身影了。

可达性分析算法

可达性分析算法的主要思路是,从一系列称为“GC Roots”的对象做为起点,然后从这些起点开始向下搜索,搜索所路过的对象结点路线称其为引用链,当一个对象到GC Roots中的任何一个对象都没有引用链连接,这时就说明这个对象现在是不再使用的。
在这里插入图片描述

可以作为GC Roots的对象包括:

  1. 本地变量:虚拟机栈中的方法栈帧本地变量表中的对象引用变量
  2. 类中定义的静态对象引用变量
  3. 类中定义的常量对象引用变量
  4. 本地方法栈中引用的对象
  5. 方法区中的类对象
  6. 同步锁持有的对象
  7. 各种异常类对象

finalize()方法

虚拟机通过可达性算法标记出没有被引用的对象后,需要对其进行回收,而彻底回收对象内存,虚拟机规定要经过两次GC过程,第一次没有找到它到GC Roots的引用链时,做一次标记,但是这次没不回收对象,而真正的回收是在第二次GC时,第二次时对象就直接被回收了。在第二次标记后,对象的finalize()方法有可能会被调用(说有可能是虚拟机不保证100%调用到)。所以些方法基本上没有什么用的。在程序开发过程中不建议使用这个方法。

Java中的四大引用类型

强引用(Strong Reference)

当对象被强引用时,虚拟机就永远不会回收对象占用的内存,一般通过 new关键字创建的引用都是强引用

软引用(Soft Reference)

一般用软引用引用的对象不会被GC回收,只有当虚拟机即将发生内存溢出时,其引用的对象才会被GC回收

弱引用(Weak Reference)

只要发生GC,弱引用的对象都会被回收掉。

虚引用(Phantom Reference)

虽进都会被回收,在程序开发过程中一般没有什么用处,可能唯一的用处就是用在监控GC是否可能正常工作。

Java代码执行过程优化

解释执行与JIT执行

Java虚拟机执行代码时,按照字节码的顺序一行一行的解释执行,这种执行方式就叫做解释执行

有些代码在执行的过程中会被频繁的调用,如下代码,这样的代码如果还是按照解释执行的方式,执行的效率是相当低的。

for (int i = 0; i < 50000000; i++) {
    // TODO  method call and other something
}

上面这段代码由于要执行5千万次循环,而执行代码段中代码是一样的,这样的代码被称为热点代码,虚拟机遇到这样或与之类似的代码时,会将这段代码编译成与平台相关的机器码,并进行各种层次的优化,以提高执行的效率,而完成这个工作的编译器就叫做JIT(Just In Time Compiler)即时编译器,也就是JIT编译器

虚拟机的热点代码探测机制,虚拟机为每个方法设置了两种计数器,一个是方法调用次数计数器(Invocation Counter),一个叫做回边计数器(Back Edge Counter),这两个计数器都有一个确定的阈值,当计数器的值超过了这个阈值时,就会触发JIT编译。

对象分配过程中的逃逸分析—对象的栈上分配

什么是逃逸?

通过观察方法的动态作用域,来决定方法中有没有发生逃逸。看如下代码:

void amethod() {
    Object obj = new Object();
}

这段代码中的obj对象无论什么时间,只可以在amethod方法内部使用,一旦出了方法的作用域,就没有其它地方可以引用到obj对象,这种方法调用称为没有逃逸的方法调用。

class Observer {
    public void call(Object obj) {
        System.out.printf(obj.toString());
    }
}
void amethod(Observer observer) {
    Object obj = new Object();
    observer.call(obj);
}

上面这段代码在amethod方法内部new出obj后,传递给了observer的call方法,obj的作用域已经不在方法内部,这种形式的方法调用,称为可逃逸方法。假如Observer对象创建线程和amethod方法调用线程是同一个线程,可以称为方法逃逸。假如Observer对象的创建线程和amethod方法调用线程不是同一个线程,这时和逃逸称为线程逃逸。

在程序启动进可能通过配置:-XX:+DoEscapeAnalysis开启逃逸分析,通过配置:-XX:-DoEscapeAnalysis关闭逃逸分析。可以通过这个配置,写一段代码,观察下开启开关闭逃逸分析状态下,热点代码的执行时间。逃逸分析对代码执行性能的影响。

如果逃逸分析的结果是不可逃逸时,并且在热点代码中出现对象分配时,这里对象就可以在栈上进行分配,以提交执行效率。为什么会提高效率,我分析原因,可能是栈上分配的话,不会涉及到GC机制,而在堆上分析大量对象时,非常容易触发内存抖动,影响程序的执行效率,而在栈上就不会出现这种情况。

对象的分配策略

  1. 进行逃逸分析,决定是否进行栈上分配。是的话,真接栈上分配 ,否则进入下面步骤。

  2. 判断是否需要进行本地线程缓冲分配。是的话,进行缓冲分配,不是的话,进行以下步骤。

  3. 大多数情况下,对象会直接分配到年轻代的Eden区,当Eden区没有足够的空间进行对象分配时,会触发一瞻仰Minor GC。

  4. 大对象直接进入老年代,

    这种情况一般会发生在需要连续内存空间的很长的字符串,或者一个非常大的容量的数组时。为什么会这样?一般情况下Eden区的内存对象都是存活时间不长的对象,基本上每次GC都会回收90%以上的内存空间,针对这种情况Eden区有专门的复制算法进行GC回收,它的算法要求Eden区的空间每次回收后都是未使用的空间,而如果大对象的话,如果一次加收的话,下次再需要时还需要再次分配 ,非常耗时;如果不回收的话,就要复制到年轻代from和to区的空间,大量的内存复制会造成效率降低;再有就是它有可能造成,明明还有耒使用空间,而提交触发GC,导致性能下降,所以只能让其放入老年代。

  5. 长期存活对象进入老年代

  6. 对象年龄动态判定,这个策略的意思,是从from和to区将GC分代年龄在1-3的对象占用空间与from和to区的总空间比较,如果超过一半的话,将这些GC分代年龄在1-3的对象移动到老年代。

  7. 空间分配担保;如果年轻代内存空间不够分配,可能要GC后进行分配,在GC之前需要根据年轻代与老年代的剩余空间比较,决定是进行Minor GC,还是Full GC。

GC分代理论

  1. 绝大部分对象的创建都是朝生夕死

  2. 可以熬过多次GC回收的对象就越验证回收

    根据上面两个经验,大部分虚拟机都实现为将第一种情况的对象存入一个区域,第二种情况的对象放入另外一个区域,这两个区域就是年轻代和老年代。

GC分类

根据 分代理论,GC一般分为年轻代回收,老年代回收,和Full GC(同时回收年轻代、老年代、还有方法区等,这类GC非常耗时)

垃圾回收算法

复制算法

将可用内存分为两部分,每次分配只用其中一块,当一块空间用完后,发生GC时,将还存活的对象复制到另一部分空间中,然后对之前的那部分空间进行整体格式化。这样每次进行对象内存分配时,可用空间的的内存都是连续的,分配效率会大大提高,只是这种算法会将可用内存缩小。
在这里插入图片描述
将存活对象移动到另一部分空间中,然后将左半部分整体清理,这个算法由于移动了对象,对象的存储地址发生变化,需先停止用户线程的操作,GC后,再将新地址反馈给用户线程,然后恢复用户线程操作,由于算法效率较高,用户线程停止时间不会太长。

Appel回收算法

由于98%以上的对象都是朝生夕死,所以在年轻中每次存活对象并不多,为了提高空间利用率,Appel算法,将年轻代分为Eden区,Survivor1(from)区,和Survivor2(to)区,空间比例为(8:1:1),这样每次对象都分配到Eden和两个Survivor区其中的一块,另一块保留,每次GC将还存活对象复制到Survivor区保留的一块中。最后清理掉之前使用的Eden区和Survivor区的空间,下次再将之前使用的区域作为保留区,这样往复。即提高了空间利用率,也保留了复制算法的高效性。

标记清除算法

算法需要经过两个阶段:标记–>清除,第一次扫描内存中所有的对象,标记出需要回收的对象,第二次扫描清理被标记需要回收的对象占用的内存。因为需要扫描两遍,所以执行效率上会稍低一点。这个算法不适用于年轻代,因为年轻代中的对象大部分都是朝生夕死的,而这个算法需要标记出需要回收的对象,如果运到年轻代的话,可以会频繁的做大量的标记工作,效率不高。所以这个算法适用于对象不会轻易被回收的老年代。

它的优点是在进行GC时,不需要停止用户线程,因为它整个过程中不会移动用户线程正在使用的对象的内存地址。

缺点是,由于清理对象可能造成不连续的内存空间,形成内存尪碎片,导致再有大对象内存分配的时候,需要进行一次GC,或者有可能造成OOM。
在这里插入图片描述

标记整理算法

首先标记出所有需要回收的对象, 在标记完成后, 后续步骤不是直接对可回收对象进行清理, 而是让所有存活的对象都向一端移动, 然后直接清理掉端边界以外的内存。 标记整理算法虽然没有内存碎片, 但是效率偏低。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-erKMHy9X-1632206556398)(C:\Users\cm\AppData\Roaming\Typora\typora-user-images\image-20210921144042135.png)]

标记整理与标记清除算法的区别主要在于对象的移动。 对象移动不单单会加重系统负担, 同时需要全程暂停用户线程才能进行, 同时所有引用对象的地方都需要更新(直接指针需要调整) 。

的内存。 标记整理算法虽然没有内存碎片, 但是效率偏低。
在这里插入图片描述

标记整理与标记清除算法的区别主要在于对象的移动。 对象移动不单单会加重系统负担, 同时需要全程暂停用户线程才能进行, 同时所有引用对象的地方都需要更新(直接指针需要调整) 。

标签:Java,对象,引用,虚拟机,回收,GC,垃圾,内存
来源: https://blog.csdn.net/kingbojin/article/details/120401528

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有