java栈有没有垃圾回收_Java的垃圾回收机制

JVM内存结构

整体结构

这里先看下面一张图,很好说明了JVM内存结构:

jvm内存结构.png

可以看到jvm内部有着不同的区域,这些区域中有的会产生内存垃圾,有的不会产生,产生垃圾的地方就会需要JVM的内存管理机制来管理内存的释放。针对不同的区域产生的垃圾,GC的回收策略也有所不同,也就是对应执行的算法会不一样,下面简单介绍以下这些结构的基本用途。

PC寄存器(Program Counter Register)

该结构所占内存很小,而且它是在CPU上的,开发人员是没有办法操作PC寄存器的。它主要就是用于JVM在执行字节码指令的时候,标记当前线程执行到的行号,在程序中,字节码解释器工作的时候就是通过改成PC寄存器的数值来选取下一条要执行的指令。记录程序执行的位置很重要,因为程序中会有很多情况:分支、跳转、循环等等,这些情况下,如果没有一个确切的方式来记录位置,程序的执行就会变得杂乱无章。另外还有一点,在多线程环境下,同一时刻,内核中只能有一个线程运行,当线程从失去内核执行到再次获取执行权限,必须要有PC寄存器来保存上次执行的进度,以便于恢复线程继续往下执行。从这里可以猜到,它是每个线程都必须具备的,所以是线程私有的。当然这里说的寄存器存储的指令地址是指程序在执行Java代码的时候,如果程序执行的是本地方法,这时的指令地址是空的。同时这是唯一一个不存在内存异常的区域。

JVM栈

这块就是我们常说的栈区(我们经常粗略的将JVM分为堆区和栈区)。需要明确的是它的生命周期和线程是一致的,它是线程私有的,每个方法被执行的时候,都会产生一个栈帧,用于存储局部变量表、动态链接、操作数、方法出口等信息 。它的局部变量表中一般存储的是一些基本类型数据以及引用数据,方法的执行就是栈帧的出栈入栈的过程。这个内存区域会有两种可能的Java异常:StackOverFlowError和OutOfMemoryError。

本地方法栈

它其实和JVM栈类似,只不过JVM栈服务于Java程序,而本地方法栈服务于Java中本地方法,因为在Java中有许多操作是需要调用操作系统中的Native方法的,这块区域用于记录本地方法的执行情况。

JVM中区域最大的部分。它是Java程序主要的工作空间,同样的,在Java的性能调优上,主要针对的也是这块区域,只有这块区域可以通过一些人为的调控来适应具体的业务场景。堆内存区域也是垃圾回收的主要区域。可通过-Xms和-Xmx来控制程序启动时堆区的初始大小和最大堆内存。不过一般都将其设置为相等的值以避免频繁启动GC导致程序性能下降。

方法区

一般认为方法区是存在于永久代中的,它主要存储的是类的加载信息,常量,静态变量等等。在方法区中有一个运行时常量池,这里面主要用于存放程序在编译过程中产生的字面量和引用。注意:有些常量在编译期间就可以确定,但是有些常量可以在运行时再进行确定,如:String的intern方法。

垃圾回收算法

Java的最大特色就是JVM的自动垃圾回收机制,它解决了C++开发中令人头痛的内存管理问题,有利就有弊,自动垃圾回收带来方便的同时,也带来了一系列问题。理论上:Java程序不会出现内存溢出或者内存泄露的情况,但是在实际应用中,因为垃圾收集的一些缺陷以及开发人员的忽略,就会出现内存泄露或者内存溢出的情况。

提到垃圾回收,就不得不提垃圾收集算法,目前比较知名的垃圾收集算法有:引用计数法、标记-清除法、标记-整理法、复制算法、分代算法。

引用计数法

引用计数法是一个比较经典的算法,它是指在程序中,每当一个对象被引用一次,就会有一个计数器进行加1,当对象失去一个引用的时候,计数器就减1,当在一段时间内,引用计数器中的值一直为0,那么就认为这个对象可以进行回收了。这个算法有它的好处,简单直接,效率高,但是在Java中有一个致命的缺陷:循环引用。当A对象引用了B,同时B中也引用了A,当A和B都没有用的时候,引用计数法是不能将A和B对象占用的内存区域回收的。因为它俩的引用计数器中始终不会为0。针对引用计数法的缺陷,Sun的JVM中采用的是一种“根搜索算法”,如图:

根搜索示例.jpg

可以看到,它给对象都维护了一个GCRoots的根节点,在进行回收之前,会便利整棵树,如果根节点到对应对象之间有路径(即对象与GCRoots之间是可达的),就认为该对象仍然存活,如果根节点与对象之间没有路径,也就是术语中的“不可达”,那就认为对象已经失去了引用是可以被回收的,无论它的下面引用了多少对象。

标记-清除算法

依据根搜索算法,引出了标记-清除算法,它分为标记和清除两个步骤,首先根据算法,标记处所有已经失去引用的对象(即:GC根节点到对象不可达),标记完成之后,清理无用对象所占用的内存区域。这个算法有一个缺陷:就是会造成大量的内存碎片。零零散散的内存碎片过多,如果没有及时整理,后续再有较大对象申请内存的时候,就会导致申请失败,从而出发一次GC操作,进而降低系统性能。

标记-压缩算法

它是标记-清除算法的优化,它首先它会对内存区域中有用的对象进行标记,然后,将仍然存活的对象整理起来,将它们移动到内存的一端,然后将存活的内存区域边界外的区域全部回收。可以看到,它很好地解决了内存碎片的问题,这实际上算是一种压缩(将原本零散的对象压缩为整齐排列的对象),但是它的开销比较大,耗时较长,适用于持久代。

复制算法

主要用于新生代中,新生代的堆内存结构分为两大块,Eden区和Survivor区,其中Survivor区分成两个大小相等且可以角色互换的两个区域s0区和s1区,复制算法的工作原理就是,当需要对新生代进行垃圾清理的时候,会首先对新生代内部进行“打扫”,将存活的对象复制到某一幸存区中,这里假设复制到s1区,然后进行“战场”清理,将s0区和Eden区直接释放,从而达到新生代的垃圾回收工作。此时需要注意:若在复制阶段,一块幸存区接纳不了所有复制对象,则有些对象直接进入老年代中(也就是所说的“晋升”)。

分代收集算法

它并不是一个新的算法,只是综合了以上算法的情况,在JVM进行垃圾回收时,针对不同区域使用不同的回收算法。所以它的核心就是分治,它会对内存进行区域划分,每个区域中存放不同年龄的对象,然后根据区域中对象的特点(是老年代还是新生代),灵活采用适合该区域的回收算法。

垃圾收集器

基于不同的回收算法,随之就出现了很多垃圾收集器。需要明确的是:

垃圾收集器有不同种类,有的可能同时作用于老年代和新生代,而有的时候,需要为新生代和老年代选择不同的垃圾收集器。新生代的收集频率比较高,所以对效率要求比较高;而老年代的回收频率较低,但是对内存空间比较敏感,所以基于复制的算法基本应该避免。

无论采用何种算法进行垃圾收集,应用程序都会出现程序暂停,不同的只是把这个暂停的时间进行不同程度的缩短。

收集的方式有串行收集,也有并行收集。

串行收集器(Serial)

采用单线程方式进行收集,GC工作的时候会使应用程序进入暂停状态:stop-the-world(STW),系统停顿时间的长短决定了收集器的性能优劣。Serial收集器作用与新生代,是基于“复制”的算法。Serial Old主要作用与老年代,基于“标记-整理”算法。

并行收集器(ParNew)

充分利用多处理器的特点来进行垃圾收集,采用多个GC线程进行垃圾收集。同样的它也有STW,不过在STW期间会有多个线程进行垃圾收集,所以STW时间会大大缩短,在HotSpot中具有代表性的并行收集器有:

ParNew收集器:主要作用与新生代,基于“复制”算法。

Parallel Old收集器:主要作用于老年代,基于“标记-整理”算法。

吞吐量优先收集器(Parallel Scanvenge)

在ParNew基础上提供了一组参数,用于配置期望的收集时间或吞吐量。仅作用于新生代。可以通过VM选项控制吞吐量的大致范围:

-XX:MaxGCPauseMillis:期望收集时间上限,控制收集器对程序停顿时间的影响

-XX:GCTimeRatio:期望的GC时间占总时间比较,控制系统吞吐量

-XX:UseAdaptiveSizePolicy:自动分代大小调节策略

吞吐量和停顿时间无法同时达到最优,它们是相悖的,在实际应用中,只能根据具体应用场景,将它们控制在一种合理的范围内。

CMS收集器(Concurrent Mark-Sweep)

并发标记清除收集,它的特点是并发,GC线程工作时可以和应用程序并发执行,以此来降低系统的停顿时间。注意这里虽然说是可以并行,但是在特定的地方还是会有STW发生,只是时间比较短。非常适合于交互响应时间要求比较高的系统,它仅作用于老年代。使用的仍然是基于“标记-清理”算法,这里的标记采用的方式仍然是前面提到的可达性分析,根据对象和GCRoots之间是否有路径可达,从而标记对象是否可用。

CMS收集器的工作分为六个步骤:初始标记、并发标记、并发预清理、重新标记、并发清理和并发重置。注意,这里会发生两次STW,分别是:初始标记和重新标记。前三步都是对内存对象进行初次分析,由于程序是可以并发执行的,所以有可能在此期间会发生其他状况导致预分析阶段的对象关系出现变化,所以第四步对内存中的对象进行重新标记,然后进行对象回收。

这里虽然发生了两次STW,但是这个时间非常短暂,第一次的STW是初始标记,这里的初始标记仅仅标记与GCRoots直接相关联对象,因为GCRoots数量有限,所以标记时间会很短。第二次STW的标记是基于前期的标记基础,仅仅对并发标记阶段遭到破坏的对象引用关系进行修复,所以搜索量也是有限的,时间较为短暂。

G1收集器

该收集器重新定义了堆空间,它对堆空间重新划分,将其划分为一个个区域,这样在收集的时候,不必全部堆空间都要收集,只是对局部范围进行收集,这样带来的好处就是停顿时间变得可以预测。G1的使用官方建议满足以下条件:

实时数据占据超过半数的堆空间

对象分配率活“晋升”的速度变化明显

期望消除较长时间的GC停顿(超过0.5~1秒)

G1的核心就是将堆空间分成一个个Region,它们大小相同。它分六步完成垃圾回收:初始标记、根区域扫描、并发标记、重新标记、清理和复制。注意这里会发生三次STW,分别是:初始标记、重新标记和复制。具体内容这里不做深入探讨,可自行搜索G1收集器相关工作过程。

更多关于垃圾收集内容,可参考《HotSpot实战》第5章。

你可能感兴趣的:(java栈有没有垃圾回收)