Java虚拟机垃圾回收机制问题总结

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/smileiam/article/details/80909007

看完了《深入理解Java虚拟机》一书,对于垃圾回收机制也有一定了解,现在总结下,加深下理解。

先说说Java虚拟机的内存模型,知道哪些对象分别存在JVM的哪个区域,垃圾收集器主要负责回收哪块区域:

1. Java虚拟机运行时内存模型




Java虚拟机的内存区域分成五块,其中三个是线程私有的:程序计数器、Java虚拟机栈、本地方法栈;另两个是线程共享的:Java堆、方法区。线程私有的区域等到线程结束时(栈帧出栈时)会自动被释放,空间较易被清理。而线程共享的Java堆和方法区中空间较大且没有线程回收容易,GC垃圾回收主要负责这部分。

(1) PC计数器

线程私有,用于记录当前线程正在执行字节码的地址,如果执行的是native本地方法,PC计数器为空。

(2)Java栈

线程私有,也叫作Java虚拟机栈,用于存储栈帧,栈帧的入栈出栈过程即方法调用到执行结束的过程。栈帧中主要存放方法执行所需的局部变量表(包括局部变量的声明数据类型、对象引用等)、操作数栈、方法出口等信息。

(3)本地方法栈

与Java栈功能类似,只是用于存储native本地方法的相关信息。

(4)Java堆

线程公用,用于存放对象实例,包括数组,也叫GC区,是GC主要工作的区域。也正是如此,由于GC频率过快与效率不高,堆区的可能成为JVM性能瓶颈,于是考虑到性能,堆区不再是对象内存分配的唯一选择。这里就涉及到了对象的逃逸分析与栈上分配。

逃逸分析就是用来分析对象的作用域是否在方法内部,当方法返回了当前类实例对象、方法中为当前类成员变量赋值、方法中引用当前类成员变量的值时就会发生逃逸,依然在堆上分配内存。但当对象的作用域就在方法内时,比如在方法内创建了该类的实例,没有返回、没有引用,则这种情况就直接在Java栈上分配内存,随着栈帧的出栈释放空间,减轻了堆区GC的压力。

(5)方法区

线程公用,存储了每一个Java类的结构信息,比如:字段、各种方法的字节码内容数据、运行时常量池等。方法区也被称为永久带。一般没有显示要求,GC只对方法区中的常量池回收以及类型卸载。

(6)运行时常量池

属于方法区的一部分,类加载器将类的字节码文件加载如JVM中后,会把字节码文件中的常量池表转化为运行时常量池。


Java堆和方法区主要存放各种类型的对象(方法区也存储一些静态变量和全局常等信息),故在使用GC对其进行回收时首先要考虑的就是如何判断一个对象是否应该被回收。

2. 如何判断一个对象是否该被回收

主要通过对象是否还有其他引用或关联使该对象处于存活的状态。判断对象是否存活有两种比较常见方法:

(1)引用计数法

在堆中存储对象时,在对象头维护一个counter计数器,若一个对象增加了一个引用与之相连,则counter++,如一个引用关系失效则counter--,若一个对象的counter变为0,则说明该对象已被废弃,不处于存活状态。

存在的问题:

1)jdk1.2开始增加了多种引用方式,在不同引用情况下,程序应采用不同的操作,只采用一个引用计数法无法准确区分这么多种引用的情况。

2)引用计数无法解决操作中死锁的循环持有。

(2)对象可达性分析算法

通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链(从GC Roots到这个对象不可达)时,则证明此对象是不可用的。它们到GC Roots是不可达的,将会判断为是可回收的对象。

可作为GC Roots的对象:

1)虚拟机栈(栈帧中的本地变量表)中引用的对象;

2)方法区中类静态属性引用的对象;

3)方法区中常量引用的对象;

4)本地方法栈中JNI(即一般说的Native方法)引用的对象;

GC Roots即指对象的引用,对对象的操作是通过引用来实现的,引用是指向堆内存对象的指针,如果当前对象没有引用指向,那该对象无法被操作,被视为垃圾。可达性分析算法主要从对象的引用出发,寻找对象是否存在引用,若不存在进行标识处理,为GC做准备。

3. HotSpot虚拟机如何实现可达性算法

HotSpot需要枚举所有的GC Roots根节点,虚拟机栈空间不大,但方法区的空间很可能有数百M,遍历一次需要很久,而且遍历所有的GC Roots根节点时,需要暂停所有用户线程,因此需要一个此时的“虚所机快照”,若不暂停用户线程,则虚拟机仍处运行态,无法确保正确遍历所有的根节点。

(1)OopMap数据结构

HotSpot实现了一种OopMap的数据结构,它会在类加载完时把对象内什么偏移量是什么类型计算出来,在JIT编译时,也会记录栈和寄存器中哪些位置是引用,当需要遍历根结点时访问所有OopMap即可。

(2)安全点

HotSpot在OopMap帮助下可以快速且准确完成GCRoots枚举,但运行中非常多的指令会导致引用关系变化,且为这些指令都生成OopMap,需要的空间成本太高。故只在特定位置记录OopMap引用关系,这些位置称为安全点。选定标准“是否具有让程序长时间执行的特征”,如方法调用、循环跳转、循环的末尾、异常跳转等,只有具有这些功能的指令才会产生Safepoint.

(3)如何在安全点上停顿

A)抢先式中断

不需要线程主动配合,GC发生时,先中断所有线程,如果发现不在Safepoint上的线程,就恢复让其运行到Safepoint上;

B)主动式中断

在GC发生时,不直接操作线程中断,仅简单设置一个标志,让各线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起;

4. 判断一个对象生存还是死亡

要真正宣告一个对象死亡,至少需要经历两次标记过程:

(1)第一次标记

在可达性分析后发现到GC Roots没有任何引用链相连时,被第一次标记,进行一次筛选,此对象是否有必要执行finalize()方法

没有必要执行情况:对象没有覆盖finalize()方法;finalize()方法已被JVM调用过一次;这两种情况可认为对象已死,可以回收;

有必要执行:对有必要执行finalize()方法的对象被放入F-Queue队列中,稍后JVM自动建立、低优先级的Finalizer线程(可能多个线程)中触发此方法;

(2)第二次标记

GC将F-Queue队列中的对象进行第二次小规模标记,finalize()方法是对象逃脱死亡的最后一次机会

A)若对象在finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出“即将回收”的集合;

B)若对象没有,也可认为对象已死,可以回收了。

finalize()方法执行时间不确定,甚至是否被执行也不确定(Java程序不正常退出),且运行代价高昂,无法保证各对象调用顺序。

5. GC触发条件

(1)Minor GC触发条件

当Eden区满时,触发Minor GC

(2)Major GC触发条件

老年代空间不足时,触发Major GC,许多Major GC是由Minor GC触发的。

(3)Full GC触发条件

A)调用System.gc时,系统建议执行FullGC,但不是必然执行

B)老年代空间不足

C)方法区空间不足

D)为避免新生代晋升到老年代失败,当MinorGC后进入老年代的对象占用内存空间平均大小大于老年代的可用内存;

F)年代晋升失败,如由Eden区存活的对象晋升到S区放不下,又尝试晋升到Old区又放不下,会触发FullGC.

6. Minor GC\MajorGC\Full GC区别

HotSpot堆结构


(1)新生代(Young Generation)

绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会变得不可达,所以很多对象创建在新生代,用完被回收。对象从Young Generation区域被回收的过程称为minor GC, 即Minor GC cleans the Young Generation

新生代用来保存那些第一次被创建的对象,它可以被分为三个空间

A)Eden空间,内存被调用的起点

B)Survivor 0\Survivor1  S0àS1,age++

MinorGC回收操作:

YoungGen区空间不足时,会触发MinorGC,这会把存活的对象转移进入Survivor区。采用复制整理算法进行回收,先扫描出存活的对象,并复制到一块新的完全未使用的空间中,对于新生代,就是在Eden和From Space或To Space之间的复制。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。

(2)老年代(Old Generation)

当对象在Survivor区熬过一定次数的Minor GC后,就会晋升到老年代。空间比新生代大,发生在老年代上的GC比新生代少,对象从老年代中回收的过程称为Major GC。

MajorGC对象的回收操作:

由于老年代对象存活时间较长、较稳定,因此它采用标记(Mark)算法来进行回收,先扫描出存活的对象,再进行回收未标记的对象,回收后对空出的空间要么进行合并、要么标记出来便于下次进行分配,总之目的就是要减少内存碎片带来的效率损耗。

(3)永久代(permanent generation)

也称方法区,主要存放.class等文件,类方法、类名、常量池,数组中的引用等。FullGC发生时,永久代也可能会被回收。

(4)MinorGC\MajorGC\FullGC区别

Minor GC是清理年轻代的

Major GC是清理老年代的

Full GC是清理整个堆空间,包括年轻代和永久代

7. 符合什么条件的对象会进入老年代

(1)大对象

大对象是指需要大量连续内存空间的Java对象,典型的大对象包括很长的字符中、数组等,大对象对虚拟机的内存分配来说不是个好消息,尤其是一些朝生夕死的大对象。

(2)长期存活的对象

虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过一次MinorGC后仍存活,且能被Survivor容纳,被移到Survivor区,并将年龄设为1。对象在Survivor区中每熬过一次MinorGC,年龄就增加1,当它年龄增加到一定程度(默认15)时,就会晋升到老年代中。对象晋升老年代的年龄阈值可通过参数-XX:MaxTenuringThreshold来设置。

(3)动态对象年龄判定

为能更好适应不同程度的内存状况,虚拟机并不是永远要求对象必须达到MaxTenuringThreshold才能晋升到老年代,若在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间一半,年龄大于或等于此年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

Java虚拟机的内存区域分成五块,其中三个是线程私有的:程序计数器、Java虚拟机栈、本地方法栈;另两个是线程共享的:Java堆、方法区。线程私有的区域等到线程结束时(栈帧出栈时)会自动被释放,空间较易被清理。而线程共享的Java堆和方法区中空间较大且没有线程回收容易,GC垃圾回收主要负责这部分。

Java堆和方法区主要存放各种类型的对象(方法区也存储一些静态变量和全局常等信息),故在使用GC对其进行回收时首先要考虑的就是如何判断一个对象是否应该被回收。

猜你喜欢

转载自blog.csdn.net/smileiam/article/details/80909007