对GC自动回收机制的理解

参考文章:https://www.cnblogs.com/wjtaigwh/p/6635484.html,在他的基础上加入自己理解的一些东西

GC的简单了解

        GC:(Garbage Collection),是垃圾回收器,释放垃圾占用的内存。让创建的对象不需要像c、c++那样delete、free掉。对于c、c++开发来说,内存是开发人员分配的,也就是说还要对内存进行维护和释放。对于Java程序员来说,一个对象的内存分配是在虚拟机的自动内存分配机制的帮助下,不再需要为每一个new操作去写配对的delete/free代码,而且不容易出现内存泄漏和内存溢出的问题。但是,如果出现了内存泄漏和内存溢出的问题,而开发者又不了解怎么分配内存的话,那么定位错误和排除错误将是一件很困难的事情。

下面是JVM的内存管理结构:

或者是这种:

1.程序计数器(Program Counter Register)程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。

        每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。

        如果程序计数器执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。

 2.虚拟机栈(JVM Stack):一个线程的每个方法在执行的同事时,都会创建一个栈帧(Stack Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。

局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机,一个Slot就是32个bit),其他都是1个Slot。需要注意的是局部变量表在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。

虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StackOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。

每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。

3、本地方法栈(Native Method Stack):本地方法栈在作用、运行机制、异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。

本地方法 栈也是线程私有的。

4、堆区(Heap):堆区是理解Java GC最重要的区域没有之一。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存。

一般,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以说固定大小的,也可以是可扩展的,目前主流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space 异常。

5、方法区(Method Area):在Java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实上,方法区并不是堆(Non-Heap);另外,不少人的博客中,将Java GC的分代收集分为3个代:青年代,老年代,永久代,这些作者将方法区定义为“永久代”,这是因为,对于之前的HotSpot Java虚拟机的实现方法中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。不过,除HotSpot之外的多数虚拟机,并不将方法区当做永久代,HotSpot本身,也计划取消永久代。

        方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。

        方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。一般的,方法区上执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(HotSpot),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。

        在方法区上进行垃圾收集,条件苛刻而且相当困难,效果也不令人满意,所以一般不做太多考虑,可以留作以后做进一步深入研究时使用。

        运行常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译器常量外,也可以存储在运行时间产生的常量(比如String类型的intern()方法,作用是String维护了一个常量池,如果调用的字符串“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。

6、直接内存(Direct Memory):直接内存并不是JVM管理的内存,是JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。由于直接内存收到及其内存的限制,所以也可能出现OutOfMemoryError的异常。

JVM将堆分成了两个大区新生代(Young)和老年代(Old),新生代又被进一步划分为Eden和Survivor区,而Servivor由FromSpace和ToSpace组成,也有些人喜欢用Survivor1和Survivor2来代替。这里为什么要将Young划分为Eden,Survivor1,Survivor这三块,给出的解释是:

Young中的98%的对象是朝生夕死,所以将内存分为一块较大的Eden和两块较小的Survivor1、Survivor2,JVM默认分配是8:1:1,每次调用Eden和其中的Survivor1(FromSpace),当发生回收的时候,将Eden和Survivor1(FromSpace)存活的对象复制到Survivor2(ToSpace),然后直接清理掉Eden和Survivor1的空间。

堆结构如下图:

新生代:新创建的对象都是新生代分配内存,Eden内存不足时,触发Minor GC,这时会把存活的对象转移进Survivor区。

老年代:老年代用于存放经过多次Minor GC之后仍然存活的对象。

新生代的GC(Minor GC):新生代通常存活时间较短基于Copying算法进行回收,所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和FromSpace或ToSpace之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从Eden到Survivor,最后到老年代。

老年代的GC(Major GC/Full GC):老年代与新生代不同,老年代对象存活时间比较长、比较稳定,因此采用标记(Mark)算法进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对空出的空间要么进行合并、要么标记下来便于下次进行分配,总之目的就是要减少内存碎片带来的效率损耗。

下面介绍几种垃圾收集算法:

1)      Mark-Sweep(标记-清除)算法:                                                     

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:

从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

2)              Copy(复制)算法

为了解决Mark-Sweep算法的缺陷,Copying算法就被提出来了。它将可用的内存按照容量划分为大小相等的两块,每次使用其中的一块。当这一块的内存用完了,就将还活着的对象复制到另一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:

这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活的对象很多,那么Copying算法的效率将会大大降低。我们的新生代GC算法采用的就是这种算法。

3)           Mark-Compact(标记-整理)算法:

          为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是完成标记之后,它不是直接清理掉可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:

                      

在一般厂商JVM中老年代GC就是使用的这种算法,因为老年代的特点是每次都只回收少量对象。

上面是一些常见的垃圾收集算法,垃圾收集算法是内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。下面有几种创建的垃圾收集器,用户可以根据自己的需求组合出新生代和老年代使用的收集器。下面是常见的划分办法:

新生代GC:串行GC(SerialGC)、并行回收GC(ParallelScavenge)和并行GC(ParNew)

什么时候使用GC

Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC做进一步的尝试,若仍无法满足要求,则JVM将报“out of memory”的错误,Java应用将停止。

猜你喜欢

转载自blog.csdn.net/weixin_38664232/article/details/83421848
今日推荐