GC(Garbage Collection)垃圾收集机制,这也是Java和C/C++之间的主要区别之一。对于一个Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对于内存泄漏和溢出的问题,不需要那么特别注意。
总的来说:GC机制对于JVM中的内存进行标记,确定哪一些内存需要回收。再根据一些回收策略,自动进行回收内存,永不停息的保证JVM中的内存空间。这也就防止了内存泄漏和溢出问题了。
对于了解GC的原理,那么JVM的内存分布肯定是需要了解的。在Java运行的时候,JVM管理的内存区域有以下几块:
区域名称 | 特性 |
---|---|
程序计数器 | 指示当前程序执行到了哪一行,执行JAVA方法时记录正在执行的虚拟机字节码指令地址;执行本地方法时,计数器值为undefined |
虚拟机栈 | 用于执行JAVA方法。栈帧存储局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。程序执行时栈帧入栈;执行完成后栈帧出栈 |
本地方法栈 | 用于执行本地方法,其它和虚拟机栈类似 |
区域名称 | 特性 |
---|---|
Java堆 | 堆区是理解Java GC机制最重要的区域,没有之一。JVM管理内存中,最大的一块。 堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例。 |
方法区 | 方法区是各个线程共享的区域,用于存储已经被虚拟机加载的 类信息 (即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、 final常量 、 静态变量 、编译器即时编译的代码等。 |
GC主要针对堆内存,所以将堆内存进行详细阐述。
堆内存主要分为三块: 新生代(Youn Generation)、老年代(Old Generation)、持久代(Permanent Generation) 。
三代的特点不同,造就了他们使用的GC算法不同:
新生代(Youn Generation):大致分为Eden区和Survivor区,Survivor区又分为大小相同的两部分:FromSpace和ToSpace。新建的对象都是从新生代分配内存,Eden区不足的时候,会把存活的对象转移到Survivor区。当新生代进行垃圾回收时会出发 Minor GC (也称作 Youn GC )。Eden占比80%,两块Survivor占比20%。
新生代的回收,引用Copy算法。回收过程大致如下:
注意:若没有填满,每次MinorGC的时候,给存活对象标记+1,根据–Xx:MaxTenuringThreshold(默认15)。标记大于1的时候,同样移进老年代。
老年代进行垃圾回收的时候,成为MajorGC/FullGC。
发生Minor GC时,虚拟机会检查每次晋升进入老年代的大小是否大于老年代的剩余空间大小,如果大于,则直接触发一次Full GC。
否则,如果小于的话,JVM就查看是否设置了-XX:+HandlePromotionFailure(允许担保失败)。如果允许,则只会进行MinorGC,此时可以容忍内存分配失败;如果不允许,则仍然进行Full GC(这代表着如果设置-XX:+Handle PromotionFailure,则触发MinorGC就会同时触发Full GC,哪怕老年代还有很多内存,所以,最好不要这样做)
注意:FullGC比MinorGC的速度慢10被以上。因为FullGC的时候,用户线程暂停,降低系统性能、吞吐量。
永久代的回收有两种:常量池中的常量,无用的类信息,常量的回收很简单,没有引用了就可以被回收。
对于无用的类进行回收,必须保证3点:
JDK1.8之前,这些数据保存于此。JDK8,将永久代从堆中取出,数据存在本地内存地区(堆外空间)。
通过一系列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为 引用链/Reference Chain , 当一个对象到 GC Roots 没有任何 引用链 相连时, 即该对象不可达, 也就说明此对象是不可用的。
如图:对象5、6、7就是不可达的,需要被回收
当前主流VM垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据对象存活周期的不同将内存划分为几块, 如JVM中的 新生代 、 老年代 、 永久代 . 这样就可以根据各年代特点分别采用最适当的GC算法:
注意:除去按代回收,还有按区回收算法。
分区收集
上面介绍的分代收集算法是将对象的生命周期按长短划分为两个部分, 而分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是 可以控制一次回收多少个小区间 .
在相同条件下, 堆空间越大, 一次GC耗时就越长, 从而产生的停顿也越长. 为了更好地控制GC产生的停顿时间, 将一块大的内存区域分割为多个小块, 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次GC所产生的停顿.
把堆均分成两个大小相同的区域,只使用其中的一个区域,直到该区域消耗完。此时垃圾回收器终端程序的执行,通过遍历把所有活动的对象复制到另一个区域,复制过程中它们是紧挨着布置的,这样也可以达到消除内存碎片的目的。复制结束后程序会继续运行,直到该区域被用完。
但是,这种方法有两个缺陷:
该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清理掉所有被标记的对象。
该算法会有以下两个问题:
1. 效率问题: 标记和清除过程的效率都不高;
2. 空间问题: 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集.
标记清除算法会产生内存碎片问题, 而复制算法需要有额外的内存担保空间, 于是针对老年代的特点, 又有了 标记整理算法 . 标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存。
在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用 复制收集算法 , 为了提升内存利用率, 只使用了其中一个 Survivor 作为轮换备份, 因此当出现大量对象在Minor GC后仍然存活的情况时, 就需要老年代进行分配担保, 让Survivor无法容纳的对象直接进入老年代, 但前提是老年代需要有足够的空间容纳这些存活对象.
但存活对象的大小在实际完成GC前是无法明确知道的。
因此Minor GC前, VM会先 首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小 , 如果条件成立, 则进行Minor GC, 否则进行Full GC(让老年代腾出更多空间)。
然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只好在失败后重新发起一次Full GC(让老年代腾出更多空间).
此文章是站在各位大佬的肩膀上进行总结的,感谢。
http://www.importnew.com/23035.html
https://blog.csdn.net/antony9118/article/details/51375662
https://blog.csdn.net/anjoyandroid/article/details/78609971
http://baijiahao.baidu.com/s?id=1604308216748480477&wfr=spider&for=pc