performance-optimization
  • Introduction
  • JVM优化篇
    • JVM结构剖析
    • JAVA程序运行原理分析
    • JVM内存模型
    • 详细垃圾回收机制
    • 常见的垃圾回收器有那些
    • JVM性能调优以及配置
      • jstat参数详细配置
      • G1垃圾回收器参数配置
      • JVM性能调优——常用配置
      • JVM性能调优——案例
      • JVM性能调优——理论篇
      • JVM性能调优——实战篇
      • JVM性能优化分析工具
    • ClassLoader详解
    • Tomcat WebappClassLoader 类加载机制源码分析
  • SQL优化篇
    • MySQL优化篇
      • Mysql的联合索引
      • Mysql如何避免回表查询?什么是索引覆盖?
      • 数据库存储的引擎分析
      • 详解索引及优化
        • 索引优缺点
        • 索引种类
        • 不走索引的情况
        • 索引实现分析
        • 高性能前缀索引
      • SQL性能分析
        • 执行计划
        • 慢SQL监控
      • SQL语句分析
        • 业务层面优化
        • 数据库层面优化
        • SQL语句拆分简单sql
      • 理解MYSQL底层索引
      • MySQL性能优化之参数配置
      • MySQL锁机制详解及死锁处理方式
      • MYSQL中update的low_priority
      • InnoDB数据库死锁
      • MySQL中Innodb的聚簇索引和非聚簇索引
      • B+Tree讲解
        • B-/B+树看 MySQL索引结构
        • B+Tree详细讲解
        • MySQL索引背后的数据结构及算法原理
        • 从 MongoDB 及 Mysql 谈B/B+树
      • MySQL索引的数据结构及算法原理
  • WEB容器优化篇
    • Tomcat容器优化篇
      • Tomcat容器内部原理
      • Tomcat可配参数分析
      • Benchmark压力测试
      • Tomcat调优篇实战
    • WEB程序容器结构剖析
Powered by GitBook
On this page
  • 概述
  • JVM将内存划分为
  • JVM分别对新生代和旧生代采用不同的垃圾回收机制
  • 1)新生代的GC:
  • 2)年老代的GC:

Was this helpful?

  1. JVM优化篇

详细垃圾回收机制

PreviousJVM内存模型Next常见的垃圾回收器有那些

Last updated 5 years ago

Was this helpful?

概述

gc即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存。java语言并不要求jvm有gc,也没有规定gc如何工作。不过常用的jvm都有gc,而且大多数gc都使用类似的算法管理内存和执行收集操作。

在充分理解了垃圾收集算法和执行过程后,才能有效的优化它的性能。有些垃圾收集专用于特殊的应用程序。比如,实时应用程序主要是为了避免垃圾收集中断,而大多数OLTP应用程序则注重整体效率。理解了应用程序的工作负荷和jvm支持的垃圾收集算法,便可以进行优化配置垃圾收集器。

垃圾收集的目的在于清除不再使用的对象。gc通过确定对象是否被活动对象引用来确定是否收集该对象。gc首先要判断该对象是否是时候可以收集。两种常用的方法是引用计数和对象引用遍历。

JVM将内存划分为

1)、New(新生代):

年轻代用来存放JVM刚分配的Java对象

2)、Tenured(年老代):

年轻代中经过垃圾回收没有回收掉的对象将被Copy到年老代

3)、永久代(Perm)

永久代存放Class、Method元信息,其大小跟项目的规模、类、方法的量有关,一般设置为128M就足够,设置原则是预留30%的空间

JVM分别对新生代和旧生代采用不同的垃圾回收机制

1)新生代的GC:

新生代通常存活时间较短,因此基于Copying算法来进行回收,所谓Copying算法就是扫描出存活的对象,并复制到一块新的完全未使用的空间中,对应于新生代,就是在Eden和From Space或To Space之间copy。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。当连续分配对象时,对象会逐渐从eden到survivor,最后到旧生代,用java visualVM来查看,能明显观察到新生代满了后,会把对象转移到旧生代,然后清空继续装载,当旧生代也满了后,就会报OutOfMemoryError的异常,如下图所示:

  • Eden用来存放JVM刚分配的对象

  • Survivor1、Survivro2:两个Survivor空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,就会被Copy到Tenured。显然,Survivor只是增加了对象在年轻代中的逗留时间,增加了被垃圾回收的可能性。

在执行机制上JVM提供了串行GC(Serial GC)、并行回收GC(Parallel Scavenge)和并行GC(ParNew)

  • 串行GC

在整个扫描和复制过程采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是非常高的应用上,是client级别默认的GC方式,可以通过-XX:+UseSerialGC来强制指定

  • 并行收集器(吞吐量优先)

在整个扫描和复制过程采用多线程的方式来进行,适用于多CPU、对暂停时间要求较短的应用上,是server级别默认采用的GC方式,可用-XX:+UseParallelGC来强制指定,用-XX:ParallelGCThreads=4来指定线程数

  • 并发收集器(响应时间优先)

与旧生代的并发GC配合使用

2)年老代的GC:

旧生代与新生代不同,对象存活的时间比较长,比较稳定,因此采用标记(Mark)算法来进行回收,所谓标记就是扫描出存活的对象,然后再进行回收未被标记的对象,回收后对用空出的空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。在执行机制上JVM提供了串行GC(Serial MSC)、并行GC(parallel MSC)和并发GC(CMS),具体算法细节还有待进一步深入研究。

以上各种GC机制是需要组合使用的,指定方式由下表所示:

指定方式

新生代GC方式

旧生代GC方式

-XX:+UseSerialGC

串行GC

串行GC

-XX:+UseParallelGC

并行回收GC

并行GC

-XX:+UseConeMarkSweepGC

并行GC

并发GC

-XX:+UseParNewGC

并行GC

串行GC

-XX:+UseParallelOldGC

并行回收GC

并行GC

-XX:+ UseConeMarkSweepGC-XX:+UseParNewGC

串行GC

并发GC

不支持的组合

1、-XX:+UseParNewGC -XX:+UseParallelOldGC2、-XX:+UseParNewGC -XX:+UseSerialGC

  • JVM命令行参数

无论是客户端应用还是服务器端应用,一旦系统运行缓慢并且垃圾回收所占时间过长,你就会希望通过调整堆大小来改善这一点。不过,为了不影响其他也跑在同一个系统中的应用,不应该将堆大小设置的过大。

GC调优是很重要的。找到最佳的分代堆空间是一个迭代的过程[3,10,12]。这里我们假定你已经为你的应用找到了最佳堆大小。那么你可以采用下面的JVM命令来进行设置:

GC命令行选项

描述

-Xms

设置Java堆大小的初始值/最小值。例如:-Xms512m (请注意这里没有”=”).

-Xmx

设置Java堆大小的最大值

-Xmn

设置年轻代对空间的初始值,请注意,年老代堆空间大小是依赖于年轻代堆空间大小的。在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然。此值关系到JVM垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的3/8

-XX:NewSize=1024m

设置年轻代初始值为1024M。

-XX:MaxNewSize=1024m

设置年轻代最大值为1024M

-XX:NewRatio=4

设置年轻代(包括1个Eden和2个Survivor区)与年老代的比值。表示年轻代比年老代为1:4。

-XX:PermSize=<n>[g|m|k]

设置持久代堆空间的初始值和最小值

-XX:MaxPermSize=<n>[g|m|k]

设置持久代堆空间的最大值

-Xss128k

设置较小的线程栈以支持创建更多的线程,支持海量访问,并提升系统性能。

-XX:SurvivorRatio=6

设置年轻代中Eden区与Survivor区的比值。系统默认是8,根据经验设置为6,则2个Survivor区与1个Eden区的比值为2:6,一个Survivor区占整个年轻代的1/8。

-XX:ParallelGCThreads=8

设置并行收集器的线程数,即同时8个线程一起进行垃圾回收。此值一般配置为与CPU数目相等。

-XX:MaxTenuringThreshold=0

设置垃圾最大年龄(在年轻代的存活次数)。如果设置为0的话,则年轻代对象不经过Survivor区直接进入年老代。对于年老代比较多的应用,可以提高效率;如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。根据被海量访问的动态Web应用之特点,其内存要么被缓存起来以减少直接访问DB,要么被快速回收以支持高并发海量请求,因此其内存对象在年轻代存活多次意义不大,可以直接进入年老代,根据实际应用效果,在这里设置此值为0。

-XX:+UseConcMarkSweepGC

设置年老代为并发收集。CMS(ConcMarkSweepGC)收集的目标是尽量减少应用的暂停时间,减少Full GC发生的几率,利用和应用程序线程并发的垃圾回收线程来标记清除年老代内存,适用于应用中存在比较多的长生命周期对象的情况。

  • JVM命令行辅助参数

-XX:-CITime

打印消耗在JIT编译的时间。

-XX:ErrorFile=./hs_err_pid.log

保存错误日志或数据到指定文件中。

-XX:HeapDumpPath=./java_pid.hprof

指定Dump堆内存时的路径。

-XX:-HeapDumpOnOutOfMemoryError

当首次遭遇内存溢出时Dump出此时的堆内存。

-XX:OnError=";"

出现致命ERROR后运行自定义命令。

-XX:OnOutOfMemoryError=";"

当首次遭遇内存溢出时执行自定义命令。

-XX:-PrintClassHistogram

按下 Ctrl+Break 后打印堆内存中类实例的柱状信息,同JDK的 jmap -histo 命令。

-XX:-PrintConcurrentLocks

按下 Ctrl+Break 后打印线程栈中并发锁的相关信息,同JDK的 jstack -l 命令。

-XX:-PrintCompilation

当一个方法被编译时打印相关信息。

-XX:-PrintGC

每次GC时打印相关信息。

-XX:-PrintGCDetails

每次GC时打印详细信息。

-XX:-PrintGCTimeStamps

打印每次GC的时间戳。

-XX:-TraceClassLoading

跟踪类的加载信息。

-XX:-TraceClassLoadingPreorder

跟踪被引用到的所有类的加载信息。

-XX:-TraceClassResolution

跟踪常量池。

-XX:-TraceClassUnloading

跟踪类的卸载信息。

  • 关于参数名称等

标准参数(-),所有JVM都必须支持这些参数的功能,而且向后兼容;例如:

    • -client

      ——设置JVM使用Client模式,特点是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或开发调试;在32位环境下直接运行Java程序默认启用该模式。

    • -server

      ——设置JVM使Server模式,特点是启动速度比较慢,但运行时性能和内存管理效率很高,适用于生产环境。在具有64位能力的JDK环境下默认启用该模式。

  • 非标准参数(-X),默认JVM实现这些参数的功能,但是并不保证所有JVM实现都满足,且不保证向后兼容;

  • 非稳定参数(-XX),此类参数各个JVM实现会有所不同,将来可能会不被支持,需要慎重使用;

  • 算法分析

Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾收集算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。

大多数垃圾回收算法使用了根集(root set)这个概念;所谓根集就是正在执行的Java程序可以访问的引用变量的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。垃圾收集首选需要确定从根开始哪些是可达的和哪些是不可达的,从根集可达的对象都是活动对象,它们不能作为垃圾被回收,这也包括从根集间接可达的对象。而根集通过任意路径不可达的对象符合垃圾收集的条件,应该被回收。下面介绍几个常用的算法。

1、 引用计数法(Reference Counting Collector)

引用计数法是唯一没有使用根集的垃圾回收的法,该算法使用引用计数器来区分存活对象和不再使用的对象。一般来说,堆中的每个对象对应一个引用计数器。当每一次创建一个对象并赋给一个变量时,引用计数器置为1。当对象被赋给任意变量时,引用计数器每次加1当对象出了作用域后(该对象丢弃不再使用),引用计数器减1,一旦引用计数器为0,对象就满足了垃圾收集的条件。

基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜地必须 实时运行的程序。但引用计数器增加了程序执行的开销,因为每次对象赋给新的变量,计数器加1,而每次现有对象出了作用域生,计数器减1。

注意:这种算法的思路是如果某一个对象被别的对象引用,那么就把他们引用计数器加上1,这样当进行垃圾回收时如果判断该引用的数量为0,此时就代表没有进行任何对象对其进行引用,此时就进行回收

2、tracing算法(标记-清除算法)

tracing算法是为了解决引用计数法的问题而提出,它使用了根集的概念。基于tracing算法的垃圾收集器从根集开始扫描,识别出哪些对象可达,哪些对象不可达,并用某种方式标记可达对象,例如对每个可达对象设置一个或多个位。在扫描识别过程中,基于tracing算法的垃圾收集也称为标记和清除(mark-and-sweep)垃圾收集器.

图例:

缺点:1.效率问题 2 .空间问题(标记清除后会产生大量不连续的碎片)

3、compacting(标记-整理)算法(Compacting Collector)

为了解决堆碎片问题,基于tracing的垃圾回收吸收了Compacting算法的思想,在清除的过程中,算法将所有的对象移到堆的一端,堆的另一端就变成了一个相邻的空闲内存区,收集器会对它移动的所有对象的所有引用进行更新,使得这些引用在新的位置能识别原来的对象。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

图例:

4、copying(复制)算法(Coping Collector)

该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成 一个对象 面和多个空闲面,程序从对象面为对象分配空间,当对象满了,基于coping算法的垃圾 收集就从根集中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。

一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。

图例:

5、generation(分代收集)算法(Generational Collector)

stop-and-copy垃圾收集器的一个缺陷是收集器必须复制所有的活动对象,这增加了程序等待时间,这是coping算法低效的原因。在程序设计中有这样的规律:多数对象存在的时间比较短,少数的存在时间比较长。因此,generation算法将堆分成两个或多个,每个子堆作为对象的一代 (generation)。由于多数对象存在的时间比较短,随着程序丢弃不使用的对象,垃圾收集器将从最年轻的子堆中收集这些对象。在分代式的垃圾收集器运行后,上次运行存活下来的对象移到下一最高代的子堆中,由于老一代的子堆不会经常被回收,因而节省了时间。

图例:

6、adaptive算法(Adaptive Collector)

在特定的情况下,一些垃圾收集算法会优于其它算法。基于Adaptive算法的垃圾收集器就是监控当前堆的使用情况,并将选择适当算法的垃圾收集器。

触发GC(Garbage Collector)的条件/垃圾回收动作何时执行?

1、GC在优先级最低的线程中运行,一般在应用程序空闲即没有应用线程在运行时被调用。但下面的条件例外。

2、Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制调用GC线程。若GC一次之后仍不能满足内存分配,JVM会再进行两次GC,若仍无法满足要求,则JVM将报“out of memory”的错误,Java应用将停止。

3、当年轻代内存满时,会引发一次普通GC,该GC仅回收年轻代。需要强调的时,年轻代满是指Eden代满,Survivor满不会引发GC。

4、当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代。

5、当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载。

垃圾回收的两个重要方法

1)、System.gc()方法

使用System.gc()可以不管JVM使用的是哪一种垃圾回收的算法,都可以请求Java的垃圾回收。在命令行中有一个参数-verbosegc可以查看Java使用的堆内存的情况,它的格式如下:java -verbosegc classfile 由于这种方法会影响系统性能,不推荐使用,所以不详诉。

2)、 finalize()方法

在JVM垃圾回收器收集一个对象之前,一般要求程序调用适当的方法释放资源,但在没有明确释放资源的情况下,Java提供了缺省机制来终止该对象心释放资源,这个方法就是finalize()。它的原型为:protected void finalize() throws Throwable 在finalize()方法返回之后,对象消失,垃圾收集开始执行。原型中的throws Throwable表示它可以抛出任何类型的异常。

之所以要使用finalize(),是存在着垃圾回收器不能处理的特殊情况。例如:

1)由于在分配内存的时候可能采用了类似 C语言的做法,而非JAVA的通常new做法。这种情况主要发生在native method中,比如native method调用了C/C++方法malloc()函数系列来分配存储空间,但是除非调用free()函数,否则这些内存空间将不会得到释放,那么这个时候就可能造成内存泄漏。但是由于free()方法是在C/C++中的函数,所以finalize()中可以用本地方法来调用它。以释放这些“特殊”的内存空间。

2)又或者打开的文件资源,这些资源不属于垃圾回收器的回收范围。

减少GC开销的措施

1)、不要显式调用System.gc()。此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。大大的影响系统性能。

2)、尽量减少临时对象的使用。临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。

3)、对象不用时最好显式置为Null。一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。

4)、尽量使用StringBuffer,而不用String来累加字符串。由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

5)、能用基本类型如Int,Long,就不用Integer,Long对象。基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

6)、尽量少用静态对象变量。静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

7)、分散对象创建或删除的时间。集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

8)、调整新生代的大小到最合适

9)、减少使用全局变量和大对象;

10)、设置老年代的大小为最合适

11)、选择合适的GC收集器