Java垃圾回收机制

时间:2020-12-10 00:02:36

问题?Java垃圾回收机制

1.碎片的整理

都知道java对象,回收后,会形成碎片,然后它是如何回收和整理的呢?

然后接下来的写操作就会变得更加费时,因为查找下一个可用空闲块已不再是一个简单操作。

JVM在创建新对象的,会在连续的区块中分配内存。因此如果碎片已经严重到没有一个空闲块能足够容纳新创建的对象时,内存分配便会报错。

为了避免,这种状况的发生,JVM需要确保碎片化在可控范围内。因此,在垃圾回收的过程中,除了进行标记和删除外,还有一个“内存去碎片化”的过程,最后再将存在的空间紧挨起来,这样不就加快了查找的速度了。


Java垃圾回收机制



分代假设

   对象越多,回收的时间也越长。那么我们能不能在更小的内存区域上进行回收呢?


2.应用中绝大多数的内存分配会分为两大类:
绝大部分的对象很快会变为不可用状态。
还有一些,它们的存活时间通常也不会很长。

为了解决
最终结论:构成了弱分代假设(Weak Generational Hypothesis)。基于这一假设,虚拟机内的内存被分为两类,新生代(Young Generation)及老生代(Old Generation)。后者又被称为年老代(Tenured Generation)。
Java垃圾回收机制
这样我们就解决了,新生代老生代区域内的对象垃圾回收了。
有了各自独立的可清除区域后,这才出现了众多不同的回收算法,正是它们一直以来在持续提升着GC的性能。


但是这个时候新的问题又出现了?不同分代中的对象可能彼此间有引用,在进行分代回收时,它们便为视为是“事实上”的GC根对象(GC roots)。JVM对于这种适中的生命周期对象,感觉也无能为力了。因为这样不好回收垃圾啊!


别怕,JVM有绝招,那就是卡片标记。先理解一下的概念介绍

3.伊甸区(Eden)


新对象被创建时,通常便会被分配到伊甸区。由于通常都会有多个线程在同时分配大量的对象,因为伊甸区又被进一步划分成一个或多个线程本地分配缓冲(Thread Local Allocation Buffer,简称TLAB)。有了这些缓冲区使得JVM中大多数对象的分配都可以在各个线程自己对应的TLAB中完成,从而避免了线程间昂贵的同步开销。
TLAB中无法完成分配(通常是由于没有足够的空间),便会到伊甸区的共享空间中进行分配。如果这里还是没有足够的空间,则会触发一次新生代垃圾回收的过程来释放空间。如果垃圾回收后伊甸区还是没有足够的空间,那么这个对象便会到老生代中去分配。


                         Java垃圾回收机制


这里画一个过程图:


当进行伊甸区的回收时,垃圾回收器会从根对象(也就是引用对象)开始遍历所有的可达对象,并将它们标记存活状态。伊甸区检查可达对象的时候,将引用了老生代的对象标记成为“”对象,

当检查完所有对象,将存活的对象全部复制到一个存活区,于是这个时候的伊甸区可认为是清空了,可分配对象了,这一个过程称为“标记复制”。存活的对象先被标记,随后被复制到存活区(survivor)中。
                     Java垃圾回收机制


了解了JVM怎么处理垃圾的,还要补充一下,存活区(survivor)的概念。
紧挨着伊甸区的是两个存活区,分别是from和to区,其中一个始终都是空的(from),想想为什么?

空的那一个活动区,在下次新对象产生的时候,迎来上一批产生的对象(垃圾回收开始的时候)。最后整个新生代的对象都会被标记复制到to存活区,存活对象会不断地在两个存活区之间来回地复制,直到其中的一些对象被认为是已经成熟。也就是足够成熟到已经成为老对象了,这些老对象就会被提升为老生代空间去,不会再重复复制到另一个活动区去了(在一段时间都需要用到它了),直到它不被引用,被垃圾回收机制回收。


当一个对象的年龄超过了一个特定的年老阈值之后,它便会被提升到老年代中,那么这个时限是多少:

JVM会动态地调整实际的年龄阈值,不过通过指定

-XX:+MaxTenuringThreshold参数可以给该值设置一个上限。

-XX:+MaxTenuringThreshold设置为0则立即触发对象提升,而不会复制到存活区中。

在现代的JVM中,这个值默认会被设置为15个GC周期。在HotSpot虚拟机中这也是该值的上限。


*****如果存活区的大小不足以供应,JVM会提早出发对象提升******



4.老生代

老生代的内存空间的实现则更为复杂。老生代的空间通常都会非常大,里面存放的对象都是不太可能会被回收的。老生代的GC比新生代的GC发生的频率要少得多。由于老生代中的多数对象都被认为是存活的,也就不会存在标记-复制操作了。在GC中,这些对象会被挪到一起以减少碎片。老生代的回收算法通常都是根据不同的理论来构建的,与新生代的回收算法是不同的。

不过都会经历以下几步:

(1)标记可达对象,设置GC根对象可达的所有对象后的标记位
(2)删除不可达对象
(3)整理老生代空间的对象,将存活对象复制到老生代开始的连续空间内。


5.持久代

持久代是JVM8以前才有的空间,主要是针对字符串的,元数据比如类相关数据存放的地方。除此之外,像驻留的字符串(internalized string)也会被存放在持久代中,但是对开发人员来说很麻烦,因为无法预测它究竟需要多少空间的控制。所以到JVM8代之久就没有这个持久代空间了。

评估不到位偏会抛出java.lang.OutOfMemoryError: Permgen space的异常。只要不是真的因为内存泄漏而引起的OutOfMemoryError异常,可以通过增加持久代空间的大小来解决这一问题,比如下例中的把持久代最大空间设置为256MB:

java -XX:MaxPermSize=256m com.mycompany.MyApplication


6.元空间:

因为持久代的空间对于程序员来说是济南控制和预测的,所以JVM8之后,就把持久代去掉了,从而推出了元空间,从此以后什么杂七杂八的东西都放到中来使用了。


7.元空间的大小只受限于Java进程的可用本地内存的大小,他们不会再因为多增加了一个类而引发
java.lang.OutOfMemoryError: Permgen space异常了。虽然看似元空间大小毫无限制了,但这一些并非
是没有代价的,你可能会面临的是频繁的内存交换(swapping)或者是本地内存分配失败。
避免此类情况,可以设置元空间的大小:


         java -XX:MaxMetaspaceSize=256m com.mycompany.MyApplication




新生代GC(Minor GC) vs 老生代GC(Major GC) vs Full GC,其实他们之间差别不大,不过还是来了解



1.新生代垃圾的回收被称作Minor GC,在使用新生代垃圾回收需要注意一些地方,只要JVM无法为新创建的对象分配空间,就肯定会触发新生代GC,比方说Eden区满了。因此对象创建得越频繁,新生代GC肯定也更频繁。只要JVM无法为新创建的对象分配空间,就肯定会触发新生代GC,比方说Eden区满了。因此对象创建得越频繁,新生代GC肯定也更频繁。

一旦内存池满了,它的所有内容就会被拷贝走,指针又将重新归零。因此和经典的标记(Mark),清除(Sweep),整理(Compact)的过程不同的是,Eden区和Survivor区的清理只涉及到标记和拷贝。在它们中是不会出现碎片的。写指针始终在当前使用区的顶部。
在一次新生代GC事件中,通常不涉及到年老代。年老代到年轻代的引用被认为是GC的根对象。而在标记阶段中,从年轻代到年老代的引用则会被忽略掉。
和通常所理解的不一样的是,所有的新生代GC都会触发“stop-the-world”暂停,这会中断应用程序的线程。对绝大多数应用而言,暂停的时间是可以忽略不计的。如果Eden区中的大多数对象都是垃圾对象并且永远不会被拷贝到Survivor区/年老代中的话,这么做是合理的。如果恰好相反的话,那么绝大多数的新生对象都不应该被回收,新生代GC的暂停时间就会变得相对较长了。
现在来看新生代GC还是很清晰的——每一次新生代GC都会对年轻代进行垃圾清除。


老年代GC与Full GC
你会发现关于这两种GC其实并没有明确的定义。JVM规范或者垃圾回收相关的论文中都没有提及。不过从直觉来说,根据新生代GC(Minor GC)清理的是新生代空间的认识来看,不难得出以下推论(这里应当从英文出发来理解,Minor, Major与Full GC,翻译过来的名称已经带有明显的释义了):


Major GC清理的是老年代的空间。
Full GC清理的是整个堆——包括新生代与老年代空间
不幸的是这么理解会有一点复杂与困惑。首先——许多老年代GC其实是由新生代GC触发的,因此在很多情况下两者无法孤立来看待。另一方面——许多现代的垃圾回收器会对老年代进行部分清理,因此,使用“清理”这个术语则显得有点牵强。


那么问题就来了,先别再纠结某次GC到底是老年代GC还是Full GC了,你应该关注的是这次GC是否中断了应用线程还是能够和应用线程并发地执行。