JVM的基本概念与维护调优

33次阅读

共计 13481 个字符,预计需要花费 34 分钟才能阅读完成。

一、概念
数据类型
java 虚拟机中,数据类型可以分为两类:
基本类型
引用类型
基本类型的变量保存原始值,即:他代表的值就是数值本身;而引用类型的变量保存引用值。基本类型包括:byte,short,int,long,float,double,Boolean。
“引用值”代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址位置。引用类型包括:类的类型,接口类型和数组。
堆与栈
栈是运行时单位
堆是存储单位
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储问题,即数据怎么放,放在哪。
在 java 中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息都是跟当前线程相关的信息。包括局部变量、程序运行状态、方法返回值等;而堆只负责存储对象信息。
为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?

从 software 设计角度来看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。分而治之思想。这种隔离,模块化思想在软件设计的方面都有体现。
堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这种共享的收益很多的。一方面这种共享提供了一种有效的数据交互方式(共享内存),另一方面,堆中的共享常量和缓存可以被所有栈访问,节省了空间。
栈因为运行时的需要,比如保存系统运行上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需要记录堆中的一个地址即可。
面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写了处理数据的逻辑。不得不承认,面向对象的设计,确实完美。

堆中存什么?栈中存什么?
堆中存对象
栈中存最基本数据类型和堆中对象的引用
一个对象的大小是不可估计的,或者说是可估计的,或者说是可以动态改变的,但在栈中,一个对象只对应 4byte 的引用。
为什么不把基本类型放堆中呢?
因为其占用的空间一般是 1~8 个字节,需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况,长度固定,因此栈中存储就够了,如果把他存在堆中是没什么意义的。可以说是浪费。基本类型和对象的引用都是在存放在栈中,而且都是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。但是基本类型、对象引用和对象本身就是有所区别的,因为一个栈中的数据一个是堆中的数据。最常见的一个问题就是,java 中参数传递时的问题。
java 中的参数传递时传值呢?还是传引用?

不要试图与 C 进行类比,java 中没有指针的概念
程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象本身。

java 在方法调用传递参数时,因为没有指针,所以它都是进行传值调用。因此,很多书里面都说 java 是进行传值调用,这点没有问题,而且也简化了 C 中的复杂性。
但是传引用的错觉是如何造成的呢?
在运行栈中,基本类型和引用的处理都是一样的,都是传值,所以,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处理跟基本类型是完全不一样的。但是当进入被调用方法时,被传递的这个引用的值,被程序解释到堆中的对象,这个时候才对应到真正的对象,如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即修改的是堆中的数据。所以这个修改是可以保持的了。
对象,是由基本类型组成的。可以把一个对象看做成一棵树,对象的属性如果还是对象,则还是一棵树(即非叶子节点),基本类型则为树的叶子节点。程序参数传递时,被传递的值本身都是不能进行修改的,但是,如果这个值是一个非叶子节点(即一个对象引用),则可以修改这个节点下面的所有内容。
堆和栈中,栈是程序运行最根本的东西。程序运行可以没有堆,但不能没有栈。而堆是为栈进行数据存储服务,说白了堆就是一块共享的内存。不过,正是因为堆和栈的分离的思想,才使得 java 的垃圾回收成为可能。
java 中,栈的大小通过 -Xss 来设置,当栈中存储的数据比较多时,需要适当调大这个值,否则会出现 java.lang.StackOverflowError 异常。常见的出现这个异常的是无法返回的递归,因为此时栈中保存的信息都是方法返回的记录点。
引用类型
对象引用类型分为:
强引用
软引用
弱引用和虚引用

强引用:就是我们一般声明对象是时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对象是否被强引用,如果被强引用,则不会被垃圾回收
软引用:软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空间:如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生 OutOfMemory 时,肯定是没有软引用存在的。
弱引用:弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾回收时,是一定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。
强引用是我们系统一般在使用时都是用的强引用。而“软引用”和“弱引用”比较少见。他们一般被作为缓存使用,而且一般是在内存大小比较受限的情况下做为缓存。因为如果内存足够大的话,可以直接使用强引用作为缓存即可,同时可控性更高。因而,他们常见的是被使用在桌面应用系统的缓存。
二、基本垃圾回收算法
此算法结合了“标记 - 清除”和“复制”两个算法的优点。也是分两阶段:第一阶段从根节点开始标记所有被引用对象。第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记 - 请除”的碎片问题,同时也避免了“复制”算法的空间问题。
按系统线程分
串行收集:串行收集使用单线程处理所有垃圾回收工作,因为无需多线程交互,实现容易而且效率比较高,但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集通过单处理器机器。当然,此收集器也可以用在小数据量(100M 左右)情况下的多处理器机机器上。
并行收集:并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上 CPU 数目越多,越能体现出并行收集器的优势。
并发收集:相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂时停止整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。
三、垃圾回收面临的问题
如何区分垃圾?
上面说到的“引用计数”法,通过统计控制生成对象和删除对象时的引用数来判断。垃圾回收程序收集计数为 0 的对象即可。但是这种方法无法解决循环引用。所以,后来实现的垃圾判断算法中,都是从程序运行的根节点出发,遍历整个对象引用,查找存活的对象。那么在这种方式的实现中,垃圾回收从哪儿开始的呢?从哪儿开始查找哪些对象是正在被当前系统使用的,上面分析的堆和栈的区别,其中栈是真正进行程序执行地方,所以要获取哪些对象正在被使用,则需要从 Java 栈开始。同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。
同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。这样,以栈或寄存器中的引用为起点,我们可以找到堆中的对象,又从这些对象找到对堆中其他对象的引用,这种引用逐步扩展,最终以 null 引用或者基本类型结束,这样就形成了一颗以 Java 栈中引用所对应的对象为根节点的一颗对象树,如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要的对象,不能被垃圾回收。而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。
因此,垃圾回收的起点是一些根对象(java 栈,静态变量,寄存器)。而最简单的 Java 栈就是 Java 程序执行的 main 函数。这种回收方式,也是上面提到的“标记 - 请除”的回收方式。
如何处理碎片?
由于不同 Java 对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间,以及程序运行效率降低。所以,在上面提到的基本垃圾回收算法中,“复制”方式和“标记 - 整理”方式,都可以解决碎片的问题.
如何解决同时存在的对象创建和对象回收问题?
垃圾回收线程是回收内存的,而程序运行线程则是消耗(或分配)内存的,一个回收内存,一个分配内存,从这点看,两者是矛盾的。因此,在现有的垃圾回收方式中,要进行垃圾回收前,一般都需要暂停整个应用(即暂停内存的分配),然后进行垃圾回收,回收完成后再继续应用。这种实现方式是最直接,而且最有效的解决一者矛盾的方式。但是这种方式有一个很明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增大,对应应用暂停的时间也会相应的增大。一些对相应时间要求很高的应用,比如最大暂停时间要求是几百毫秒,那么当堆空间大于几个 G 时,就很有可能超过这个限制,在这种情况下,垃圾收将会成为系统运行的一个瓶颈。为解决这种矛盾,有了并发垃圾回收算法,使用这种算法,垃圾回收线程与程序运行线程同时运行。在这种方式下,解决了暂停的问题,但是因为需要在新生成对象的同时又要回收对象,算法复杂性会大大增加,系统的处理能力也会相应降低,同时,“碎片”问题将会比较难解决。
四、分代垃圾回收详述
为什么要分代?
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。在 Java 程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如 Http 请求中的 Session 对象、线程、Socket 连接,这类对象跟业务直接挂钩,因此生命的周期比较长,但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String 对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次通历,但是他们依旧存在因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
虚拟机中的共划分为三个代:
年轻代(Young Generation)
年老代(Old Generation)
持久代(Permanent Generation)

其中持久代主要存放的是 Java 类的类信息,与垃圾收集要收集的 Java 对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
年轻代:所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区:1 个 Eden 区,2 个 Survivor 区(一般而言)。大部分对象在 Eden 区中生成,当 Eden 区满时,还存活的对象将被复制到 Survivor 区(两个中的一个),当这个 Survivor 区满时,此区的存活对象将被复制到另外一个 Survivor 区,当这个 Survivor 区也满了的时候,从第一个 Survivor 区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor 的两个区是对称的,没先后关系,所以同一个区中可能同时存在从 Eden 复制过来对象,和从前一个 Survivor 复制过来的对象,而复制到年老区的只有从第一个 Survivor 区过来的对象。而且,Survivor 区总有一个是空的。同时,根据程序需要,Survivor 区是可以配置多个的,这样可以增加对象在年轻代中存在的时间,减少被放到老代的可能。
年老代:在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中,因此,可认为年老代中存放的都是一些生命周期较长的对象。
持久代:用于存放静态文件,java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或调用一些 class,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类,持久代大小通过 -XX:MaxPermSize=<N> 进行设置。
什么情况下触发垃圾回收?
由于对像进行了分代处理,因此垃圾回收区域、时间也不一样。GC 有两种类型:
Scavenge GC
Full Gc

Scavenge GC:一般情况下,当新对象生成,并且在 Eden 申请空间失败时,就会触发 Scavenge GC,对 Eden 区域进行 GC,清除非存活对象,并且把尚且存活的对象移动到 Survivor 区,然后整理 Survivor 的两个区。这种方式的 GC 是对年轻代的 Eden 区进行,不会影响到年老代。因为大部分对象都是从 Eden 区开始的,同时 Eden 区不会分配的很大,所以 Eden 区的 GC 会须繁进行。因而,一般在这里需要使用速度快、效率高的算法,使 Eden 去能尽快空闲出来。
Full GC:对整个堆进行整理,包括 Young、Tenured 和 Perm。Full GC 因为需要对整个堆进行回收,所以比 Scavenge GC 要慢,因此应该尽可能减少 Full GC 的次数。在对 JVM 调优的过程中,很大一部分工作就是对于 Full GC 的调节。有如下原因可能导致 Full GC:

年老代(Tenured)被写满
持久代(Perm)被写满
System.gc()被显示调用
上一次 GC 之后 Heap 的个域分配策略动态变化

选择合适的垃圾收集算法
串行收集器:用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。但是,也无法使用多处理器的优势,所以此收集器适合单处理器机器。当然,此收集器也可以用在小数据量(100M 左右)情况下的多处理器机器上。可以使用 -XX:+UseSerialGC 打开。
并行收集器:对年轻代进行井行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用 -XX:+UseParallelGC 打开,并行收集器在 J2SE5.0 第六 6 更新上引入,在 JavaSE6.0 中进行了增强,可以对年老代进行并行收集,如果年老代不使用并发收集的话,默认是使用单线程进行垃圾回收,因此会制约扩展能力。使用 XX:+UseParalelOldGC 打开并行收集器收集年老代,使用 -XX:ParalelGCThreads=<N> 设置并行垃圾回收的线程数。
此收集器可以进行如下配置:最大垃圾回收暂停:指定垃圾回的时的最长暂停时间,通讨 -XX:MaxGCPauseMilis=<N> 指定。<N> 为毫秒,如果指定了此值的话,堆大小和垃圾回收相关参数会进行调整以达到指定值。设定此值可能会减少应用的吞吐量。吞吐量:吞吐量为垃圾回收时间与非垃圾回收时间的比值,通过 -XX:GCTimeRatio=<N> 来设定,公式为 1 /(1+N)。例如,-XX:GCTimeRatio=19 时,表示 5% 的时间用于垃圾回收。默认情况为 99,即 1% 的时间用于垃圾回收。
并发收集器:可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模的应用。使用 -XX:+UseConcMarkSweepGC 打开。
并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器会对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。并发收集器使用处理器短暂的停顿时间。在一个 N 个处理器的系统上,并发收集部分使用 KWN 个可用处理器进行回收,一般情况下 1 <=K<=N/4。在只有一个处理器的主机上使用并发收集器,设置为 incrementalmode 模式也可获得较短的停顿时间。
浮动垃圾:由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在拉圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次拉圾回收周期时才能回收掉,所以,并发收集器一般需要 20% 的预留空间用于这些浮动垃圾。
Concurrent Mode Failure:并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃极回收。
启动并发收集器:因为并发收集在应用运行时进行收集,所以必须保证收集完成之前足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”. 通过设置 XX:CMSInitiatingOccupancyFraction=<N> 指定还有多少剩余堆时开始执行并发收集。
小结
串行处理器:适用情况:数据量比较小(100M 左右):单处理器下并且对响应时间无要求的应用。缺点:只能用于小型应用。
并行处理器:适用情况:“对吞吐量有高要求”,多 CPU、对应用响应时间花装求的中、大型应用。举例:后台处理、科学计算。缺点:垃圾收集过程中应用响应时间可能加长。
并发处理器:适用情况:“对响应时间有高要求”,多 CPU、对应用响应时间有较高要求的中、大型应用。举例:Web 服务器 / 应用服务器、电信交换、集成开发环境。
五、常见配置汇总
堆设置
-Xms:初始堆大。-Xmx:最大堆大小。初始堆大小与最大堆大小尽量小于 4G。两值不相等的话,内存地址中会计算会对应用程序产生影响,索性设为相等。
-XX:NewSize=n:设置年轻代大小。-XX:NewRatio=n:设置年轻代和年老代的比值。如:为 3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代年老代的 1 /4。-XX:SurvivorRatio=n:年轻代中 Eden 区与连个 Survivor 区的比值。注意 Survivor 区有两个。如:3,表示 Eden:Survivor=3:2,一个 Survivor 区占整个年代的 1 /5。-XX:MaxPermSize=n:设置持久代大小。
收集器设置
-XX:+UseSerialGC:设置串行。-XX:+UseParallelGC:设置并行。-XX:+UseParalledlOldGC:设置并行年老代。-XX:+UseConcMarkSweepGC:设置并发。
垃圾回收统计信息
-XX:+PrintGC:输出 GC 日志。-XX:+PrintGCDetails:输出 GC 的详细日志。-XX:+PrintGCTimeStamps:输出 GC 的时间戳(以基准时间的形式)。-Xloggc:filename:日志文件的输出路径。
并发收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集使用的 CPU 数。并行收集线程数。-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间。-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式 1 /(1+n)。-XX:+CMSlncrementalMode:设置为增量模式。适用于单 CPU 情况。-XX:ParallelGCThreads=n:设置并发收集器年期代收集方式,使用的 CPU 数。并行收集线程。
六、典型配置举例
堆大小设置
JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32bit 还是 64bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32 位系统下,一般限制在 1.5G~2G;64 位操作系统对内存无限制。在 Windows Server 2003 系统,3.5G 物理内存,JDK5.0 下测试,最大可设置为 1478m。
典型设置:
java -Xms3550m -Xmx3550m -Xmn2G -Xss128k

-Xmx3550m:设置 JVM 最大可用堆内存。-Xms3550m:设置 JVM 初始堆内存为 3550m。此值可以设置与 -Xmx 相同,以避免每次垃圾回收完成后 JVM 重新分配内存。-Xmn2g:设置年轻代大小为 2G。整个堆大小 = 年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定在 64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun 官方推荐配置整个堆的 3 /8-Xss128k:设置每个线程的堆栈大小。JDK5 以后每个线程堆栈大小为 1M,以前每个线程堆栈大小为 256k。具应用的线程所需内存大小进行调整。在相同的物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,阀值在 3000-5000 左右。
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermmSize=16m -XX:MaxTenuringThreshold=0

-XX:NewRatio=4:设置年轻代 (包括 Eden 和两个 Survivor 区) 与年老代的比值(除去持久刘代)。设置为 4,则年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1 /5。-XX:SurvivorRatio=4:设置年轻代中 Eden 区与 Survivor 区的大小比值。设置为 4,则两个 Survivor 区与一个 Eden 区的比值为 2:4,一个 Survivor 区占整个年轻代的 1 /6。-XX:MaxPermSize=16m:设置持久代大小为 16m。-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入老年代。对于年老代比较多的应用,可以提高效率。如果将此值设为一个较大值,则年轻对象会在 Survivor 区进行多次复制,这样可以增加对象在年轻代的存活时间,增加年轻代即被回收的概论。
回收器选择
JVM 给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0 以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数,JDK5.0 以后,JVM 会根据当前系统配置进行判断。
吞吐量优先的并行收集器
如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。典型配置:
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParalelGC -XX:ParallelGCThreads=20

-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。年轻代使用并行收集,而年老代仍旧使用串行收集。-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParalleloldGC

-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集:JDK6.0 支持对年老代并行收集。
java-Xmx3550m-Xms3550m-Xmn2g-Xss128k-XX:+UseParallelGC -XX:MaxGCPauseMilis=100

-XX:MaxGCPauseMilis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM 会自动调整大小,以满足此值
java -Xmx 3550m -Xms 3550m -Xmn2g -Xss128k -XX:+UseParalleGC -XX:MaxGCPauseMilis=100 -XX:+UseAdaptiveSizePolicy

-XX:+UseParalleGC:设置此项后,并行收集器会自动选择年轻代区大小和相应的 Surivor 区比例,已达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器,一直打开。
响应时间优先的并发收集器
如上文所述,并发收集器主要保证系统的响应时间,减少垃圾收集时间的停顿时间。适用于应用服务器、电信领域等。典型配置:
java-Xmx3550m-Xms3550m-Xmn2g-Xss128k-XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio= 4 的配置失效了,原因不明。所以,此时年轻代大小最好用 -Xmn 设置。XX:+UseParNewGC:设置年轻代为并行收集,可与 CMS 收集同时使用 JDK5.0 以上 JVM 会根据系统配置自行设置,所以无需再设置此值。
java-Xmx3550m-Xms3550m-Xmn2g-Xss128k-XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

-XX:CMSFulGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次 GC 以后对内存空间进行压缩、整理。-XX:CMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片。
七、调优总结
年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择);在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。吞吐量优先的应用:尽可能的设置大,可能到达 Gbit 的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合 8CPU 以上的应用。
年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式:如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

并发垃圾收集信息
持久代并发收集次数
传统 GC 信息
花在年轻代和年老代回收上的时间比例减少年轻代和年老代花费的时间,一般会提高应用的效率

吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次 Full GC 后,对年老代进行压缩。
八、常见异常
内存泄漏检查内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。内存泄漏对系统危害比较大,因为他可以直接导致系统的崩溃。需要区别一下,内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最终果是一样的。内存泄漏是用完的资源没有回收引起错误,而系统超负荷则是系统确实没有那么多资源可以分配了(其他的资源都在使用)。
年老代堆空间被占满异常:java.lang.OutOfMemoryError.Java heap space 说明:堆大小与使用堆成锯齿状。这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。锯齿状每一次形成三角的顶点而下降为回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点,可以发现一条由底到高的线,这说明,随时间的推移,系统的堆空间被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄漏。(上面的图仅供示例,在实际情况下收集数据的时间需要更长,比如几个小时或者几天)解决:通过 jmap 把可用对象打出来,这种方式解决起来也比较容易,一般就是根据垃极回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。
持久代被占满异常:java.lang.OutOfMemoryError:PermGen space 说明:Perm 空间被占满。无法为新的 cdass 分配存储空间而引发的异常。这个异常以前是没有的,但是在 Java 反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致 Perm 区被占满。更可怕的是,不同的 classLoader 即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有 N 个 classLoader 那么他将会被加载 N 次。因此,某些情况下,这个问题基本视为无解。当然,存在大量 classLoader 和大量反射类的情况其实也不多。解决:1.-XX:MaxPermSize=16m。2. 换用 JDK。比如 JRocket。
堆栈溢出异常:java.lang.StackOverflowError 说明:一般就是递归没返回,或者循环调用造成.
线程堆栈满异常:Fatal:Stack size too small 说明:java 中一个线程的空间大小是有限制的。JDK5.0 以后这个值是 1M。与这个线程相关的数据会保存在其中。但是当线程空间满了以后,将会出现上面异常。解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。
系统内存被占满异常:java.lang.OutOfMemoryError:unable to create new.native thread 说明:这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在 Java 堆中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。分配给 Java 虚拟机的内存越多,系统剩余的资源就越少,因此,当系统内存固定时,分配给 Java 虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改 -Xss 来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。解决:1. 重新设计系统减少线程数量。2. 线程数量不能减少的情况下,通过 -Xss 减小单个线程大小。以便能生产更多的线程。
九、JVM 调优工具
JVM 可视化监控工具:Jconsole:jdk 自带,功能简单,但是可以在系统有一定负荷的的情况下使用。对垃圾回收算法有很详细的跟踪。Jprofile:商业软件,需要付费。功能强大。VisualVM:JDK 自带,功能强大,与 JProfile 类似。推荐。
JVM 调优监控工具 jstat(性能分析)https://www.jianshu.com/p/213710fb9e40
jstack(分析线程情况)https://jingyan.baidu.com/article/4f34706e3ec075e387b56df2.htmlhttp://www.importnew.com/23601.html
jmap(堆栈 dump)https://www.cnblogs.com/kongzhongqijing/articles/3621163.html
jinfo(jvm 信息情况)https://www.jianshu.com/p/ece32dacce64
jps(jvm 进程情况)https://www.jianshu.com/p/d39b2e208e72
JVM 调优工具转载至一些优质的博客,可以详细学习参考
文档整理于 JVM 视频学习课程,感谢提供教育视频的 IT 人才们,整理分享还未接触到这层知识的人们,也收藏个人学习。

正文完
 0