GC原理与调优

GC原理与调优

GC主要就是在JAVA堆中进行的。

Java 的内存管理实际上就是对象的管理,其中包括对象的分配和释放,对于程序员来说,分配对象使用new关键字;释放对象时,只要将对象所有引用赋值为null,让程序不能够再访问到这个对象,我们称该对象为"不可达的".GC将负责回收所有"不可达"对象的内存空间。

GC算法

GC算法.

    1. 拷贝,将所有仍然生存的对象搬到另外一块内存后,整块内存就可回收。这种方法有效率,但需要有一定的空闲内存,拷贝也有开销.
    1. 跟踪收集器,跟踪收集成追踪从根节点开始的对象引用图。基本的追踪算法叫作“标记并清除”,也就是垃圾收集的两个阶段。标记阶段,垃圾收集器遍历引用 数,标记每一个遇到的对象。清除阶段,未被标记的对象被释放。可能在对象本身设置标记,要么就是用一个独立的位图来设置标记。 压缩(可选),垃圾收集同 时要应对碎片整理的任务。标记和清除通常使用两种策略来消除堆碎片:压缩和拷贝,这两种方法都是快速移动对象来减小碎片, 加在一起叫做mark-sweep-compact.
    1. 还有一种引用计数收集器,这种方法时堆中的每个对象都有一个引用计数,在引用赋值时加1,置空或作为基本类型的引用超出生命期(如方法退出而栈回收)时减1,其对多个对象的循环引用无能为力,但引用计数都不为0 ,还有引用数的增减带来额外开销,故已不再使用.
    1. 分代收集器,
      根据程序的统计, 大多数对象生命周期都很短,都很快被释放掉.但也有部分对象生命周期较长, 甚至永久有效. 对于拷贝算法来说,每次收集时,所有的活动对象都要移动来移动去。对于短生命的对象还好说,经常可以就地解决掉,可是对于长生命周期的对象就纯粹是个体力 劳动了,把它挪来挪去除消耗大量的时间,没有产生任何效益。分代收集能直接让长生命周期的对象长时间的呆在一个地方按兵不动。GC 的精力可以更多的花在收集短命对象上。
        这种方法里,堆被分成两个或更多的子堆,每一个堆为一“代”对象服务。最年幼的那一代进行最频繁的垃圾收集。因为多数对象是短命的,只有很小部分的年 幼对象可以在经历第一次收集后还存活。如果一个最年幼的对象经历了好几次垃圾收集后仍是活着的,那这个对象就成为寿命更高的一代,它被转移到另外一个子堆 中去。年龄更高一代的收集没有年轻一代来得频繁。每当对象在所属的年龄代中变得成熟(多次垃圾收集后仍幸存)之后,就可以转移到更高年龄的一代中去。
        分代收集一般在年轻堆中应用于拷贝算法,年老代应用于标记清除算法。不管在哪种情况下,把堆按照对象年龄分组可以提高最基本的垃圾收集的性能。

GC收集器

回收器 概述 年轻代 老年代
串行回收器(serial collector) 客户端模式的默认回收器,所谓的串行,指的就是单线程回收,回收时将会暂停所有应用线程的执行 X serial old回收器标记-清除-合并。标记所有存活对象,从头遍历堆,清除所有死亡对象,最后把存活对象移动到堆的前端,堆的后端就空了
并行回收器 服务器模式的默认回收器,利用多个线程进行垃圾回收,充分利用CPU,回收期间暂停所有应用线程 Parallel Scavenge回收器,关注可控制的吞吐量(吞吐量=代码运行时间/(代码运行时间加垃圾回收时间)。吞吐量越大,垃圾回收时间越短,可以充分利用CPU。但是 parrellel old回收器,多线程,同样采取“标记-清除-合并”。特点是“吞吐量优先”
CMS回收器 停顿时间最短,分为以下步骤:1初始标记;2并发标记;3重新标记;4并发清除。优点是停顿时间短,并发回收,缺点是无法处理浮动垃圾,而且会导致空间碎片产生 X 适用
G1回收器 新技术,将堆内存划分为多个等大的区域,按照每个区域进行回收。工作过程是1初始标记;2并发标记;3最终标记;4筛选回收。特点是并行并发,分代收集,不会导致空间碎片,也可以由编程者自主确定停顿时间上限 适用 适用

针对GC的建议

根据GC的工作原理,我们可以通过一些窍门技巧和方式,让GC运行更加有效率,更加符合应用程序的要求。以下就是一些程序设计的几点建议:

    1. 最基本的建议就是尽早释放无用对象的引用。大多数程序员在使用临时变量的时候,都是让引用变量在退出活动域(scope)后自动设置为null。我们 在使用这种方式时候,必须特别注意一些复杂的对象图,例如数组、队列、树、图等,这些对象之间有相互引用,关系较为复杂。对于这类对象,GC回收它们一般 效率较低。如果程序允许,尽早将不用的引用对象赋为null。这样可以加速GC的工作。
    1. 尽量少用finalize函数。Finalize函数是Java提供给程序员一个释放对象或资源的机会,但是,它会加大GC的工作量,因此尽量少采用finalize方式回收资源。
    1. 注意集合数据类型,包括数组、树、图、链表等数据结构,这些数据结构对GC来说回收更为复杂。另外,注意一些全局的变量,以及静态变量,这些变量往往容易引起悬挂对象(dangling reference),造成内存浪费。
    1. 当程序有一定的等待时间,程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范标准并不保证GC一定会执行,此时使用增量式GC可以缩短Java程序的暂停时间。。
    1. 减少new对象。每次new对象之后,都要开辟新的内存空间。这些对象不被引用之后,还要回收掉。因此,如果最大限度地合理重用对象,或者使用基本数据类型替代对象,都有助于节省内存;
    1. 多使用局部变量,减少使用静态变量。局部变量被创建在栈中,存取速度快。静态变量则是在堆内存;
    1. 避免使用finalize,该方法会给GC增添很大的负担;
    1. 如果是单线程,尽量使用非多线程安全的,因为线程安全来自于同步机制,同步机制会降低性能。例如,单线程程序,能使用HashMap,就不要用HashTable。同理,尽量减少使用synchronized
    1. 用移位符号替代乘除号。eg:a*8应该写作a<<3
    1. 对于经常反复使用的对象使用缓存;
    1. 尽量使用基本类型而不是包装类型,尽量使用一维数组而不是二维数组;
    1. 尽量使用final修饰符,final表示不可修改,访问效率高
    1. 单线程情况下(或者是针对于局部变量),字符串尽量使用StringBuilder,比StringBuffer要快;
    1. String为什么慢?因为String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象。如果不能保证线程安全,尽量使用StringBuffer来连接字符串。这里需要注意的是,StringBuffer的默认缓存容量是16个字符,如果超过16,apend方法调用私有的expandCapacity()方法,来保证足够的缓存容量。因此,如果可以预设StringBuffer的容量,避免append再去扩展容量。如果可以保证线程安全,就是用StringBuilder。

OutOfMemoryError

OutOfMemoryError是内存溢出, 有多种情况会出现内存溢出:

    1. java堆溢出java.lang.OutOfMemoryError: Java heap space.
    1. java永久堆溢出,通常是反射,代理用的较多导致类生成过多,java.lang.OutOfMemoryError: PermGen space.
    1. 本地堆溢出,这可能是由于操作系统无法分配足够的内存,可能是系统已无内存,还可能是java进程内存空间耗尽,这里有点意思,一般32位系统进程只 有4G地址空间,而又因为java实现使用本地堆或内存映射区作为java堆的存储空间,再去除内核映射区,java使用的堆一般只有2G以内,而如果 java堆xmx占的过大,导致jni的本地堆过小,也会生成内存溢出.本地堆可以是jni用new, malloc,也可能是DirectBuffer等实例.
      java.lang.OutOfMemoryError: request bytes for . Out of swap space?
      这时候,如果java堆足够用的话, 减少xmx的值,反而会解决这种问题.
    1. jni方法的溢出.而前者是由jvm检测的本地溢出,而此是在jni方法调用时,无法分配内存.
      java.lang.OutOfMemoryError: (Native method)

GC优化需要考虑的JVM参数

类型 参数 描述
堆内存大小 -Xms 启动JVM时堆内存的大小
-Xmx 堆内存最大限制
新生代空间大小 -XX:NewRatio 新生代和老年代的内存比
-XX:NewSize 新生代内存大小
-XX:SurvivorRatio Eden区和Survivor区的内存比

GC优化时最常用的参数是-Xms,-Xmx和-XX:NewRatio。-Xms和-Xmx参数通常是必须的,所以NewRatio的值将对GC性能产生重要的影响。

有些人可能会问如何设置永久代内存大小,你可以用-XX:PermSize和-XX:MaxPermSize参数来进行设置,但是要记住,只有当出现OutOfMemoryError错误时你才需要去设置永久代内存。

GC调优方法:
jstat使用

你可能感兴趣的:(GC原理与调优)