Java教程

Java虚拟机垃圾回收机制

本文主要是介绍Java虚拟机垃圾回收机制,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

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虚拟机垃圾回收机制的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!