java调优及垃圾回收

  1. java中堆、栈的前世今生

    在java的世界里,每个Java应用都会唯一对应一个JVM实例,每个实例唯一对应一个堆(堆(heap)说白了就是一块内存空间)。应用程序在运行中所创建的所有类实例或数组都放在这个堆中,并由应用所有的线程共享。

与C/C++不同,Java中分配堆内存是自动初始化的,而且所有对象的存储空间都是在堆中分配的,不过需要注意的是这个对象的引用却是在栈(也有人叫做堆栈,为了不与堆搞混,我们就叫他栈)中分配。这就是说在建立一个对象时会从两个地方分配内存,在堆中分配的内存建立实际对象,而在栈中分配一个指向这个堆对象的指针(引用)。
到此我们介绍了堆和栈,其他还有一个叫做方法区(method).
这样我们就知道了JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method) .

  1. 那么我们具体来看看堆是由何构成的,请听小生一一道来。
    堆内存 = 年轻代 + 年老代 + 永久代(如下图)
    年轻代 = Eden区 + 两个Survivor区(From和To)
    1.png
    新生代又被分为了eden(伊甸园区)、from survivor、to survivor(不过三者之间的具体比例根据不同的JDK是有差异的,本人当前使用的一个JDK,S1空间大小为0的)。划分的目的是为了更好的管理堆内存中的对象,便于GC算法(复制算法)来进行垃圾回收。Perm不属于堆内存,由虚拟机直接分配,但可以通过-XX:PermSize -XX:MaxPermSize 等参数调整其大小。
    JVM每次只会使用eden和其中一块survivor来为对象服务,所以无论什么时候,都会有一块survivor空间。
    因为讲堆就不得不提到一下垃圾回收,虽然后面还会详细介绍JAVA的垃圾回收机制,还是在这里先透露一点关于垃圾回收,先记住一个概念:新生代和老年代都有自己的垃圾回收机制。
    新生代GC(minor gc)是指发生在新生代的垃圾回收动作,minor gc非常平凡,使用复制算法快速的回收。新生代内存空间是所有JAVA对象出生的地方,对象申请的内存和存放都是在这个地方。
    那么对象是如果熬到老年代的呢?对象如何成年呢?
    大致是这样的,新对象会首先分配在 Eden 中(如果新对象过大,会直接分配在老年代中)。在GC中,Eden 中的对象会被移动到survivor中,直至对象满足一定的年纪(定义为熬过GC的次数),会被移动到老年代。
    因为大多新生对象都会在GC中被收回,所以新生代的GC 使用复制算法。在GC前To 幸存区(survivor)保持清空,对象保存在 Eden 和 From 幸存区(survivor)中,GC运行时,Eden中的幸存对象被复制到 To幸存区(survivor)。From 幸存区(survivor)中的幸存对象,会考虑对象年龄,如果年龄没达到阀值(tenuring threshold),对象会被复制到To 幸存区(survivor)。如果达到阀值对象被复制到老年代。复制阶段完成后,Eden 和From 幸存区中只保存死对象,可以视为清空。如果在复制过程中To 幸存区被填满了,剩余的对象会被复制到老年代中。最后 From 幸存区和 To幸存区会调换下名字,在下次GC时,To 幸存区会成为From 幸存区。 不是很复杂吧,觉得复杂就多读几遍。
    根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不要求),可以是固定大小的,也可以是可扩展的,目前主流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。

  2. 堆参数

    调整JVM堆大小参数将会直接影响性能
     JVM运行时堆的大小
    -Xms堆的最小值
    -Xmx堆空间的最大值
     新生代堆空间大小调整
    -XX:NewSize新生代的最小值, 整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,推荐配置为整个堆的3/8。
    -XX:MaxNewSize新生代的最大值
    -XX:NewRatio设置新生代与老年代在堆空间的大小,例如 -XX:NewRatio=3,指定老年代/新生代为3/1. 老年代占堆大小的 3/4 ,新生代占 1/4
    -XX:SurvivorRatio新生代指定伊甸园区(Eden)与幸存区大小比例,作用于新生代内部区域。例如, -XX:SurvivorRatio=10 表示伊甸园区(Eden)是 幸存区To 大小的10倍(也是幸存区From的10倍).所以,伊甸园区(Eden)占新生代大小的10/12, 幸存区From和幸存区To 每个占新生代的1/12 .注意,两个幸存区永远是一样大的..
     永久代大小调整
    -XX:MaxPermSize
     其他
    -XX:MaxTenuringThreshold,设置将新生代对象转到老年代时需要经过多少次垃圾回收,但是仍然没有被回收。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

  3. 如刚才在堆中所描述,栈是存放函数中定义的一些基本类型的变量和对象的引用变量。引用变量相当于为数组或者对象起的一个别名,或者代号。
    引用变量是普通变量,定义时在栈中分配,在程序运行到作用域外释放。而数组和对象本身在堆中分配,即使程序运行到使用new产生数组和对象的语句所在地代码块之外,数组和对象本身占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候,才变成垃圾,在随后的一个不确定的时间内被垃圾回收器释放掉。栈中的变量指向堆内存中的变量,可以理解成 Java 中的指针。
     每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
     每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
     栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
    栈大小设置可以通过-Xss来实现。
    -Xss128k:设置每个线程的堆栈大小。JDK5.0以前每个线程栈的大小为256K,5.0以后改成了1M。根据应用的线程所需内存大小进行调整。

  4. 方法区:

    方法区也叫静态区,和堆一样,能被所有的线程共享。方法区包含所有的class和static变量。
    方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。

  5. Java垃圾回收机制

    我们已经知道java中所有对象都在堆中产生,如果不及时处理我们就会发现堆中空间很快消耗掉,那么后面新产生的对象就无法在内存中存储了,导致程序无法正常执行下去。为此需要有机制保证堆中能腾出空间给后面的对象,这个机制就是垃圾回收机制,我们可以叫他GC。

  6. Java垃圾回收概况

    Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一。Java开发者不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样胆战心惊。因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。该机制对JVM(Java Virtual Machine)中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证JVM中的内存空间,防止出现内存泄露和溢出问题,一旦出现内存泄露和溢出问题,会快速耗尽内存资源导致进程崩溃。
      Java GC机制主要完成3件事:WHAT?确定哪些内存需要回收,WHEN?确定什么时候需要执行GC,HOW?如何执行GC。Java GC机制几乎可以自动的做绝大多数的事情。然而,如果从事较大型的应用软件开发,就可能会出现过内存优化的需求,这就必定要研究Java GC机制。
      了解ava GC机制,可以帮助排查各种内存溢出或泄露问题,解决性能瓶颈,达到更高的并发量,写出更高效的程序。

  7. 垃圾回收算法

    通过了解堆和栈,我们知道在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。
    一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。实现简单,而且效率较高,但是它无法解决循环引用的问题,因此在Java中并没有采用这种方式(Python采用的是引用计数法)。
    在Java中采取了 可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

  8. 垃圾回收种类

    在Java中,垃圾回收是一个自动的进程可以替代程序员进行内存的分配与回收这些复杂的工作。
    Java有四种类型的垃圾回收器:
     串行垃圾回收器(Serial Garbage Collector)
     并行垃圾回收器(Parallel Garbage Collector)
     并发标记扫描垃圾回收器(CMS Garbage Collector)
     G1垃圾回收器(G1 Garbage Collector)
    每种类型都有自己的优势与劣势。可以通过JVM选择垃圾回收器类型。我们通过向JVM传递参数进行选择。每种类型在很大程度上有所不同并且可以提供完全不同的应用程序性能。理解每种类型的垃圾回收器并且根据应用程序选择进行正确的选择是非常重要的。
    串行垃圾回收器
    串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以不适合服务器环境。它最适合的是简单的命令行程序。通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
    并行垃圾回收器
    并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,它也会冻结所有的应用程序线程当执行垃圾回收的时候
    并发标记扫描垃圾回收器
    并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
     当标记的引用对象在tenured区域;
     在进行垃圾回收的时候,堆内存的数据被并发的改变。
    相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。
    通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
    G1垃圾回收器
    G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域
    通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器
    运行的垃圾回收器类型
    -XX:+UseSerialGC 串行垃圾回收器
    -XX:+UseParallelGC 并行垃圾回收器
    -XX:+UseConcMarkSweepGC 并发标记扫描垃圾回收器
    -XX:ParallelCMSThreads= 并发标记扫描垃圾回收器 =为使用的线程数量
    -XX:+UseG1GC G1垃圾回收器
    老年代GC(major gc)指发生在老年代的垃圾回收动作,所采用是的标记整理算法。
    老年代几乎都是经过survivor过来的(还有一些大块直接分配),不会那么容易“被处理掉”,因此major gc不会像minor gc那样频繁。
    JVM会根据机器的硬件配置对每个内存代选择适合的回收算法,比如,如果机器多于1个核,会对年轻代选择并行算法,关于选择细节请参考JVM调优文档。
    并行算法是用多线程进行垃圾回收,回收期间会暂停程序的执行,而并发算法,也是多线程回收,但期间不停止应用执行。所以,并发算法适用于交互性高的一些程序。经过观察,并发算法会减少年轻代的大小,其实就是使用了一个大的年老代,这反过来跟并行算法相比吞吐量相对较低。

  9. 垃圾回收触发

触发垃圾回收条件如下:

 当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC
 当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代
 当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载
何时会抛出OutOfMemoryException,并不是内存被耗空的时候才抛出
 JVM98%的时间都花费在内存回收
 每次回收的内存小于2%
满足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印Heap Dump。

  1. 垃圾回收常见配置汇总
收集器设置

-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

  1. 调优总结
年轻代大小选择

 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
年老代大小选择
 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。减少年轻代和年老代花费的时间,一般会提高应用的效率
 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
 -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
 -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

  1. 调优原则
调优原则

1、多数的Java应用不需要在服务器上进行GC优化;
2、多数导致GC问题的Java应用,很少因为参数设置错误,而是代码问题;
3、在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);
4、减少创建对象的数量;
5、减少使用全局变量和大对象;
6、GC优化是到最后不得已才采用的手段;
7、在实际使用中,分析GC情况优化代码比优化GC参数要多得多;
8、选择合适的GC收集器;

标签: none

添加新评论