JAVA基础知识-JVM的GC算法

  前言

        在学习GC算法之前,先了解GC是什么?说到GC,大家都比较熟悉,垃圾回收(Gabage collectin)嘛; 没错,我觉得在了解GC之前,得先熟悉JVM是怎么分配内存的,然后识别那些内存是需要回收的,最后才是用哪些方式回收。 

        

 内存分配

    Java的内存分配原理与C/C++不同,C/C++每次申请内存时都要malloc进行系统调用,而系统调用发生在内核空间,每次都要中断进行切换,这需要一定的开销,而Java虚拟机是先一次性分配一块较大的空间,然后每次new时都在该空间上进行分配和释放,减少了系统调用的次数,节省了一定的开销,这有点类似于内存池的概念;二是有了这块空间过后,如何进行分配和回收就跟GC机制有关了。

     java一般内存申请分为2种:静态内存和动态内存,很容易理解,编译时就能确定的内存就是静态内存,即内存是固定的,系统一次性分配,比如Int类型变量;动态内存分配就是在程序执行的时候才知道要分配的内存空间大小,比如java对象的内存空间java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。但是java堆和方法区就不一样,我们只有在程序运行期间才会知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般我们所说的垃圾回收也是针对的这一部分。

    总之Stack(栈)的内存管理是顺序分配的,而且定长,不存在内存回收问题;而Heap(堆) 则是为java对象的实例随机分配内存,不定长度,所以存在内存分配和回收的问题;


 垃圾检测、回收算法

    垃圾收集器一般必须完成两件事:检测出垃圾;回收垃圾。怎么检测出垃圾?一般有以下几种方法:

     引用计数法:给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。

    好了,问题来了,如果我有两个对象A和B,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,造成死循环,导致无法回收,所以还有另一种方法:

     可达性分析算法以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集一般包括java栈中引用的对象、方法区常量池中引用的对象,本地方法中引用的对象等。
     总之,JVM在做垃圾回收的时候,会检查堆中的所有对象是否会被这些根集对象引用,不能够被引用的对象就会被垃圾收集器回收。一般回收算法也有如下几种:

1.标记-清除(Mark-sweep)

       算法和名字一样,分为两个阶段:标记和清除。标记所有需要回收的对象,然后统一回收。这是最基础的算法,后续的收集算法都是基于这个算法扩展的。

   不足:效率低;标记清除之后会产生大量碎片。效果图如下:

    

2:复制算法(copying)

   此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。


3.标记-整理(Mark-Compact)

      此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。


4.分代收集算法

    分代收集算法是将对象分为新生代和老年代,然后使用不同的GC策略来进行回收,提高整体的效率。

    分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

    首先:为什么要运用分代垃圾回收策略?

    在java程序运行的过程中,会产生大量的对象,因每个对象所能承担的职责不同所具有的功能不同所以也有着不一样的生命周期,有的对象生命周期较长,比如Http请求中的Session对象,线程,Socket连接等;有的对象生命周期较短,比如String对象,由于其不变类的特性,有的在使用一次后即可回收。试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,那么消耗的时间相对会很长,而且对于存活时间较长的对象进行的扫描工作等都是徒劳。因此就需要引入分治的思想,所谓分治的思想就是因地制宜,将对象进行代的划分,把不同生命周期的对象放在不同的代上使用不同的垃圾回收方式。

   其次:如何划分?

   将对象按其生命周期的不同划分成:年轻代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)。其中持久代主要存放的是类信息,所以与java对象的回收关系不大,与回收息息相关的是年轻代和年老代。

    年轻代:是所有新对象产生的地方。年轻代被分为3个部分——Enden区和两个Survivor区(From和to)当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区(假设为from区)。Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区(假设为to区)。这样在一段时间内,总会有一个空的survivor区。经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。需要注意,Survivor的两个区是对称的,没先后关系,from和to是相对的。

    年老代:在年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到年老代中,可以说他们都是久经沙场而不亡的一代,都是生命周期较长的对象。对于年老代和永久代,就不能再采用像年轻代中那样搬移腾挪的回收算法,因为那些对于这些回收战场上的老兵来说是小儿科。通常会在老年代内存被占满时将会触发Full GC,回收整个堆内存。

    持久代:用于存放静态文件,比如java类、方法等。持久代对垃圾回收没有显著的影响。 


分代回收的效果图如下:



    分代算法综合使用了前面几种算法,年轻代使用复制算法,年老代使用标记-整理算法。

    注意:其中年老代除了使用标记-整理算法外,还会使用CMS收集器。

   CMS采用标记-清除算法,由于标记-清除算法会产生内存碎片,所以JVM提供了参数来使CMS可以在几次清除后作一次整理。

     -XX:CMSFullGCsBeforeCompaction由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。

      -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片。

    应用场景:

     1:对于和用户有交互的应用,比如Web应用,一个重要的考量是系统的响应时间,要保证系统的响应时间就要保证由GC导致的stop the world次数少,或者让用户线程和GC线程一起运行。所以Web应用是使用CMS收集器的一个重要场景。CMS减少了stop the world的次数,不可避免地让整体GC的时间拉长了

     2:对于计算密集型的应用可能会考虑计算的吞吐量,这时候可以使用Parallel Scavenge收集器来保证吞吐量。


基本的命令(JVM和GC相关)

  1:查看JVM启动参数 

1. jps -v

2. jinfo -flags pid

3. jinfo pid -- 列出JVM启动参数和system.properties

4. ps -ef | grep Java

  2:查看当前堆的配置       

      1. jstat -gc pid 1000 3  -- 列出堆的各个区域的大小

      2. jstat -gcutil pid 1000 3 -- 列出堆的各个区域使用的比例

      3. jmap -heap pid  -- 列出当前使用的GC算法,堆的各个区域大小

  3:查看线程的堆栈信息

      1. jstack -l pid 

  4:dump堆内的对象

      1. jmap -dump:live,format=b,file=xxx pid

      2. -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=xxx  -- 设置JVM参数,当JVM OOM时输出堆的dump

      3. ulimit  -c unlimited  -- 设置Linux ulimit参数,可以产生coredump且不受大小限制。之前在线上遇到过一个极其诡异的问题,JVM整个进程突然挂了,这时候依靠JVM本身生成dump文件已经不行了,只有依赖Linux,让系统来生成进程挂掉的core dump文件。

      使用jstack 可以来获得这个coredump的线程堆栈信息:  jstack "$JAVA_HOME/bin/java" core.xxx > core.log

  5:获得当前系统占用CPU最高的10个进程,线程

        ps Hh -eo pid,tid,pcpu,pmem | sort -nk3 |tail > temp.txt

  6:图形化界面
     1. jvisualvm 里面有很多插件,比如Visual GC,可以可视化地看到各个堆区域时候的状态,从而可以对整体GC的性能有整体的认识。



总结

      一个优秀的Java程序员必须了解GC的工作原理、GC基本的算法、如何优化GC的性能、如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统、实时系统等,只有全面提升内存的管理效率 ,才能提高整个应用程序的性能。

      毕竟我们的目标不是代码的搬运工,而是开发攻城狮!





  


猜你喜欢

转载自blog.csdn.net/champion2009/article/details/70199609