首页 > 代码库 > Java GC机制
Java GC机制
通常面试如果说会java,这个问题一般必问,如果能从堆内存划分、回答到垃圾回收器、再到GC监控,这样就比较满意了
JVM进行GC的时候会停止应用程序的执行,除了GC线程外,其他线程都处于等待状态,所以GC的优化很多时候就是尽量减少停顿时间。
说到Java垃圾回收,先有必要介绍一下java的内存结构,截图一张:
程序计数器:线程私有。是一块较小的内存。是当前线程所执行的字节码的行号指示器。是java虚拟机规范中唯一没有OOM的区域。
栈:线程私有。生命周期和线程相同。执行每个方法都会创建一个栈,用于存储局部变量和操作数(对象引用)。局部变量所需的内存空间大小在编译期间完成分配。所以栈的大小不会改变。存在两种异常情况:若线程请求大于栈的深度,抛StachOverFlowError。若栈在动态扩展时无法请求足够内存,抛OOM。
Java堆:所有线程共享。虚拟机启动时创建。存放对象实例和数组。所占内存最大。分为新生代(Young区),老年代(Old区)。新生代分Eden区,Servior区。Servior区又分为From space区和To Space区。Eden区和Servior区的内存比为8:1。 当扩展内存大于可用内存,抛OOM。
方法区:所有线程共享。用于存储已被虚拟机加载的类信息、常量、静态变量等数据。又称为非堆(Non – Heap)。方法区又称“永久代”。GC很少在这个区域进行,但不代表不会回收。这个区域回收目标主要是针对常量池的回收和对类型的卸载。当内存申请大于实际可用内存,抛OOM。
本地方法栈:线程私有。与Java栈类似,但是不为Java方法(字节码)服务,而是为本地非Java方法服务。也会抛StackOverflowError和OOM。
java里面的垃圾回收主要针对堆内存和永久代(方法区),注:JDK1.8已经没有永久代这一说了,为什么?见另一篇文章,GC常见问题
java的堆内存是分代的,主要分为新生代(young)、老年代(old)、永久代(perm)
永久代也被称为方法区(method area)。他用来保存类常量及字符串常量。因此,这个区域不是用来永久存储那些从老年代存活下来的对象。这个区域也可能发生GC,并且在这个区域发生的GC为full GC。
新生代又分为Eden和两个Survivor区:
每个空间的顺序如下:
1.每个新建的对象大多数时候都会在Eden区域,非常大的对象直接进入老年代。(比如一个大数组);
2.当Eden区的空间满时(新建对象申请空间失败)则会触发一次GC,将Eden区存活的对象移动到一个survivor区,这里暂且称为survivor0;接下来Eden区域的GC后存活的对象都会移动到survivor0;
3.当survivor0满后,还在存活的对象会移动到survivor1,之后会清空survivor0。这样反复几次后,存活的对象会被移动到old代。
接下来就是old代的GC了,old的GC又叫fullGC,old代的GC触发有很多种情况:
<style>p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Helvetica; color: #454545 } p.p2 { margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px "PingFang SC Semibold"; color: #454545 } p.p3 { margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px "PingFang SC"; color: #454545 } span.s1 { font: 12.0px "PingFang SC" } span.s2 { font: 12.0px Helvetica }</style>1、System.gc()方法的调用
2、老年代空间不足(有可能是新生代太小,导致大部分对象都到了老年代)
3、永久代空间不足
4、统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间
这是一个较为复杂的触发情况,Hotspot为了避免由于新生代对象晋升到旧生代导致旧生代空间不足的现象,在进行Minor GC时,做了一个判断,如果之
前统计所得到的Minor GC晋升到旧生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC。
新生代和老年代的垃圾回收特点不太一样,在新生代中,GC后,能够回收大部分对象,但是在老年代,GC后,大部分对象还是存活的。
所以新生代和老年代GC的算法也不一致,前者使用的是标记-清理的方法,后者使用的标记整理方法。
java常见的垃圾回收算法介绍:
(1).标记-清除算法:
最基础的垃圾收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。
标记-清除算法的缺点有两个:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
(2).复制算法:
将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
复制算法的缺点显而易见,可使用的内存降为原来一半。
(3).标记-整理算法:
标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。
标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。
复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高。
(4).分代收集算法:
根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。
垃圾回收器介绍
1、串行垃圾回收器
串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序。
通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
2、并行垃圾回收器
并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,它也会冻结所有的应用程序线程当执行垃圾回收的时候
3、并发标记扫描垃圾回收器(CMS)
并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
- 当标记的引用对象在tenured区域;
- 在进行垃圾回收的时候,堆内存的数据被并发的改变。
相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。
通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
4、G1垃圾回收器
G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域
通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器
<style>p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px "PingFang SC"; color: #454545 } p.p2 { margin: 0.0px 0.0px 2.0px 0.0px; font: 14.0px "PingFang SC Semibold"; color: #454545 } p.p3 { margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Helvetica; color: #454545 } li.li1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px "PingFang SC"; color: #454545 } span.s1 { font: 14.0px Helvetica } span.s2 { font: 14.0px "PingFang SC" } span.s3 { font: 12.0px "PingFang SC" } span.s4 { font: 12.0px Helvetica } ol.ol1 { list-style-type: decimal } ul.ul1 { list-style-type: disc }</style>
Java GC机制