垃圾回收机制(第十二天)

一、垃圾回收机制概述 Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。

内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。
什么是垃圾回收机制 垃圾回收机制不定时的向堆内存清理不可达的对象。
垃圾回收简要过程 这里必须点出一个很重要的误区:不可达的对象并不会马上就会被直接回收,而是至少要经过两次标记的过程。
  1. 第一次被标记过的对象,会检查该对象是否重写了finalize()方法。如果重写了该方法,则将其放入一个F-Query队列中,否则,直接将对象加入“即将回收”集合。在第二次标记之前,F-Query队列中的所有对象会逐个执行finalize()方法,但是不保证该队列中所有对象的finalize()方法都能被执行,这是因为JVM创建一个低优先级的线程去运行此队列中的方法,很可能在没有遍历完之前,就已经被剥夺了运行的权利。那么运行finalize()方法的意义何在呢?这是对象避免自己被清理的最后手段:如果在执行finalize()方法的过程中,使得此对象重新与GC Roots引用链相连,则会在第二次标记过程中将此对象从F-Query队列中清除,避免在这次回收中被清除,恢复成了一个“正常”的对象。但显然这种好事不能无限的发生,对于曾经执行过一次finalize()的对象来说,之后如果再被标记,则不会再执行finalize()方法,只能等待被清除的命运。
  2. 之后,GC将对F-Queue中的对象进行第二次小规模的标记,将队列中重新与GC Roots引用链恢复连接的对象清除出“即将回收”集合。所有此集合中的内容将被回收。
手动GC回收
public class JVMDemo05 { public static void main(String[] args) { JVMDemo05 jvmDemo05 = new JVMDemo05(); //jvmDemo05 = null; System.gc(); } protected void finalize() throws Throwable { System.out.println("gc在回收对象..."); } }

finalize作用 Java技术使用finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
内存溢出和内存泄露 1、内存泄漏memory leak :是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。 对象已经没有被应用程序使用,但是垃圾回收器没办法移除它们,因为还在被引用着。
2、内存溢出 out of memory :指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。
3、两者关系:
  • 内存泄漏的堆积最终会导致内存溢出;
  • 内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误;
  • 内存泄漏:是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之后,把钥匙丢了或者没有将钥匙还回去,那么结果就是这个柜子将无法供给任何人使用,也无法被垃圾回收器回收,因为找不到他的任何信息;
  • 内存溢出:一个盘子用尽各种方法只能装4个果子,你装了5个,结果掉倒地上不能吃了。这就是溢出。比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出。说白了就是我承受不了那么多,那我就报错。
4、内存溢出的原因:
  • 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
  • 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  • 代码中存在死循环或循环产生过多重复的对象实体;
  • 使用的第三方软件中的BUG;
  • 启动参数内存值设定的过小。
5、内存溢出解决方案:
  • 修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加);
  • 检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。
  • 对代码进行走查和分析,找出可能发生内存溢出的位置。
二、垃圾回收机制算法 引用计数法 给对象中添加一个引用计数器,默认是15,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不再被使用的,垃圾收集器将回收该对象使用的内存。
优点:
引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:
无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.而且每次加减非常浪费内存。
复制算法 S0和s1将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:可使用的内存降为原来一半。
新生代使用复制算法
标记清除算法 标记-清除(Mark-Sweep)算法顾名思义,主要就是两个动作,一个是标记,另一个就是清除。标记就是根据特定的算法(如:引用计数算法,可达性分析算法等)标出内存中哪些对象可以回收,哪些对象还要继续用。标记指示回收,那就直接收掉;标记指示对象还能用,那就原地不动留下。
缺点:
1. 标记与清除没有连续性效率低;
2. 清除之后内存会产生大量碎片;
标记-压缩算法 标记压缩法在标记清除基础之上做了优化,把存活的对象压缩到内存一端,而后进行垃圾清理。
老年代使用标记-压缩法
分代收集算法 根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。
对于新生代和老年代来说,新生代回收频率很高,但是每次回收耗时很短,而老年代回收频率较低,但是耗时会相对较长,所以应该尽量减少老年代的GC。
垃圾回收时的停顿现象 垃圾回收的任务是识别和回收垃圾对象进行内存清理,为了让垃圾回收器可以更高效的执行,大部分情况下,会要求系统进如一个停顿的状态。停顿的目的是为了终止所有的应用线程,只有这样的系统才不会有新垃圾的产生。同时停顿保证了系统状态在某一个瞬间的一致性,也有利于更好的标记垃圾对象。因此在垃圾回收时,都会产生应用程序的停顿。
三、垃圾收集器 什么是Java垃圾回收器 Java垃圾回收器是Java虚拟机(JVM)的三个重要模块(另外两个是解释器和多线程机制)之一,为应用程序提供内存的自动分配(Memory Allocation)、自动回收(Garbage Collect)功能,这两个操作都发生在Java堆上(一段内存快)。某一个时点,一个对象如果有一个以上的引用(Rreference)指向它,那么该对象就为活着的(Live),否则死(Dead),视为垃圾,可被垃圾回收器回收再利用。垃圾回收操作需要消耗CPU、线程、时间等资源,所以容易理解的是垃圾回收操作不是实时的发生(对象死亡马上释放),当内存消耗完或者是达到某一个指标(Threshold,使用内存占总内存的比列,比如0.75)时,触发垃圾回收操作。有一个对象死亡的例外,java.lang.Thread类型的对象即使没有引用,只要线程还在运行,就不会被回收。
串行回收器(Serial Collector) 单线程执行回收操作,回收期间暂停所有应用线程的执行,client模式下的默认回收器,通过-XX:+UseSerialGC命令行可选项强制指定。参数可以设置使用新生代串行和老年代串行回收器。
年轻代的回收算法:
把Eden区的存活对象移到To区,To区装不下直接移到年老代,把From区的移到To区,To区装不下直接移到年老代,From区里面年龄很大的升级到年老代。回收结束之后,Eden和From区都为空,此时把From和To的功能互换,From变To,To变From,每一轮回收之前To都是空的。设计的选型为复制。
年老代的回收算法:
年老代的回收分为三个步骤,标记(Mark)、清除(Sweep)、合并(Compact)。标记阶段把所有存活的对象标记出来,清除阶段释放所有死亡的对象,合并阶段把所有活着的对象合并到年老代的前部分,把空闲的片段都留到后面。设计的选型为合并,减少内存的碎片。
并行回收器(ParNew回收器) 并行回收器在串行回收器基础上做了改进,他可以使用多个线程同时进行垃圾回收,对于计算能力强的计算机而言,可以有效的缩短垃圾回收所需的时间。
ParNew回收器是一个工作在新生代的垃圾收集器,他只是简单的将串行回收器多线程化,他的回收策略和算法和串行回收器一样。
使用XX:+UseParNewGC 新生代ParNew回收器,老年代则使用串行回收器
ParNew回收器工作时的线程数量可以使用XX:ParaleiGCThreads参数指定,一般最好和计算机的CPU相当,避免过多的栽程影响性能。
并行回收集器(ParallelGC) 老年代ParallelOldGC回收器也是一种多线程的回收器,和新生代的ParallelGC回收器一样,也是一种关往吞吐量的回收器,他使用了标记压缩算法进行实现。
-XX:+UseParallelOldGC 进行设置
-XX:+ParallelCThread也可以设置垃圾收集时的线程教量。
四、Tomcat配置调优测试
  1. 测试串行吞吐量
    -XX:+PrintGCDetails -Xmx32M -Xms32M -XX:+HeapDumpOnOutOfMemoryError -XX:+UseSerialGC -XX:PermSize=32M

    项目启动GC回收6次,吞吐量390
  2. 扩大最大堆的内存,初始堆不变
    -XX:+PrintGCDetails -Xmx512M –Xms32M -XX:+HeapDumpOnOutOfMemoryError -XX:+UseSerialGC -XX:PermSize=32M

    项目启动GC回收6次,吞吐量445
  3. 调整初始堆
    -XX:+PrintGCDetails -Xmx512M –Xms512M -XX:+HeapDumpOnOutOfMemoryError -XX:+UseSerialGC -XX:PermSize=32M

    项目启动GC回收0次,吞吐量492
  4. 并行回收(UseParNewGC)
    -XX:+PrintGCDetails -Xmx512M –Xms512M -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParNewGC -XX:PermSize=32M -没有设置线程数:默认单线程

    项目启动GC回收0次,吞吐量452
  5. 并行合并回收(UseParallelGC)
    -XX:+PrintGCDetails -Xmx512M -Xms256M -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:ParallelGCThreads=8//8个线程 -XX:PermSize=32M

    【垃圾回收机制(第十二天)】项目启动GC回收0次,吞吐量638
总结:
  1. 初始堆值和最大堆内存内存越大,吞吐量就越高。
  2. 最好使用并行收集器,因为并行收集器速度比串行吞吐量高,速度快。
  3. 设置堆内存新生代的比例和老年代的比例最好为1:2或者1:3。
  4. 减少GC对老年代的回收。

    推荐阅读