JVM总结
Posted itxiaok
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM总结相关的知识,希望对你有一定的参考价值。
一:java内存区域
jvm布局:
jdk1.6版本JVM布局分为:heap(堆),method(方法区),stack(虚拟机栈),native stack(本地方法栈),程序计数器共五大区域。
其中方法区包含运行时常量池。堆和方法区是线程共享的,虚拟机栈和本地方法栈、程序计数器是随线程而创建的。
堆
堆:储存对象信息和数组。
对象信息/数组包括对象头,实例数据和对齐填充共三个区域;
用于存放由new创建的对象和数组**。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。**
1.1.1、对象头包括二/三部分内容:
一是类型指针,即对象指向它的类元数据的指针,通过这个指针来确定那个类的实例(指向方法区储存的对象类型数据);
二是用于存储对象自身的运行时数据**(mark down)**,如哈希码,gc分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间戳等
三是如果存储的是数组,对象头除了包含以上俩个内容外还必须得有数组的长度数据。
其中类型指针还有一个知识点是对象的访问定位,对象的访问方式主流的有俩种,句柄和直接指针。这俩种区别与特点是:
①、在对象被移动时(垃圾收集时会移动对象即内存整理),句柄只需修改句柄中的对象实例数据的指针,栈中的reference(引用)不需要修改。而直接指针需要修改。
②、直接指针的优势就是速度更快,因为少了一次指针定位的时间开销。
1.1.2、实例数据包括实例化对象存储的数据。
1.1.3、对齐填充:对齐填充并不是必然存在的,也没有特殊含义。由于hotspot规定对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时就需要通过对齐填充来补全。
1.1.4、对象创建分配内存有俩种方式:指针碰撞(无内存泄漏)和空间列表(内存不连续,分配时找到一块足够大的内存划分给对象实例);选择哪种方式创建由垃圾收集器是否有压缩整理功能决定。
方法区
1.2、方法区:存储已被jvm加载的类信息(类名,访问修饰符,字段描述,方法描述等)、常量、静态变量、即时编译器编译后的代码等数据。也就是说 final/static 的“基本数据类型变量”数据和指针放在方法区,没有final/static 的“基本数据类型变量”数据和指针放在虚拟机栈中。jdk1.6及以前方法区和堆独立区域,jdk1.7方法区中的字符串常量池放在堆中,jdk1.8删除方法区改成元空间(metaSpace)。
1.2.1、运行时常量池:存储编译期间生成的各种字面量和符号引用。其中有个知识点是string的intern()方法。
详情见http://blog.csdn.net/hupoling/article/details/62423613的总结。
虚拟机栈
1.3、虚拟机栈:,一般由三部分组成:局部变量表、操作数据栈和帧数据区
局部变量表:局部变量**和对象的引用**
操作数栈:主要保存计算过程的中间结果,同时作为计算过程中的变量临时的存储空间。
帧数据区:常量池解析**的一些数据(指向常量池的指针和异常处理表),这里帧数据区保存着访问常量池的指针,方便程序访问常量池,另外当函数返回或出现异常时虚拟机必须有一个异常处理表,方便发现异常的时候找到异常的代码,因此异常处理表也是帧数据区的一部分。**
其实严格来说虚拟机栈包含局部变量表、操作数栈、动态链接、方法出口等信息。其中局部变量表存储八种基本数据类型(byte,boolean,char,short,int,float,long,double)和对象引用。平常讨论的栈都是指的局部变量。
本地方法栈
1.4、本地方法栈:本地方法栈和虚拟机栈作业是十分相似的,只不过虚拟机栈是为虚拟机执行java方法(也就是字节码)服务,而本地方法栈是为虚拟机执行native方法服务。sun hotspot虚拟机本地方法栈和虚拟机栈合二为一。
程序计数器
1.5、程序计数器:可以看做是当前线程执行字节码的行号指示器。此区域是jvm规范中唯一没有规定任何OOM情况的区域。
直接内存
直接内存:在jdk1.4之后新加入了NIO,可以使用native函数库直接分配堆外内存(本机内存),然后通过一个存储在java堆中的directByteBuffer对象作为这块内存的引用进行操作。避免了io在java堆和native堆来回复制数据提升对写性能。直接内存大小默认是java堆xmx;
OOM
OOM
全称“Out Of Memory”,翻译成中文就是“内存用完了”,来源于java.lang.OutOfMemoryError。看下关于的官方说明: Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector. 意思就是说,当JVM因为没有足够的内存来为对象分配空间并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(注:非exception,因为这个问题已经严重到不足以被应用处理)。
为什么会出现OOM?
1)分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。
2)应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。
3.1、堆溢出:Xmx:最大堆内存,Xms:最小堆内存(初始堆内存),当Xmx=Xms时堆不扩展。-XX:+HeapDumpOnOutOfMemoryError ;实例化大量对象可测试堆溢出。
3.1.1、OOM解决:增大xmx扩大堆内存
3.2、栈溢出:栈溢出有俩种情况:
3.2.1、栈的深度超过最大深度限制,抛出StackOverflowError异常
3.2.2、栈扩展时内存不足,抛出OOM;
Xss:每个栈的大小;Xoss:本地方法栈大小,不过实际上xoss无效,栈容量只由-Xss参数设置。
3.2.3、实测中3.2.1说法有点不全。
单线程下只有一个栈,所以只可能出现StackOverflowError,无论是栈的深度太大还是每个栈帧过大导致内存不足;
多线程下会出现OOM,不断创建线程时会出现内存不足;
3.2.4、OOM/StackOverflowError解决:
3.2.4.1、OOM:由于栈内存=操作系统内存 - 堆内存 - 方法区内存 - 程序计数器内存,所以可以减小堆内存来扩大栈内存大小。栈内存大小影响系统并发线程量(栈内存>=每个栈的大小xss线程量);具体设置由n台服务器,每台服务器m个cpu,则最大线程量=nm;每个栈的大小xss<=栈内存/n*m; 注意一下有个问题 ,这个公式没有直接内存?
3.2.4.2、StackOverflowError深度:如果使用jvm默认设置,栈的深度大多数情况下可达到1000~2000,足以在日常开发中使用。注意避免代码中存在超过1000的方法嵌套。每个方法嵌套对应一个栈帧。
3.2.4.3、StackOverflowError栈帧大小:单线程下避免代码中存在大量基本类型或对象引用。
3.2.4.4、多线程下假设每个栈帧特大,jvm是抛出OOM还是StackOverflowError?(待考察研究)
3.3、方法区OOM:-XX:PermSize:最小方法区内存大小;-XX:MaxPermSize:最大方法区内存大小。
3.3.1、OOM解决:避免大量的string.intern();避免大量的动态java(jsp,java反射)。
3.4、本机直接内存溢出:-XX:MaxDirectMemorySize:最大直接内存;默认为Xmx
3.4.1、OOM解决:使用NIO分配本机内存多注意是否超过MaxDirectMemorySize;平常开发容易忽略直接内存;
二:垃圾收集
垃圾回收条件
jvm垃圾回收分为俩点,一是对象的内存回收也就是堆内存回收,二是字面量(运行时常量池)和类信息内存回收也就是方法区内存回收。
对象内存回收(heap)
如何判断对象是否可以回收,市场上有引用计数算法和可达性分析算法两种方法 :
引用计数算法
引用计数算法:简单来说就是,一个对象A被引用一次,对象A的引用次数就加一。当对象A的引用次数为0(也就是没有其他地方引用此对象)时可以回收。然而这里存在个问题就是 对象之间互相引用 A->B,B->A时 AB无法回收。不过微软公司的com(componet object model)技术用的就是这个算法,而目前流行的主流的jvm没有选用此算法的。
可达性分析算法
可达性分析算法:简单来说就是,通过一系列名为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,可回收的。
java语言中,GC Roots包括四种对象:
1.1.2.1 虚拟机栈(栈帧中的局部变量表)中引用的对象
1.1.2.2 本地方法栈中(native方法)引用的对象
1.1.2.3 方法区中类静态属性引用的对象
1.1.2.4 方法区中常量引用的对象
之所以是上面四种,总结为:GC管理的主要区域是Java堆,一般情况下只针对堆进行垃圾回收。方法区、栈和本地方法区不被GC所管理,因而选择这些区域内的对象作为GC roots
方法区内存回收
方法区内存回收:jvm规范不要求虚拟机在方法区实现垃圾收集(因为性价比低,回收一次回收的空间实际情况很小),但是jvm还是有方法区回收的。
1.2.1 字面量回收:如果一个字面量没有被引用就回被回收;
1.2.2 类信息回收:类信息回收需要同时满足3个条件:
1.2.2.1 java堆中不存在此类的实例;
1.2.2.2 加载该类的classloader已经被回收
1.2.2.3 该类对应的java.lang.Class对象没有任何地方引用,也就是说无法再任何地方通过反射访问该类的方法。
四大类型引用
四大类型引用:自jdk1.2之后(jdk1.1引用分的种类不够明确),jvm中还有几种类型引用比较特殊;。分别是 强引用、软引用、弱引用、虚引用。
1.3.1 强引用:Object obj=new Object();只要强引用还存在,垃圾收集器永远不会回收被引用的对象。假如obj=null或者obj引用的栈帧出栈(不存在)就会被回收。
1.3.2 软引用:将要发生内存溢出之前,将会把软引用的对象进行回收。SoftReference类实现软引用。
1.3.3 弱引用:只能生存到下一次gc之前。WeakReference类实现弱引用。
1.3.4 虚引用:这个需要注意,虚引用无法用来取得一个对象的实例。虚引用不会对对象的生存时间有任何影响,只是能在这个对象被回收时收到一个系统通知。推荐这篇文章更加了解虚引用:http://www.mamicode.com/info-detail-988201.html。
关于四种引用推荐一篇文章里面有详细例子:http://blog.csdn.net/u011277123/article/details/53908315
finalize() 对象自救
对象在可达性分析之后还需要进行一些逻辑。哪怕对象在可达性分析之后没有发现与GcRoots的引用链,jvm还需要进行下面几步(需要俩次标记):
如果对象在进行可达性分析后发现没有和GCRoots引用链向关联,会被第一次标记, 第一次被标记过的对象,会检查该对象是否重写了finalize()方法。如果重写了该方法,则将其放入一个F-Query队列中,已经加载过finalize()方法或者没有重新,直接将对象加入“即将回收”集合。在第二次标记之前,F-Query队列中的所有对象会逐个执行finalize()方法,但是不保证该队列中所有对象的finalize()方法都能被执行,这是因为JVM创建一个低优先级的线程去运行此队列中的方法(防止执行的很缓慢或者死循环),很可能在没有遍历完之前,就已经被剥夺了运行的权利。那么运行finalize()方法的意义何在呢?这是对象避免自己被清理的最后手段:如果在执行finalize()方法的过程中,使得此对象重新与GC Roots引用链相连,则会在第二次标记过程中将此对象从F-Query队列中清除,避免在这次回收中被清除,恢复成了一个“正常”的对象。但显然这种好事不能无限的发生,对于曾经执行过一次finalize()的对象来说,之后如果再被标记,则不会再执行finalize()方法,只能等待被清除的命运。之后,GC将对F-Queue中的对象进行第二次小规模的标记,将队列中重新与GC Roots引用链恢复连接的对象清除出“即将回收”集合。所有此集合中的内容将被回收
垃圾收集算法
2.垃圾收集算法:jvm垃圾收集可以分为四种(严格来说是三种):标记-清除算法;标记-整理算法;复制算法;分代收集算法。
2.1 标记-清除算法:先把可回收内存标记,然后在清除掉;缺点是:会存在内存碎片导致内存泄漏。
2.2 标记-整理算法:先把可回收内存标记,然后让存活的对象都向一端移动,最后清除点端边界以外的内存;缺点:比较标记-清除时间增长;
2.3 复制算法:把内存平分为两份s1,s2;保证只用一个内存区s1,另一个为空s2;把存活的对象复制到为空的那个内存区域s2,然后在清除掉这个区域s1;缺点:内存减半
2.4 分代收集算法:根据jvm特性,年轻代实际每次gc时存活对象较少,故用推荐复制算法;年老代存活对象较多,并且没有其他内存为年老代分配担保(分配担保:举个栗子:年老代为年轻代进行分配担保,当年轻代minor gc内存不足时【比如有个大对象obj1】obj1会放到年老代中;而年老代没有其他空间为其分担了)所以推荐标记-整理算法。
3.hotspot算法实现:暂时未深入理解
垃圾收集器
4.垃圾收集器:一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的
共七中垃圾收集器,分别是serial、parnew、parallel scavenge和cms、serial old、 parallel old、G1;其中serial、parnew、parallel scavenge用于年轻代,cms、serial old、 parallel old用于年老代,g1用于年轻代和年老代。所有收集器都存在stop the world,不过在java发展中 一直在优化停顿时间。
先总结比较这七个收集器:
年轻代:
Serial串行回收器
4.1.Serial串行回收器-XX:+UseSerialGC命令行可选项强制指定。参数可以设置使用新生代串行和老年代串行回收器:
适用于年轻代垃圾回收,复制算法,jdk1.3之前,回收期间暂停所有应用线程的执行, 推荐用于客户端模式(Client)下的虚拟机,属于单线程,对stop the world(GC停顿)无优化,属于最老的年轻代垃圾收集器产品;
把Eden区的存活对象移到To区,To区装不下直接移到年老代,把From区的移到To区,To区装不下直接移到年老代,From区里面年龄很大的升级到年老代。 回收结束之后,Eden和From区都为空,此时把From和To的功能互换,From变To,To变From,每一轮回收之前To都是空的。设计的选型为复制。
ParNew并行回收器
4.2 ParNew并行回收器XX:+UseParNewGC:
适用于年轻代垃圾回收,复制算法,jdk1.3发布,推荐用于服务端模式(Server)下的虚拟机,属于多线程,对stop the world无优化,其实就是Serial的多线程版本;是服务端开发的首先;
Parallel Scavenge并行收集器
4.3 Parallel Scavenge并行收集器:
适用于年轻代垃圾回收,复制算法,jdk1.4发布,属于多线程,对stop the world无优化,它是一个可以控制吞吐量的收集器,拥有自适应调节策略;需要注意一点的是,**gc停顿时间缩短是牺牲吞吐量和新生代空间来换取的。**
stop The World:gc时 需要停止所有线程进行垃圾回收;
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
-XX:+UseParallelOldGC 进行设置 -XX:+ParallelGCThread也可以设置垃圾收集时的线程教量。
自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提高最合适的停顿时间或最大吞吐量。
老年代:
Serial old 收集器
4.4 Serial old :适用于年老代垃圾回收,标记-整理算法,jdk1.5之前,主要用于client下的虚拟机,如果用在server模式下主要有俩个作用:一是jdk1.5以及之前用于与Parallel Scavenger搭配使用,二就是为cms提供后备预案,属于单线程,无stop the world(full GC)优化。
Parallel Old收集器
4.5 Parallel Old:适用于年老代垃圾回收,标记-整理算法,jdk1.6发布,属于多线程,注重于吞吐量控制,是为了Parallel Scavenge定制的;
CMS收集器
4.6 CMS(ConcurrentMarkSweep): 适用于年老代垃圾回收,标记-清除算法,jdk1.5发布,推荐server模式下,属于多线程,对stop the world(full GC)有优化,CMS是一种以获取最短回收停顿时间为目标的收集器,也就是优化服务器响应速度。CMS分为4步:其中 初始化标记,重新标记会发生 stop the world,比并发标记和并发清除的时间更短,而这2个不会发生stop the world,同运行线程一起运行.
"-XX:+UseConcMarkSweepGC"
1,3会进行GC 停顿, 2,4不会
4.6.1 初始标记:仅仅只是标记GcRoots可以直接关联的对象;
4.6.2 并发标记:并发的进行GC引用链搜索.
4.6.3 重新标记:修正并发标记因用户程序继续运行而导致标记发生变动那一部分对象的标记记录。
4.6.4 并发清除:并发清理标记的对象内存。
4.6.5 CMS缺点:
4.6.5.1 CMS对cpu资源非常敏感
默认启动的回收线程数是(cpu数量+3)/4;垃圾回收线程数量不少于25%的cpu资源,当cpu越大,线程数/cpu总量 越小;但是当cpu小于4个时 显得就不怎么合适了;
4.6.5.2 无法处理浮动垃圾
需要等下一次gc才能处理;并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集; "-XX:CMSInitiatingOccupancyFraction":设置CMS可用内存空间;
要是CMS运行期间预留的空间不满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这是虚拟机启动后备预案:临时启用Serial Old,这样一来停顿时间就很长了,所以-XX:CMSInitiatingOccupancyFraction 设置太高容易导致大量“Concurrent Mode Failure”失败,性能反而降低
4.6.5.3 由于用的是标记-清除算法,所以会出现内存碎片;
CMS提供-XX:UseCMSCompactAtFullCollection开关参数(默认为开),用于CMS收集器要进行
FullGC时进行内存碎片合并整理,-XX:CMSFullGCsBeforeCompaction 用来设置执行多少次不压缩Full GC后跟着来一次带压缩的(默认为0,表示每次进入Full GC都进行碎片压缩)。
总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;**但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;**
年轻代+老年代(G1收集器)
4.7 G1:jdk1.7发布,整体看是“标记-整理算法”,从局部(俩个Region之间)上来看是基于“复制”算法实现,也就是说没有内存碎片(因为不要求内存为连续的);
从长期目标来看是为了取代CMS回收器,G1回收器拥有独特的垃圾回收策略,G1属于分代垃圾回收器,区分新生代和老年代,依然有eden和from/to区,它并不要求整个eden区或者新生代、老年代的空间都连续,它使用了分区算法。
G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了.
在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
并行性: G1回收期间可多线程同时工作。 井发性G1拥有与应用程序交替执行能力,部分工作可与应用程序同时执行,在整个GC期间不会完全阻塞应用程序
如果应用追求低停顿,那G1现在可以作为一个尝试的选择,如果应该追求吞吐量,G1并不会带来什么特别的好处!!!
6、G1收集器运作过程
不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。
(A)、初始标记(Initial Marking)
仅标记一下GC Roots能直接关联到的对象;
(B)、并发标记(Concurrent Marking)
进行GC Roots Tracing的过程;
并发的标记出和GC Roots所有有关联的存活对象;
耗时较长,但应用程序也在运行;
?
并不能保证可以标记出所有的存活对象;
(C)、最终标记(Final Marking)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;
?
上一阶段对象的变化记录在线程的Remembered Set Log;
?
这里把Remembered Set Log合并到Remembered Set中
?
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;
?
采用多线程并行执行来提升效率;
(D)、筛选回收(Live Data Counting and Evacuation)
首先排序各个Region的回收价值和成本;
?
然后根据用户期望的GC停顿时间来制定回收计划;
?
最后按计划回收一些价值高的Region中垃圾对象;
?
回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;
?
可以并发进行,降低停顿时间,并增加吞吐量;
内存分配策略
内存分配策略:共有 4个策略**
1.对象优先在Eden分配:major gc时经常会伴随至少一次的minor gc,但并非绝对,比如说 Parallel Scavenge就是直接major gc没有伴随minor gc;
2.大对象直接进入老年代 -XX:PretenureSizeThreshold设置最大对象,单位B;PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效;Parallel Scavenge收集器不需要设置;如果遇到必须使用此参数的场景,可以考虑ParNew+CMS组合;
3.长期存活的对象放入老年代:虚拟机给每个对象定义了一个对象年龄(Age)计数器;gc一次Age+1,当Age大于一定程度(默认为15岁)就会被晋升到老年代;-XX:MaxTenuringThreshold:最大年龄;
4.动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄;
5.空间分配担保:jdk6 update 24后,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行major gc,否则Full GC;
三:JVM性能监控与故障处理工具
1.jdk的命令行工具简单总结介绍
1.1 jps: 获取虚拟机进程 vmid;jps -v :显示虚拟机对应启动的参数
1.2 jinfo:获取虚拟机参数值和动态修改部分运行期可改的参数。jinfo [option] pid例如:jinfo -flag PermSize vmid;显示vid对应的虚拟机的方法区大小;jinfo -flag +/- name:添加或删除name属性;
1.3 jmap:java内存影像工具, jmap -heap vmid:显示堆的详细信息,如参数配置,分代状况;jmap -histo vmid:显示堆中对象统计信息,包括类,实例数量,合计容量。
1.4 jstat:虚拟机统计监控工具,jstat -gccause vmid:输出已使用空间占各自总空间的百分比;
1.5jhat:虚拟机堆转储快照分析工具;jstack:java堆栈跟踪工具;
2.jdk的可视化工具:jConsole和visualVm;其中VisualVM有个BTrace插件值得注意。
2.1 BTrace 动态日志跟踪:可以打印调用堆栈、参数、返回值、性能监视、定位连接泄漏、内存泄漏、解决多线程竞争问题。如生产上遇到问题时需要方法输入参数和返回输出参数,但开发时没有日记记录,平常做法就是补充上日志,然后在重现上线。而BTrace可以在不停止jvm的情况下动态调试代码。(ps:BTrace后续深入研究)
推荐一篇文章:http://huanghaifeng1990.iteye.com/blog/2121419
四:性能优化
1.高性能硬件上部署程序
(1)通过64位JDK来使用大内存
优点:十几个小时或者1天才会进行一次fullGC,通常在深夜使用任务调度,自动进行GC
产生的问题: 1.内存的回收导致长时间停顿
2.64位的JDK的性能测试比32位的要低
3.需要保证程序的稳定
4.相同的程序使用64位比32位更加的会消耗内存,因为指针膨胀或者数据类型自动对齐
(2)使用若干个32位虚拟机建立逻辑集群来利用硬件资源(推荐)
产生的问题:1.尽量的避免节点竞争全局资源(各个节点访问同一个磁盘文件,造成IO异常)
2.很难高效的利用某些资源池
3.各个节点受32位的内存限制 4.大量的本地缓存
简单总结:
一、jvm调优思路:
第一步:jps 获取jvm id;
第二步:获取jvm垃圾收集器种类
第三步:查看gc次数和时间,分析原因来优化
gc次数频繁:①、内存回收率低导致短时间内回收次数多;②、内存大小太小;
gc时间长:①、内存过大;②、内存扩展导致时间长(固定内存太小)
选择适合的收集器也可大幅度优化jvm。
二、优化思路注意点:
1、64位jdk的性能测试结果普遍低于32位jdk;
2、64位jdk由于指针膨胀和数据类型对齐补白导致消耗的内存比32位大;
3、使用nio时,堆外内存不足导致内存溢出。
初始堆值和最大堆的内存越大,吞吐量就越高。
最好使用并行收集器,因为并行收集器速度比串行吞吐量更高,速度更快。
设置堆内存新生代的比例和老年代的比例最好为1:2或者1:3。
减少GC对老年代的回收。
五:类的加载过程?
加载->连接(验证,准备,解析)->初始化->启动(使用)->卸载
加载:
1.从一个类的全限定名来获取该类的二进制字节流
2.将该字节流所代表的静态储存结构转换成方法区的运行时的数据结构
3.在java堆中生成一个该类的Class对象,作为方法区访问这些数据的入口
验证
验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容**包括了下面四个阶段**
文件格式验证,元数据验证,字节码验证和符号引用验证
文件格式验证:保证输入的字节流能正确地解析并存储于方法区内;
数据验证:对元数据信息进行语义校验;
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑;
符号引用验证:对类自身以外(常量池)的信息进行匹配性校验;
准备
为类中的所有静态变量**(类变量)分配内存空间,并为其设置一个初始值**
解析
将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行**(CONSTANT_Class_info 类或者接口,CONSTANT_Fieldref_info 字段 ,CONSTANT_Methodref_info 类方法,CONSTANT_InterfaceMethodref_info 接口方法)**
(符号引用:以一组符号描述所引用的目标,符号可以是任何形式的字面量。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。)
(直接引用:直接引用可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关,如果有了直接引用,那引用的目标在内存中必定存在。)
初始化:
为类的静态变量赋予正确的初始值**,静态变量赋予的是虚拟机默认的初始值,此处赋予的才是程序编写者为变量分配的真正的初始值**
类加载过程的最后一步,前面的类加载过程中,除了加载(Loading)阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
有且仅有四种初始化
1.使用new关键字实例化对象,读取或者设置一个类的静态字段(被final修饰,已在编译期把结果放在常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
2.使用java.lang.reflect包的方法对类进行发射调用的时候,如果类没有进行过初始化
3.当初始化一个类的时候,如果发现其父类还没有初始化,需要先触发其父类的初始化
4.当虚拟机启动时,用户可以指定一个要执行的主类 ,虚拟机会先初始化这个类
触发jvm虚拟机加载一个类 ?
1. 创建类的实例,也就是new的方式
2. 访问某个类或接口的静态变量,或者对该静态变量赋值
3. 调用类的静态方法
4. 反射
5. 创建该类的子类
6. 将该类设置为JVM的启动类
初始化顺序依次是:(静态变量、静态初始化块)–>(变量、初始化块)–> 构造器;
如果有父类,则顺序是:父类static方法 –> 子类static方法 –> 父类(变量、初始化块)->
子类(变量、初始化块)->父类构造方法- -> 子类构造方法
结束生命周期
在以下情况的时候,Java虚拟机会结束生命周期 1. 执行了System.exit()方法 2. 程序正常执行结束 3. 程序在执行过程中遇到了异常或错误而异常终止 4. 由于操作系统出现错误而导致Java虚拟机进程终止
双亲委派模型
除了顶层的启动类加载器外,其他的类加载器都应有自己的父加载器.(不是继承关系来实现的,通过组合关系来复用父加载器的代码)
工作流程:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
为什么需要双亲委派模型呢?
黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。
而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。
或许你会想,我在自定义的类加载器里面强制加载自定义的java.lang.String类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。
1.启动类加载器 Bootstrap ClassLoader java_home/lib
2.其他类加载器 Extension ClassLoader java_home/lib/ext,
Application ClassLoader classpath下的类库
用户自定义类加载器
<检查顺序从后往前,加载顺序从前往后>)量 或者 赋值
虚拟机字节码执行引擎
1 运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机栈的栈元素。
每一个栈帧包括了局部变量表、操作数栈、动态连接、方法返回地址等。编译代码时,栈帧需要多大的局部变量表,多深的操作数栈都已经确定了,并写入到了方法表的Code属性中。因此一个栈帧分配多少内存,与程序运行期无关,仅仅取决于具体的虚拟机实现。
局部变量表:用于存放方法参数和方法内部定义的局部变量。在方法code属性的max_locals数据项中确定了方法的局部变量表的最大容量。
操作数栈:操作数栈的最大容量定义在code属性的max_stacks数据项中。
2 方法调用
方法调用唯一的任务就是确定被调用方法是哪一个,不涉及方法内部的具体运行过程。一切方法调用在class文件里面存储的都是符号引用,而不是直接引用。这个特性给Java带来了动态扩展能力,在类加载甚至运行期间才能确定目标方法的直接引用。 类加载的解析阶段,会将一部分符号引用转化为直接引用。(主要包括静态方法和私有方法)
Java虚拟机里面有5条方法调用字节码指令
invokestatic:调用静态方法;
invokespecial:调用实例构造器方法、私有方法、父类方法;
invokevirtual:调用所有的虚方法;
invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象;
invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行发方法
invokestatic、invokespecial指令调用的方法,可以在解析阶段确定。(静态方法、私有方法、实例构造器、父类方法这4类方法类加载时符号引用就解析为直接引用,称为非虚方法,其他方法称为虚方法)
静态分派:相等于多态中的重载
动态分派:相当于多态中的重写
动态方法调用(MethodHandle)和反射(Reflection)有什么区别?
Reflection 模拟了java代码层次的方法调用
MethodHandle 模拟了字节码层次的方法调用
六:7种垃圾收集器详解
下面先来了解HotSpot虚拟机中的7种垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1,先介绍一些垃圾收集的相关概念,再介绍它们的主要特点、应用场景、以及一些设置参数和基本运行原理。
1、垃圾收集器概述
垃圾收集器是垃圾回收算法(标记-清除算法、复制算法、标记-整理算法、火车算法)的具体实现,不同商家、不同版本的JVM所提供的垃圾收集器可能会有很在差别,本文主要介绍HotSpot虚拟机中的垃圾收集器。
1-1、垃圾收集器组合
JDK7/8后,HotSpot虚拟机所有收集器及组合(连线),如下图:
(A)、图中展示了7种不同分代的收集器:
Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;
新生代收集器:Serial、ParNew、Parallel Scavenge;
?
老年代收集器:Serial Old、Parallel Old、CMS;
?
整堆收集器:G1;
(C)、两个收集器间有连线,表明它们可以搭配使用:
Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
(D)、其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案(后面介绍);
1-2、并发垃圾收集和并行垃圾收集的区别
(A)、并行(Parallel)
指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;
?
如ParNew、Parallel Scavenge、Parallel Old;
(B)、并发(Concurrent)
指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);
?
用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;
?
如CMS、G1(也有并行);
1-3、Minor GC和Full GC的区别
(A)、Minor GC
又称新生代GC,指发生在新生代的垃圾收集动作;
?
因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;
(B)、Full GC
又称Major GC或老年代GC;
?
出现Full GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);
?
Major GC速度一般比Minor GC慢10倍以上;
下面将介绍这些收集器的特性、基本原理和使用场景,并重点分析CMS和G1这两款相对复杂的收集器;但需要明确一个观点:
没有最好的收集器,更没有万能的收集;
?
选择的只能是适合具体应用场景的收集器。
2、Serial收集器
Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器;
?
JDK1.3.1前是HotSpot新生代收集的唯一选择;
1、特点
针对新生代;
?
采用复制算法;
?
单线程收集;
?
进行垃圾收集时,必须暂停所有工作线程,直到完成;
?
即会"Stop The World";
?
Serial/Serial Old组合收集器运行示意图如下:
2、应用场景
依然是HotSpot在Client模式下默认的新生代收集器;
?
也有优于其他收集器的地方:
?
简单高效(与其他收集器的单线程相比);
?
对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;
?
在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的
3、设置参数
"-XX:+UseSerialGC":添加该参数来显式的使用串行垃圾收集器;
4、Stop TheWorld说明
JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿;
?
会带给用户不良的体验;
3、ParNew收集器
ParNew垃圾收集器是Serial收集器的多线程版本。
1、特点
除了多线程外,其余的行为、特点和Serial收集器一样; 如Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等; 两个收集器共用了不少代码; ParNew/Serial Old组合收集器运行示意图如下:
2、应用场景
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作; 但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。
3、设置参数
"-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器; "-XX:+UseParNewGC":强制指定使用ParNew; "-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;
4、为什么只有ParNew能与CMS收集器配合
CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作; CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作; 因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码; 关于CMS收集器后面会详细介绍。
4、Parallel Scavenge收集器
Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)。
1、特点
(A)、有一些特点与ParNew收集器相似
新生代收集器; 采用复制算法; 多线程收集;
(B)、主要特点是:它的关注点与其他收集器不同
CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间; 而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput);
2、应用场景
高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间; 当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互; 例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序;
3、设置参数
Parallel Scavenge收集器提供两个参数用于精确控制吞吐量:
(A)、"-XX:MaxGCPauseMillis"
控制最大垃圾收集停顿时间,大于0的毫秒数; MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降; 因为可能导致垃圾收集发生得更频繁;
(B)、"-XX:GCTimeRatio"
设置垃圾收集时间占总时间的比率,0<n<100的整数; GCTimeRatio相当于设置吞吐量大小; 垃圾收集执行时间占应用程序执行时间的比例的计算方法是: 1 / (1 + n) 例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%--1/(1+19); 默认值是1%--1/(1+99),即n=99;
垃圾收集所花费的时间是年轻一代和老年代收集的总时间;
如果没有满足吞吐量目标,则增加代的内存大小以尽量增加用户程序运行的时间;
此外,还有一个值得关注的参数:
(C)、"-XX:+UseAdptiveSizePolicy"
开启这个参数后,就不用手工指定一些细节参数,如: 新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等; JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomiscs); 这是一种值得推荐的方式: (1)、只需设置好内存数据大小(如"-Xmx"设置最大堆); (2)、然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标; (3)、那些具体细节参数的调节就由JVM自适应完成; 这也是Parallel Scavenge收集器与ParNew收集器一个重要区别;
4、吞吐量与收集器关注点说明
(A)、吞吐量(Throughput)
CPU用于运行用户代码的时间与CPU总消耗时间的比值; 即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间); 高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间;
(B)、垃圾收集器期望的目标(关注点)
(1)、停顿时间
停顿时间越短就适合需要与用户交互的程序; 良好的响应速度能提升用户体验;
(2)、吞吐量
高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务; 主要适合在后台计算而不需要太多交互的任务;
(3)、覆盖区(Footprint)
在达到前面两个目标的情况下,尽量减少堆的内存空间; 可以获得更好的空间局部性;
更多Parallel Scavenge收集器的信息请参考:
官方的垃圾收集调优指南 第6节:[http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/parallel.html#parallel_collector](http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/parallel.html%23parallel_collector)
上面介绍的都是新生代收集器,接下来开始介绍老年代收集器;
5、Serial Old收集器
Serial Old是 Serial收集器的老年代版本;
1、特点
针对老年代; 采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact); 单线程收集; Serial/Serial Old收集器运行示意图如下:
2、应用场景
主要用于Client模式; 而在Server模式有两大用途: (A)、在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配); (B)、作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用(后面详解);
更多Serial Old收集器信息请参考:
内存管理白皮书 4.3.2节:<http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf>
6、Parallel Old收集器
Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本; JDK1.6中才开始提供;
1、特点
针对老年代; 采用"标记-整理"算法; 多线程收集; Parallel Scavenge/Parallel Old收集器运行示意图如下:
2、应用场景
JDK1.6及之后用来代替老年代的Serial Old收集器; 特别是在Server模式,多CPU的情况下; 这样在注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合;
3、设置参数
"-XX:+UseParallelOldGC":指定使用Parallel Old收集器;
更多Parallel Old收集器收集过程介绍请参考:
《内存管理白皮书》 4.5.2节: [????http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf](https://blog.csdn.net/tjiyu/article/details/53983650)
7、CMS收集器
并发标记清理(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延迟(low-latency)垃圾收集器; 在前面ParNew收集器曾简单介绍过其特点;
1、特点
针对老年代; 基于"标记-清除"算法(不进行压缩操作,产生内存碎片); 以获取最短回收停顿时间为目标; 并发收集、低停顿; 需要更多的内存(看后面的缺点);
是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器; 第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
2、应用场景
与用户交互较多的场景; 希望系统停顿时间最短,注重服务的响应速度; 以给用户带来较好的体验; 如常见WEB、B/S系统的服务器上的应用;
3、设置参数
"-XX:+UseConcMarkSweepGC":指定使用CMS收集器;
4、CMS收集器运作过程
比前面几种收集器更复杂,可以分为4个步骤:
(A)、初始标记(CMS initial mark)
仅标记一下GC Roots能直接关联到的对象; 速度很快; 但需要"Stop The World";
(B)、并发标记(CMS concurrent mark)
进行GC Roots Tracing的过程; 刚才产生的集合中标记出存活对象; 应用程序也在运行; 并不能保证可以标记出所有的存活对象;
(C)、重新标记(CMS remark)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录; 需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短; 采用多线程并行执行来提升效率;
(D)、并发清除(CMS concurrent sweep)
回收所有的垃圾对象; 整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作; 所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行; CMS收集器运行示意图如下:
5、CMS收集器3个明显的缺点 (A)、对CPU资源非常敏感 并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。 CMS的默认收集线程数量是=(CPU数量+3)/4; 当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。
增量式并发收集器: 针对这种情况,曾出现了"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS); 类似使用抢占式来模拟多任务机制的思想,让收集线程和用户线程交替运行,减少收集线程运行时间; 但效果并不理想,JDK1.6后就官方不再提倡用户使用。
更多请参考:
官方的《垃圾收集调优指南》8.8节 Incremental Mode:[http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#CJAGIIEJ](http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html%23CJAGIIEJ) 《内存管理白皮书》 4.6.3节可以看到一些描述;
(B)、无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败
(1)、浮动垃圾(Floating Garbage)
在并发清除时,用户线程新产生的垃圾,称为浮动垃圾; 这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集; 也要可以认为CMS所需要的空间比其他垃圾收集器大; "-XX:CMSInitiatingOccupancyFraction":设置CMS预留内存空间; JDK1.5默认值为68%; JDK1.6变为大约92%;
(2)、"Concurrent Mode Failure"失败
如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败; 这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生; 这样的代价是很大的,所以CMSInitiatingOccupancyFraction不能设置得太大。
(C)、产生大量内存碎片
由于CMS基于"标记-清除"算法,清除后不进行压缩操作; 前面[《Java虚拟机垃圾回收(二) 垃圾回收算法》](http://blog.csdn.net/tjiyu/article/details/53983064)"标记-清除"算法介绍时曾说过: 产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。 解决方法:
(1)、"-XX:+UseCMSCompactAtFullCollection"
使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程; 但合并整理过程无法并发,停顿时间会变长; 默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);
(2)、"-XX:+CMSFullGCsBeforeCompaction"
设置执行多少次不压缩的Full GC后,来一次压缩整理; 为减少合并整理过程的停顿时间; 默认为0,也就是说每次都执行Full GC,不会进行压缩整理; 由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大; 更多关于内存分配方式请参考:《[Java对象在Java虚拟机中的创建过程](http://blog.csdn.net/tjiyu/article/details/53923392)》 总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间; 但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;
更多CMS收集器信息请参考:
《垃圾收集调优指南》 8节 Concurrent Mark Sweep (CMS) Collector:[http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#concurrent_mark_sweep_cms_collector](http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html%23concurrent_mark_sweep_cms_collector) 《内存管理白皮书》 4.6节 Concurrent Mark-Sweep (CMS) Collector:<http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf>
8、G1收集器
G1(Garbage-First)是JDK7-u4才推出商用的收集器;
1、特点
(A)、并行与并发
能充分利用多CPU、多核环境下的硬件优势; 可以并行来缩短"Stop The World"停顿时间; 也可以并发让垃圾收集与用户程序同时进行;
(B)、分代收集,收集范围包括新生代和老年代
能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配; 能够采用不同方式处理不同时期的对象;
虽然保留分代概念,但Java堆的内存布局有很大差别; 将整个堆划分为多个大小相等的独立区域(Region); 新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合; 更多G1内存布局信息请参考: 《垃圾收集调优指南》 9节:[http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html#garbage_first_garbage_collection](http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html%23garbage_first_garbage_collection)
(C)、结合多种垃圾收集算法,空间整合,不产生碎片
从整体看,是基于标记-整理算法; 从局部(两个Region间)看,是基于复制算法; 这是一种类似火车算法的实现;
都不会产生内存碎片,有利于长时间运行;
(D)、可预测的停顿:低停顿的同时实现高吞吐量
G1除了追求低停顿处,还能建立可预测的停顿时间模型; 可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;
2、应用场景
面向服务端应用,针对具有大内存、多处理器的机器; 最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案; 如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;
用来替换掉JDK1.5中的CMS收集器; 在下面的情况时,使用G1可能比CMS好: (1)、超过50%的Java堆被活动数据占用; (2)、对象分配频率或年代提升频率变化很大; (3)、GC停顿时间过长(长于0.5至1秒)。 是否一定采用G1呢?也未必: 如果现在采用的收集器没有出现问题,不用急着去选择G1; 如果应用程序追求低停顿,可以尝试选择G1; 是否代替CMS需要实际场景测试才知道。
3、设置参数
"-XX:+UseG1GC":指定使用G1收集器; "-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45; "-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒; "-XX:G1HeapRegionSize":设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region; 更多关于G1参数设置请参考: 《垃圾收集调优指南》 10.5节:[http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#important_defaults](http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html%23important_defaults)
4、为什么G1收集器可以实现可预测的停顿
G1可以建立可预测的停顿时间模型,是因为: 可以有计划地避免在Java堆的进行全区域的垃圾收集; G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表; 每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来); 这就保证了在有限的时间内可以获取尽可能高的收集效率;
5、一个对象被不同区域引用的问题
一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确? 在其他的分代收集器,也存在这样的问题(而G1更突出): 回收新生代也不得不同时扫描老年代? 这样的话会降低Minor GC的效率; 解决方法: 无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描: 每个Region都有一个对应的Remembered Set; 每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作; 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象); 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;
当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set; 就可以保证不进行全局扫描,也不会有遗漏。
6、G1收集器运作过程
不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。
(A)、初始标记(Initial Marking)
仅标记一下GC Roots能直接关联到的对象; 且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象; 需要"Stop The World",但速度很快;
(B)、并发标记(Concurrent Marking)
进行GC Roots Tracing的过程; 刚才产生的集合中标记出存活对象; 耗时较长,但应用程序也在运行; 并不能保证可以标记出所有的存活对象;
(C)、最终标记(Final Marking)
为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录; 上一阶段对象的变化记录在线程的Remembered Set Log; 这里把Remembered Set Log合并到Remembered Set中;
需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短; 采用多线程并行执行来提升效率;
(D)、筛选回收(Live Data Counting and Evacuation)
首先排序各个Region的回收价值和成本; 然后根据用户期望的GC停顿时间来制定回收计划; 最后按计划回收一些价值高的Region中垃圾对象;
回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存; 可以并发进行,降低停顿时间,并增加吞吐量;
以上是关于JVM总结的主要内容,如果未能解决你的问题,请参考以下文章