在《Java对象在Java虚拟机中的建立过程》了解到对象建立的内存分配,在《Java内存区域 JVM运行时数据区》中了解到各数据区有些什么特色、以及相关参数的调整,在《Java虚拟机垃圾回收(一) 基础》中了解到如何判断对象是存活仍是已经死亡?在《Java虚拟机垃圾回收(二) 垃圾回收算法》了解到Java虚拟机垃圾回收的几种常见算法,在《Java虚拟机垃圾回收(三) 7种垃圾收集器》了解到几种收集器的特色和应用等。html
下面来了解总结前面的一些内容:主要包括内存分配与回收策略、方法区垃圾回收、以及JVM垃圾回收的调优方法、垃圾收集器选择。java
经过在《Java虚拟机垃圾回收(二) 垃圾回收算法》"四、分代收集算法"中,咱们知道目前几乎全部商业虚拟机的垃圾收集器都采用分代收集算法,对于HotSpot通常的年代内存划分,以下图:算法
对象的内存分配从大致上讲:数组
在堆上分配(JIT编译优化后可能在栈上分配),主要在新生代的Eden区中分配;安全
若是启用了本地线程分配缓冲,将线程优先在TLAB上分配;服务器
少数状况下,可能直接分配在老年代中。并发
分配的细节取决于当前使用哪一种垃圾收集器组合,以及JVM中内存相关参数设置。oracle
接下来将会讲解几条最广泛的内存分配规则。框架
前面文章曾介绍HotSpot虚拟机新生代内存布局及算法ide
(1)、将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间;
(2)、每次使用Eden和其中一块Survivor;
(3)、当回收时,将Eden和使用中的Survivor中还存活的对象一次性复制到另一块Survivor;
(4)、然后清理掉Eden和使用过的Survivor空间;
(5)、后面就使用Eden和复制到的那一块Survivor空间,重复步骤3;
默认Eden:Survivor=8:1,即每次可使用90%的空间,只有一块Survivor的空间被浪费;
大多数状况下,对象在新生代Eden区中分配;
当Eden区没有足够空间进行分配时,JVM将发起一次Minor GC(新生代GC);
Minor GC时,若是发现存活的对象没法所有放入Survivor空间,只好经过分配担保机制提早转移到老年代。
大对象指须要大量连续内存空间的Java对象,如,很长的字符串、数组;
常常出现大对象容易致使内存还有很多空间就提早触发GC,以获取足够的连续空间来存放它们,因此应该尽可能避免使用建立大对象;
"-XX:PretenureSizeThreshold":
能够设置这个阈值,大于这个参数值的对象直接在老年代分配;
默认为0(无效),且只对Serail和ParNew两款收集器有效;
若是须要使用该参数,可考虑ParNew+CMS组合。
JVM给每一个对象定义一个对象年龄计数器,其计算流程以下:
在Eden中分配的对象,经Minor GC后还存活,就复制移动到Survivor区,年龄为1;
然后每经一次Minor GC后还存活,在Survivor区复制移动一次,年龄就增长1岁;
若是年龄达到必定程度,就晋升到老年代中;
"-XX:MaxTenuringThreshold":
设置新生代对象晋升老年代的年龄阈值,默认为15;
JVM为更好适应不一样程序,不是永远要求等到MaxTenuringThreshold中设置的年龄;
若是在Survivor空间中相同年龄的全部对象大小总和大于Survivor空间的一半,大于或等于该年龄的对象就能够直接进入老年代;
在前面曾简单介绍过度配担保:
当Survivor空间不够用时,须要依赖其余内存(老年代)进行分配担保(Handle Promotion);
分配担保的流程以下:
在发生Minor GC前,JVM先检查老年代最大可用的连续空间是否大于新生全部对象空间;
若是大于,那能够确保Minor GC是安全的;
若是不大于,则JVM查看HandlePromotionFailure值是否容许担保失败;
若是容许,就继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小;
若是大于,将尝试进行一次Minor GC,但这是有风险的;
若是小于或HandlePromotionFailure值不容许冒险,那这些也要改成进行一次Full GC;
尝试Minor GC的风险--担保失败:
由于尝试Minor GC前面,没法知道存活的对象大小,因此使用历次晋升到老年代对象的平均大小做为经验值;
假如尝试的Minor GC最终存活的对象远远高于经验值的话,会致使担保失败(Handle Promotion Failure);
失败后只有从新发起一次Full GC,这绕了一个大圈,代价较高;
但通常仍是要开启HandlePromotionFailure,避免Full GC过于频繁,并且担保失败几率仍是比较低的;
JDK6-u24后,JVM代码中已经再也不使用HandlePromotionFailure参数了;
规则变为:
只要老年代最大可用的连续空间大于新生全部对象空间或历次晋升到老年代对象的平均大小,就会进行Minor GC;不然进行Full GC;
即老年代最大可用的连续空间小于新生全部对象空间时,再也不检查HandelPromotionFailure,而直接检查历次晋升到老年代对象的平均大小;
在《Java内存区域 JVM运行时数据区》曾介绍过方法区及相关的回收问题,虽然JVM规范规定这个区域能够不实现垃圾收集,且针对常量池和类型卸载的收回效果不佳,但方法区实现垃圾回收是必要的,下面再来详细了解。
一、废弃常量
与回收Java堆中对象很是相似;
二、无用的类
同时知足下面3个条件才能算"无用的类":
(1)、该类全部实例都已经被回收(即Java椎中不存在该类的任何实例);
(2)、加载该类的ClassLoader已经被回收,也即经过引导程序加载器加载的类不能被回收;
(3)、该类对应的java.lang.Class对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法;
在大量使用反射、动态代理、常常动态生成大量类的应用,要注意类的回收;
如运行时动态生成类的应用:
一、CGLib在Spring、Hibernate等框架中对类进行加强时会使用;
二、VM的动态语言也会动态建立类来实现语言的动态性;
三、另外,JSP(第一次使用编译为Java类)、基于OSGi频繁自定义ClassLoader的应用(同一个类文件,不一样加载器加载视为不一样类)等;
一、在JDK7中
使用永久代(Permanent Generation)实现方法区,这样就能够不用专门实现方法区的内存管理,但这容易引发内存溢出问题;
有规划放弃永久代而改用Native Memory来实现方法区;
再也不在Java堆的永久代中生成中分配字符串常量池,而是在Java堆其余的主要部分(年轻代和老年代)中分配;
更多请参考:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/enhancements-7.html
二、在JDK8中
永久代已被删除,类元数据(Class Metadata)存储空间在本地内存中分配,并用显式管理元数据的空间:
从OS请求空间,而后分红块;
类加载器从它的块中分配元数据的空间(一个块被绑定到一个特定的类加载器);
当为类加载器卸载类时,它的块被回收再使用或返回到操做系统;
元数据使用由mmap分配的空间,而不是由malloc分配的空间;
三、相关参数
"-XX:MaxMetaspaceSize" (JDK8):指定类元数据区的最大内存大小;
"-XX:MetaspaceSize" (JDK8):指定类元数据区的内存阈值--超过将触发垃圾回收;
"-Xnolassgc":控制是否对类进行回收;
"-verbose:class"、"-XX:TraceClassLoading"、"-XX:TraceClassUnloading":查看类加载和卸载信息;
更多请参考:
《Java语言规范》12.7 卸载类和接口;
JDK8类元数听说明: http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/considerations.html#sthref62
内存回收与垃圾收集器是影响系统性能、并发能力的主要因素之一,通常都须要进行一些手动的测试、调整优化;
下面介绍的是一些思路,并不是是具体的参数设置。
首先应该明确咱们的应用程序调整垃圾回收指望的目标(关注点)是什么?
在前文曾介绍过一般有这些关注点:
(1)、停顿时间
GC停顿时间越短就适合须要与用户交互的程序,良好的响应速度能提高用户体验;
与用户交互较多的场景,以给用户带来较好的体验;
如常见WEB、B/S系统的服务器上的应用;
(2)、吞吐量
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间);
高吞吐量能够高效率地利用CPU时间,尽快完成运算的任务,主要适合在后台计算而不须要太多交互的任务;
应用程序运行在具备多个CPU上,对暂停时间没有特别高的要求;
程序主要在后台进行计算,而不须要与用户进行太多交互;
例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序;
(3)、覆盖区(Footprint)
在达到前面两个目标的状况下,尽可能减小堆的内存空间,以得到更好的空间局部性;
能够减小到不知足前两个目标为止,而后再解决未知足的目标;
若是是动态收缩的堆设置,堆的大小将随着垃圾收集器试图知足竞争目标而振荡;
总结就是:低停顿、高吞吐量、少用内存资源;
通常这些目标都相互影响的,增大堆内存得到高吞吐量但会增加停顿时间,反之亦然,有时需折中处理。
JVM有自适应选择、调整相关设置的功能;
通常都会先根据平台性能来选择好垃圾收集器,以及设置好其参数;
在运行中,一些收集器还会收集监控信息来自动地、动态的调整垃圾回收策略;
因此当咱们不知道何如选择收集器和调整时,应该首先让JVM自适应调整;
而后经过输出GC日志进行分析,看能不能知足明确指望的目标(第一步);
若是不能知足,或者经过打印设置的参数信息,发现能够有更好的调优时,能够进行手动指定参数进行设置,并测试;
须要明确一个观点:
没有最好的收集器,更没有万能的收集;
选择的只能是对具体应用最适合的收集器;
咱们知道HotSpot有这些组合能够搭配使用:
Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
到实践调优阶段,那必需要了解每一个具体收集器的行为特色、优点和劣势、调节参数等(请参考前面的文章内容);
而后根据明确指望的目标,选择具体应用最适合的收集器;
当选择使用某种并行垃圾收集器时,应该指按期望的具体目标而不是指定堆的大小;
让垃圾收集器自动地、动态的调整堆的大小来知足指望的行为;
即堆的大小将随着垃圾收集器试图知足竞争目标而振荡;
固然有时发现问题,堆的大小、划分也是须要进行一些调整的,通常规则:
除非应用程序没法接受长时间的暂停,不然能够将堆调的尽量大一些;
除非发现问题的缘由在于老年代的垃圾收集或应用程序暂停次数过多,不然你应该将堆的较大部分分给年轻代;
等等…
例如,使用Parallel Scavenge/Parallel Old组合,这是一种值得推荐的方式:
一、只需设置好内存数据大小(如"-Xmx"设置最大堆);
二、而后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标;
三、那些具体细节参数的调节就由JVM自适应完成;
设置调整后,应该经过在产生环境下进行不断测试,来分析是否达到咱们的目标;
更多"指望的目标和JVM自适应调整"信息请参考:
《垃圾收集调优指南》 2节 Ergonomics:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ergonomics.html#ergonomics
更多"垃圾收集器选择"信息请参考:
《垃圾收集调优指南》 5节 Available Collectors:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref27