Java——GC

来源:互联网 发布:子账号在淘宝哪里登陆 编辑:程序博客网 时间:2024/06/05 16:54

垃圾回收机制的意义

C++程序员非常头疼的一个问题就是内存管理,而垃圾回收机制使得Java程序员不用关心内存动态分配和垃圾回收的问题,交由JVM去处理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。

存在内存泄漏的可能原因:

  1. 静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象 Object 也不能被释放,因为他们也将一直被Vector等应用着。

  2. 各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。

  3. 监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

内存溢出和内存泄漏:

内存溢出(out of memory): 是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;

内存泄露 (memory leak): 是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。memory leak会最终会导致out of memory!

哪些对象会被GC

可达性分析法

一个对象在没有任何强引用指向他或该对象通过根节点不可达时需要被垃圾回收器回收。不过要注意的是被判定为不可达的对象不一定就会成为可回收对象,被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

当一个对象通过一系列根对象(比如静态属性引用的常量)都不可达时就会被回收。简而言之,当一个对象的所有引用都为null。循环依赖不算做引用,如果对象A有一个指向对象B的引用,对象B也有一个指向对象A的引用,除此之外,它们没有其他引用,那么对象A和对象B都、需要被回收。

java中可作为 GC Root 的对象有:

  1. 虚拟机栈中引用的对象(本地变量表)
      
  2. 方法区中静态属性引用的对象
      
  3. 方法区中常量引用的对象
      
  4. 本地方法栈中引用的对象(Native对象)

堆内存的划分

Java 中对象都在堆上创建,为了GC,堆内存分为三个部分,也可以说三代,分别称为新生代,老年代和永久代。

新生代(Young generation)

其中新生代又进一步分为Eden区,Survivor 1 区和Survivor 2 区(比例一般为 8:1:1)。新创建的对象会分配在Eden区,在经历一次 Minor GC 后会被移到 Survivor 1 区,如果Survivor 1区满了,则将 Eden 和 Survivor 1 区正在使用的对象复制到 Survivor 2 区,然后再通过 Minor GC 清除整理 Eden 和 Survivor 1 中的对象,周而复始,在经历几次 Minor GC 后(默认15次),还存活的对象会被移至老年代。需要注意的是,一些大对象(大对象是指需要大量连续存储空间的对象,比如长字符串或数组)可能会直接存放到老年代。新生代的gc相对老年代会频繁一些。

老年代(Tenured / Old Generation)

老年代内存比新生代也大很多(大概比例是2:1),当老年代内存满时触发 Major GC 即 Full GC,Full GC 发生频率比较低,老年代对象存活时间比较长,存活率标记高。

永久代(Perm Area)

永久代一般用来存储类的元信息、静态文件,如Java类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。

GC算法

复制算法

把内存空间划为两个区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。优点:实现简单,运行高效,克服句柄的开销和解决堆碎片。缺点:会浪费一定的内存。一般新生代采用这种算法。

标记清除算法

分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。该算法的缺点是效率不高并且会产生不连续的内存碎片,一般用于老年代的垃圾回收。

标记整理算法

标记阶段与标记清除算法一样,但后续并不是直接对可回收的对象进行清理,而是让所有存活对象都向一端移动,然后清理。该算法不会造成内存碎片,一般用于老年代的垃圾回收。在基于该算法的收集器的实现中,一般会增加句柄和句柄表。

垃圾收集器

新生代常用的垃圾收集器:Serial、PraNew、Parallel Scavenge
老年代常用的垃圾收集器:Serial Old、Parallel Old、CMS

  1. Serial 收集器:新生代单线程收集器,一种古老的收集器,标记和清理都是单线程,优点是简单高效,缺点必须暂停所有用户线程。

  2. Serial Old 收集器:老年代单线程收集器,Serial收集器的老年代版本,采用的是Mark-Compact(标记整理)算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。

  3. ParNew 收集器:新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。

  4. Parallel Scavenge 收集器:新生代并行收集器,追求高吞吐量,高效利用CPU,能够了达到一个可控的吞吐量,它在回收期间不需要暂停其他用户线程。吞吐量一般为99%,吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。

  5. Parallel Old 收集器:Parallel Scavenge 收集器的老年代版本,并行收集器,吞吐量优先,使用多线程和Mark-Compact算法

  6. CMS(Concurrent Mark Sweep)收集器:高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择

  7. G1:G1收集器是当今比较流行的收集器,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

Full GC和并发垃圾回收

并发垃圾回收器的内存回收过程是与用户线程一起并发执行的。通常情况下,并发垃圾回收器可以在用户线程运行的情况下完成大部分的回收工作,所以应用停顿时间很短。但由于并发垃圾回收时用户线程还在运行,所以会有新的垃圾不断产生。作为担保,如果在老年代内存都被占用之前,如果并发垃圾回收器还没结束工作,那么应用会暂停,在所有用户线程停止的情况下完成回收。这种情况称作Full GC,这意味着需要调整有关并发回收的参数了。

由于Full GC很影响应用的性能,要尽量避免或减少。特别是如果对于高容量低延迟的电商系统,要尽量避免在交易时间段发生 Full GC。在对JVM调优的过程中,很大一部分工作就是对于 Full GC 的调节。有如下原因可能导致Full GC:

  1. 年老代(Tenured)被写满

  2. 持久代(Perm)被写满

  3. System.gc()被显示调用

  4. 上一次GC之后Heap的各域分配策略动态变化

总结

  • 在Java中,对象实例都是在堆上创建;一些类信息,常量,静态变量等存储在方法区。堆和方法区都是线程共享的。

  • 在Java中,GC是由一个被称为垃圾回收器的守护线程执行的。

  • 在从内存回收一个对象之前会调用对象的finalize()方法。

  • 作为一个Java开发者不能强制JVM执行GC;GC的触发由JVM依据堆内存的大小来决定。

  • System.gc()和Runtime.gc()会向JVM发送执行GC的请求,但是JVM不保证一定会执行GC。

  • 发生Major GC时用户线程会暂停,会降低系统性能和吞吐量。

  • JVM的参数-Xmx和-Xms用来设置 Java 堆内存的初始大小和最大值。依据个人经验这个值的比例最好是1:1或者1:1.5。比如,你可以将-Xmx和-Xms都设为1GB,或者-Xmx和-Xms设为1.2GB和1.8GB。

  • Java中不能手动触发GC,但可以用不同的引用类来辅助垃圾回收器工作(比如弱引用或软引用)。

扩展

Java内存模型
JVM调优



[参考资料]
Java中的垃圾回收机制
深入理解java垃圾回收机制

1 0
原创粉丝点击