http://www.cnblogs.com/angeldevil/p/3801189.html值得一看
Classic VM和Exact VM两者在GC的时候准确判断heap上的数据是否还可能被使用。由于使用了准确式内存管理,Exact VM放弃了Classic VM基于handler的对象查找方式(其根本原因是进行GC之后对象将可能会被移动位置,如果将地址123456的对象移动到654321),在没有明确信息表明内存中哪些数据是reference的前提下,虚拟机是不敢把内存中所有为123456的值改成654321的
hotspot VM 融合了前两款VM的优点,添加了新的技术,热点代码探测技术
关于垃圾回收机制的深度理解可以阅读openjdk的源码,openjdk和oracle/sun的内核虚拟机的源码90%以上都是公用的
程序计数器:由于每个线程都需要分配一个独立的程序计数器,是一块较小的内存空间,可以看做是当前线程执行的字节码的行号指示器。由于java虚拟机的多线程是通过线程轮流切换并分配处理器的执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说就是一个内核)都知会执行一条形成中的指令。因此为了切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的 计算器互不影响,独立存储,“线程私有的内存”
本地方法栈,如果正在执行的是一个java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值侧为空。此内存区域是唯一一个在java虚拟机 规范中规范张美又红规范中没有规定任何OutOfMemoryError情况的区域。
本地方法栈: 顾名思义:为虚拟机使用到的Native方法服务。hotspot直接把本地方法栈和虚拟机栈合二为一。
java堆:java heap
1、虚拟机所管理的内存中最大的一块
2、所有线程共享的一个内存区域
3、分配对象和数组
4、分代
5、可扩展 -Xmx -Xms
方法区:Method Area Non-Heap
1、各个线程共享的内存区域
2、存储类信息、常量、静态变量、
有种说法将方法区称为永久代,但这种说法并不确切,因为hotspot虚拟机设计团队选择把GC分代扩展至方发区
运行时常量池:方法区的一部分。class分拣中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池Constant pool table,用于存放编译器生成的各种字面量和符号引用,这部分内容将在加载后进入方法区的运行时常量池中存放
直接内存:
在加入NIO 之后,引入一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
内存分配方法:指针碰撞法,空闲列表法,
GC过程对应的内存分配法:Compack,Mark-Sweep
为解决同步问题:方法一、实际虚拟机上采用CAS配上失败尝试的方式保证更新操作的原子性;
方法二、把内存分配的动作按照线程划分在不同的空间中进行,
本地线程分配缓冲(Thread Local Allocation Buffer TLAB)通过-XX:+/-UserTLAB参数设定。
引用方式:句柄和直接指针
采用句柄:在java堆中划分一块内存作为句柄池,reference中存储的就是对象的句柄地址
优点:对象移动(考虑到垃圾回收的行为),只要改动reference实例数据的指针地址
缺点
采用直接指针:java堆中,reference中存储的直接就是对象的地址
优点:速度更快,节省指针定位开销 hotspot采用直接指针访问
缺点:
实战OOM(OutOfMemoryError异常)
2.4.2虚拟机栈和本地方法栈溢出:
在hotspot虚拟机中并不区分虚拟机栈和本地方法栈 -Xoss 设置本地方法栈 -Xss参数设置栈大小
2.4.3 方法区和运行时常量池溢出
-XX:PermSize -XX:MaxPermSize
2.4.4 本机直接内存溢出
-XX:MaxDirectMemorySize 指定
直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后的Dump文件很小,而程序中又直接或间接使用了NIO,那就可以考虑检查下是不是这方面的原因
第3章 垃圾收集器与内存分配策略
程序计数器、虚拟机栈、本地方法栈3个区域伴随线程的生命周期
内存回收机制:判断对象对否已死?
3.2.1 引用计数算法:即当有一个地方引用它时,计数器值就+1,当引用失效时,计数器就减1,
优点:实现简单,判定效率高
主流java虚拟机没有选用引用计数器算法来管理内存,其主要的原因是它很难解决对象之间互相循环引用的问题。
3.2.2可达性分析算法
基本思路就是通过一些列称为:“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没哟任何引用链相连(用图论的话来说,就是GC Roots到这个对象不可达)
3.2.5回收方法区
永久代的垃圾回收主要回收两部分:废弃常量和无用的类。典型的例子:假如一个字符串“abc”已经进入了常量池中,如果当前系统没有任何一个String对象叫做“abc”,如果没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理常量池。
判定一个类是否是“无用的类”需要满足下面3个条件:
1、该类所有的实例都已经被回收,也就是java对中不存在该类的任何实例
2、加载该类的ClassLoader已经被回收
3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
虚拟机可以对满足上述条件的无用类进行回收
HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class 以及 -XX+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息,其中-verbose:class和-XXTraceClassLoading可以在product版的虚拟机中使用,-XX:+TraceClassUnLoading参数需要FastDebug版本虚拟机支持。
反射,动态代理 CGlib等Bycode 框架、动态生成jsp,OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类的卸载功能
3.3 垃圾收集算法
1、标记清除法 Mark-sweep算法 器主要不足的有两个:一个是效率问题,另一个是空间问题,标记清除后会产生大量的不连续的内存碎片
2、复制算法:为了解决效率问题,出现了一种称为“复制”copying的收集算法,主要思路为:将容量分为大小相等的两块,每次只使用其中的一块。,当这一块的内存用完了,就将还存货的对象复制到另外一块上面,然后再把已经用过的内存空间一次性清理掉。该方法的缺点:大幅度缩减了内存的容量。
但该商业虚拟机都采用这种方法来回收新生代
研究表明新生代中的对象98%都是朝生夕死,所以不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中存活的对象一次性复制到另一块Suvivor空间上,也就是每次新生代中可用内存空间为整个新生代容量的90%(80+10)只有10%的内存会被浪费,当Survivor空间不够用的时候,需要依赖老年代进行分担
3、标记-整理算法
标记存活的对象都向一段移动,然后直接清理掉端以外的内存,
4、分代收集算法
该算法根据对象生存周期的不同将内存分为几块。一般分为新生代和老年代,这样就可以跟腱炎不听年代的特点,采用最适当的收集算法,在新生代中,选用复制算法,在老年代中,存活率较高、没有额外空间对其进行分担,就必须使用标记清理或者标记整理算法进行回收。
3.4HotSpot的算法实现
以GC root节点找引用链可达性,体现在GC停顿上,确保快照的一致性,即分析时间整个执行系统看起来冻结在某一个时间点上(Sun将这一时间成为stop the world),即使在CMS收集器(CMS收集器是JAVA虚拟机中垃圾收集器的一种。它运行在JAVA虚拟机的老年代中。CMS收集器是基于“标记-清除”算法实现的),也必须停顿。
目前主流虚拟机使用准确是GC(exact VM)
在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的,在给数据结构的协助下,HotSpot可以快速且准确的完成GC roots枚举,并且室友在安全点(safepoint)才几率特定的位置信息。安全点设置不能太少以至于让GC等待时间太长,
关于跑到安全点停顿下来,有两种方案可供选择:抢险式中断(preemptive Suspension)和主动式(Voluntary Suspension)其中抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。现在几乎没有抢先式中断来暂停线程从而响应GC事件。
3.4.3安全区域:
safe region 针对处于Sleep状态或者Blocked状态,安全区域是指代码片段中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的,当线程执行到safe region中代码时,首先表示自己进入safe region,这样JVM要发起GC时,就不用管自己为Safe Region状态的线程。
JITJITcompiler,just-in-time compiler)将字节码转换成机器码
3.5垃圾回收器
如果两个收集器之间存在连线,就说明他们可以搭配使用。虚拟机所处的区域测表示属于新生代收集器还是老年代收集器。
3.5.1
Serial收集器:是虚拟机新生代的单线程收集器:特点:重要的是它进行垃圾回收时,必须展厅其他所有的工作线程,直到收集结束。
parallel收集器:
Concurrent Mark serial CMS乃至GC收集器的最前沿成果Garbage First(G1)收集器。
ParNew收集器:
是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括收集器可用的所有控制参数
-XX:SurvivorRadio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等、收集算法、
CMS作为老年代的收集器,却无法与JDK1.4中已经存在的新生代收集器 Parallel Scavenge配合工作,所以新生代只能选择Parnew或者Serial收集器中的一个。ParNew收集器也是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC来进行制定。可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数
并发concurrent 指用户线程与来及收集线程同时执行,(但不一定是滨兴,可能会交替执行),用户城西继续运行,而垃圾收集程序运行于另一个CPU上
并行parallel 指多条垃圾搜集线程并行工作,但此时用户线程任然处于等待状态
Parallel Scavenge收集提供了两个参数用于精确控制吞吐量
分别是控制最大两级收集停顿时间的-XX:MaxGCPauseMillis参数
直接设置吞吐量大小的-XX:GCTimeRatio参数
MaxGCPauseMillis参数允许的值是一个大一0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能是的系统的垃圾收集速度变得更快,GC停顿时间缩短是1️⃣牺牲吞吐量和新生代空间来换取的:每次收集的的少,客户停顿的时间少,但收集的频次变高,吞吐量下降。
垃圾收集时间占总时间的比例,相当于是吞吐量的倒数。如果把参数设置为19,那么允许最大GC时间就占总时间的5%
(即1/(1+19)),默认值为99,就是允许最大1%(1/(1+99))的垃圾收集时间。
由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。
除上述两个参数之外,Parallel Scavenge 收集器还有一个参数-XX:UseAdaptiveSizePolicy值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmm)、Eden与Survivor区的比例(-XX:SurvivorRadio)\晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据运行情况收集性能监控信息。
这种调节方式成为GC自适应的调节策略(GC Ergonomics)。
手工优化存在困难的时候没使用Parallel Scavenge收集器配合自适应调节策略,把内存管理任何交给虚拟机去完成
只需要把基本的内存数据设置好:如
-Xmx设置最大堆
MaxGCPauseMillis参数最大停顿时间
GCTimeRadio吞吐量
自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别
3.5.4 Serial Old 收集器
serial Old 是Serial收集器的老年代版本,单线程收集器,使用“标记-整理”算法。这个收集器的主要意义:
给Client模式下的虚拟机使用。
如果再Server模式下,有两大用途:一种用途是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用另一种用途是作为CMS收集器的后备预案,在并发收集发生ConCurrent mode failure时使用。
3.5.5parallel old 是Paralllel scavenge收集器的老年代版本,使用多线程和“标记和整理”算法
在parallel old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scanvenge加Parallel Old收集器。
3.5.6 CMS收集器
CMS(concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
ConCurrent Low Pause Collector
整个过程分为4个步骤:
1、初始标记 CMS initial mark 标记GC roots 速度快
2、并发标记 CMS concurrent mark 耗时长 GC roots tracing的过程
3、重新标记 CMS remark 动态过程中堆标记进行修正,该过程耗时稍比初始标记时间长一些
4、并发清楚 CMS concurrent sweep 耗时长
其中1,3标记仍然需要stop the world
缺点:
CMS收集器对CPU资源非常敏感。-->Incremental ConCurrent Mark Sweep/ i-CMS 增量式并发收集器:抢占式来模拟多任务机制的思想一样,就是在并发标记、清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的独占资源的时间,实践证明,增量时的CMS收集器效果很一般,不提倡使用
CMS收集器无法处理浮动垃圾(floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行之后,CMS无法在档次收集中处理掉它们,只要留待下一次GC时再清理掉。这一部分称为“浮动垃圾。”随着用户线程的持续运行,伴随的垃圾也会随之增加,因此CMS收集器不能像其它收集器那样等到老年代几乎全部填满再进行收集,需要预留当年代使用了68%的空间后就会被激活。这是一个保守的设置,如果再应用中老年代增长不是太快,可以适当提高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以降低内存回收次数从而获得更好的性能。
在JDK1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期间预留的空间无法满足程序需求,就会出现一次Concurrent Mode Failure 失败,这是虚拟机启动后备预案:临时启用Serial Old收集器来重新进行来年代垃圾收集。这样停顿时间就很长。所以说参数-XX:CMSInitatingOccupancyFraction设置的太高导致大量的Concurrent Mode Failure失败性能反而降低
CMS是基于“标记-清楚”算法实现的收集器,收集结束时会产生大量的空间碎片,这样很容易导致老年代内存大部分为空间剩余,但是无法找到足够大的连续空间来分配当前的对象,不得不提前出发一次Full GC办法为了解决这个问题,CMS收集器提高了一个-XX:+UseCMSCompactAtFullCollection开关参数,用于在CMS收集器顶不住要进行FullGC时,开启内存碎片的合并整理过程。代价就是该过程是无法并发,并且停顿时间会有点长(比FullGC端),虚拟机还提供了-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC 时都进行随便整理)
3.5.7G1 garbage first 收集器,最前沿的成果之一,早在1.7正式商业,是一款面向服务端应用的垃圾收集器。
具备如下特点:
并行和并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短stop-the-world停顿时间,部分其他收集器原本需要停顿线程实行的GC动作,G1收集器任然可以通过并发的方式让java程序继续执行。
分代收集:分代在G1中依然保留。G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果
空间整合:与CMS标记清理算法不同,G1从整体来看是基于标记整理算法实现的收集器,从两个region之间上来看是基于复制算法实现的,这两种算法意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续空间而提前出发一次GC
可预测的停顿:这是G1相对于CMS的另一个大优势,降低了停顿时间是G1和CMS共同的关注点,但是G1除了追求低停顿外,环能简历可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片内,小号在垃圾收集上的时间不得超过N毫秒,这似乎诗经实时java(RTSJ)的垃圾收集器的特征了。
在G1之前,收集器收集的范围都是新生代或者老年代
G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离,他们都是一部分Region。
G1收集器建立可预测的停顿时间模型,是因为它可以有计划的避免在整个java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,这也是Garbage-First名称的由来。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获得尽可能高的收集效率。
为了避免采用可达性判断时,对java堆的全局扫描,避免minor GC的效率降低。G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set 来避免全堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行了写操作时,会产生一个write Barrier暂时中断写操作。检查Reference引用的对象是否处于不同Region之中,(分代的例子就是检查是否老年代中的对象引用了新生代中的对象,如果是,便通过cardTable把相关引用信息记录到被引用对象所属的region的Remembered Set中),当进行内存回收时,在GC根节点的枚举范围中,加入Remembered Set 即可保证不对全堆扫描也不会遗漏。
为了避免采用可达性判断时,对java堆的全局扫描,避免minor GC的效率降低。G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set 来避免全堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行了写操作时,会产生一个write Barrier暂时中断写操作。检查Reference引用的对象是否处于不同Region之中,(分代的例子就是检查是否老年代中的对象引用了新生代中的对象,如果是,便通过cardTable把相关引用信息记录到被引用对象所属的region的Remembered Set中),当进行内存回收时,在GC根节点的枚举范围中,加入Remembered Set 即可保证不对全堆扫描也不会遗漏。
注意:文中提到minor GC 知识GC中的一种,对此不理解的可以参考
http://www.importnew.com/15820.html
其中主要定义为:Minor GC对 Eden 和survivor区域进行回收
major GC 是清理老年代
Full GC 是清理整个堆空间
3.5.8理解GC 日志
是处理java虚拟机内存问题的基础技能,
GC日志
GC full GC 说明这次垃圾收集的停顿类型,
如果有Full 说明这次GC 发生了stop-the-world
下面新生代收集器parNew的日志也会出现 [Full GC]
接下来的【DefNew 【Tenured 【Perm表示GC发生的区域
DefNew Defalt New Generation
ParNew parallel New Generation
Parallel Scavenge 收集器 psYoungGen
后面括号内部的3324k->152K(3712k)含义就是GC前内存区域已使用量->GC后该内存区域使用量(该内存区域总容量)
再往后,‘0.0025925.secs’表示该内存区域GC所占用的时间 单位为秒
[Times:user=0.01 sys=0.00,real=0.02 secs]这里的user、sys、real与linux的time命令输出的时间含义一致,分别代表用户态消耗的cpu时间,内核态消耗的cpu事件和操作从开始到结束所经过的墙钟时间(wall clock time)cpu时间和墙钟时间的区别是,墙钟时间表示费运算的等待耗时,列入磁盘IO,等待线程阻塞,而CPU时间不包括这些耗时。
3.6 内存分配与回收策略
java技术体系中所提倡的自动内存管理最终可以终结为自动化地解决了两个问题:
1、给对象分配内存以及回收分配给对象的内存
2、对象分配内存
对象的内存分配,忘大方向讲,就是在堆上分配(但也可能经过JIT compiler just-in-time)变异后被拆散为标量类型并间接地栈上分配,对象主要分配在新生代的Eden上,如果启动了本地线程分配缓冲,将按线程优先在TLAB(threading local allocate buffer)上的分配。少数情况下可能会直接分配在老年代中,分配的规则并不是百分百的固定,其细节取决于当前使用的是哪一种垃圾收集器组合。还有虚拟机与内存相关的参数设置。
最普遍的内存分配规则:
换句话说:验证的是在使用Serial/Serial Old收集器下
ParNew/Serial Old收集器组合的规则基本一致的内存分配和回收的策略
3.6.1 对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC
-XX:+PrintGCDetails 这个收集器日志参数 并且在进程退出的时候输出当前的内存各区域分配情况。
-Xms20M -Xmx20M -Xmn10M 这个三个参数限制了java堆大小为20M,其中10M分配给新生代,剩下的10M分配给老年代
-XX:SurvivorRatio=8 决定了新生代中Eden区与一个Survivor区的空间比例是8:1,
从结果也可以清晰地看到“eden space 8193k 、from space 1024k、to 1024k”的信息
这次minor GC结果为新生代6651k变为148k,而总内存占用量机会没有减少(因为allocation1,allocation2,allocation3三个对象都是存活的,虚拟机几乎没有找到可回收的对象)。这次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用6M,剩余空间已经不足以分配allocation4所需的4M内存,因此发生MinorGC。GC期间又发现虚拟机已经有的3个2MB对象全部无法放入Survivor空间(因为survivor只有1M大小,所以只好通过分配担保机制提前转移到老年代去)
Minor GC 和Full GC 的区别
1、新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为java对象大多数都具备朝生夕灭的特性,所以Minor GC扥长频繁,一般回收速度也比较快。
2、老年代GC(MajorGC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对,在Parallel Scanvenge收集器的收集策略里就有直接进行Mahor GC的策略选择过程)。Major GC的速度一般会比Minor GC 慢10倍以上
测试方式可以用
:jconsole $pid
3.6.2 大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组,大对象对虚拟机的内存分配来说就是一个坏消息(替java虚拟机抱怨一句,比遇到一个大对象更坏的消息是遇到一群朝生夕灭的短命大对象,写程序的时候应当避免),经常楚翔大对象容易导致内存还有不少空间是就提前出发垃圾收集以获取足够连续的空间来安置他们
虚拟机提供了一个-XX:PretentureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目标是避免在Eden区及两个Survivor区之间发生大量的内存复制,新生代采用复制算法收集内存。
PretentureSizeThreshold参数只对Serial和ParNew两款收集器有效
3.6.3 长期存活的对象将进入老年代
为了表示那些对象放在新生代,那些对象放在老年代中,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次monor GC后任然存活,并且能被survivor容纳的话,将被移到Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),将会被晋升到老年代中。对老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置
尝试:-XX:MaxTenuringThreshold=1 和-XX:MaxTenuringThreshold=15
3.6.4 动态对象年龄判定
如果再Survivor空间中相同年龄所有对象大小的的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无语等到MaxTenuringThreshold中的要求的年龄
3.6.5 空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC 可以确保安全,如果不成立,则
,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是都大于历次晋升到老年代对象的平均大小。
如果大于,将尝试着进行一次Minor GC,尽量这次Minor-GC是有风险的;
如果小于,或者HandlePromotionFailire设置不允许毛线,那这时也要改为进行一次Full GC
当出现在Minor GC后任然有大量的对象存活的情况下,最极端的情况就是内存回收后新生代中所有对象都存活,就需要老年代进行分配分担,把survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的。所以只好取之前每一次回收晋升到老年代对象容量的平均值作为经验值,与老年代的剩余空间进行比较,决定是都进行Full GC让老年代腾出更多的空间。
取平均值进行比较其实仍然是一种动态概率的手段,如果Minor GC 存活后的对象激增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的。但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC 过于频繁
在JDK 1.6后,规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则进行Full FC
第四章:虚拟机性能监控与故障处理工具
4.1
4.2 JDK的命令行工具
-Dcom.sunmanagement.jmxremote 开启JMX管理功能
在1.6的虚拟机上,Sun JDK监控和故障处理工具
名称 主要作用
jps JVM proces Status Tool 显示指定系统内所有的HotSpot虚拟机进程
jstat JVM Statistics Monitoring Tool,用于手机HotSpot虚拟机各方面的运行数据
jinfo Configuration Info for Java 显示虚拟机配置信息
jmap Memory Map for Java,生成虚拟机的内存转储快照(headdump 文件)
jhat JVM Heap Dump Browser 用户分析headdump文件,它会简历一个HTTP、Html服务器,让用户可以在浏览器上查看分析结果
jstack Stack Trace For Java 显示虚拟机的线程快照
4.3 JDK的可视化工具
两个强大的可视化工具:JConsole和VisualVM
第5章 调优案例分析与实战
方案一、对于用户交互性强,对停顿时间敏感的系统,可以给java虚拟机分配超大堆的前提是有把我应用程序的Full GC频率控制得足够低,至少要低到不会影响用户使用,譬如十几小时乃至一天才出现一次Full GC
控制Full GC频率的关键是看应用中绝大多数对象能够符合“朝生夕灭”的原则。即多数对象的生存时间不应太长,尤其是不能有成批量的,长生存的时间的大对象产生,这样才能保障老年代空间的稳定。
在大多数网站形式的应用里,主要对象的生存周期都应该是请求级或者页面级的,会话级全局级的长生命对象相对很少。只要代码写得合理,应当都能实现在超大堆中正常使用而没有频繁的Full GC甚至没有Full GC
缺点:1、内存回收导致的长时间停顿
2、现阶段,64位JDK的性能测试结果普遍低于32位JDK
3、 需要保证程序足够稳定,因为这种应用要是产生堆溢出几乎就无法产生堆转储快照(因为产生十几个G乃至更大的Dump文件)哪怕产生了快照也几乎无法分析。
4、相同程序在64位JDK消耗的内存一般比32位JDK大,这是由于指针膨胀,以及数据类型对齐补白等因素导致
方案二、一台物理机器上启动多个应用服务器进程,每个服务器进程分配不同端口,然后再前段搭建一个负载均衡器,以反向代理的方式来分配访问请求。
在一台物理机器上简历逻辑集群的目的仅仅是为了尽可能利用硬件资源,斌不需要关心状态保留、热转移之类的高可用性需求,也不需要保证每个虚拟机进程有绝对准确的负载均衡,因此使用无Session复制的亲合式集群是一个相当不错的选择。我们仅仅需要保障集群具备亲和性,也就是均衡按照一定的规则算法(一般根据SessionID分配)将一个固定的用户请求永远分配到固定的一个集群节点进行处理即可,这样程序开发阶段的基本不用为集群环境做特别的考虑了。
缺点:
1、尽量避免节点竞争全局资源、最典型的就是磁盘竞争,各个节点如果同时访问某个磁盘文件(尤其是鬓发操作荣出现问题),很容易导致I/O异常
2、很难最高效地利用某些资源池,譬如链接池子,一般都是在各个节点建立自己独立的连接池,这样有可能导致一些节点池满了而另一些节点仍有较多空余。尽管空余使用集中式JNDI,但这个有一定复杂性并且可能带来额外的性能开销
3、各个节点任然不可避免受到32位内存限制,win32 2G内存,linux/unix 4G限制
4、大量使用本地缓存(如大量使用K/V缓存)应用,在逻辑集群中会造成较大的内训浪费,因为每个逻辑节点都有一份缓存,这个时候可以考虑把本地缓存改为集中式缓存。
最终方案:
部署方案是:建立5个32位JDK的逻辑集群,每个进程按2G内存计算(其中堆固定为1.5G),占用了10G内存,另外建立一个Apache服务作为前端代理访问门户。考虑到用户对影响速度比较关心。并且文档服务的主要压力集中在磁盘和内存访问,CPU资源敏感度较低,因此改为CMS收集器进行垃圾回收部署方式调整后。效果很好
5.2.2 集群间同步导致的内存溢出
5.3.2 堆外内存导致的溢出错误
系统使用逆向AJAX技术(也称为Comet或者Server Side Push),选用CometD 1.1.1 作为服务器端推送框架
直接内存不能像新生代或者老年代那样,发现空间不足就通知收集器进行垃圾回收,它只能等待老年代满了之后Full GC
大量的NIO操作需要使用到Direct Memory
1、Direct Memory:可以通过-XX:MaxDirectMemorySize调整大小内存不足抛出outOfMemoryError或者
2、线程堆栈:可通过-Xss调整大小,内存不足时抛出StackOverFlowError(纵向无法分配,即无法分配新的栈帧)
3、Socket缓存区:每个Socket连接都Reseive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的话这块内存占用也比较客观。如果无法分配,则可能抛出IOEXCEPTION:too many open files
4、JNI代码,如果代码中使用JNI调用本地库,那本地库的内存也不在堆中。
5、虚拟机核GC:虚拟机、GC的代码执行也需要一定的内存
5.2.4 外部命令导致系统放慢
一个数字校园的应用系统
中间件为GlassFish
压力测试,发现求情响应时间比较慢,通过操作系统的mpstat工具发现CPU使用率很高,
找到答案:每个用户请求的处理都需要执行一个外部的Shell脚本来获得系统的一些信息。执行这个shell脚本是通过java的Runtime.getRuntime().exec()方法来调用的。
这种方式在jaba虚拟机中是非常消耗资源的操作,即使外部命令本身能很快执行完毕,频繁调用时穿件进程的开销非常可观。虚拟机执行这个命令的过程是:首先克隆一个和当前虚拟机拥有一样环境变量的进程,再用这个新的进程去执行外部命令,最后再退出这个进程,如果频繁执行这个操作,系统的消耗会很大,
5.2.5 服务器JVM进程奔溃
解决方法:通知OA门户方修复无法使用的集成接口,并将一部调用改为生产者/消费者模式的消息队列实现后,系统恢复正常
5.2.6 不恰当数据结构导致内存占用过大
内存配置为 -Xms4g -Xmx8g -Xmnlg,使用ParNew+CMS的收集器组合,平时堆外服务的Minor GC 30ms,可以接受 ,但业务上需要每10分钟加载一个80M的数据文件到内存进行数据分析,这些数据形成内纯种超过100万个HashMap Entry 在这段时间里面Minor GC就会造成500
毫秒停顿,对于这个停顿时间接受不了。
这个案例:发现平时的Minor GC时间很短,原因是新生代大部分对象都是可清楚的,在Minor GC之后Eden和Survivor基本上处于完全空闲的状态。而在分析数据文件期间,800MB的Eden空间很快就被填满引发GC,但Minor GC之后,新生代中绝大部分对象依然是存活的。,我们知道ParNew收集器是使用的是复制法,这个算法的搞笑事简历在大部分对象都朝生夕灭的特性上,如果存活对象过多把这些对象复制到Survivor并维持这些对象的引用的正确称为一种承重的负担,因此导致GC暂停时间明显变长。如果不修改程序仅从GC调优角度去解决这个问题,可以考虑将Survivor空间去掉(加入参数 -XX:SurvivorRatio=65536、-XX:MaxTenuringThreshold=0或者-XX:+AlwaysTenure),让新声带中存活的对象在第一次Minor GC后立即进入老年代,等到Major GC的时候再清理它们
第五部分 高效的并发
第12章 java内存模型与线程
衡量一个服务型能的高低好坏,每秒事务处理数(Transactions Per Second TPS)是重要的,它代表一秒服务器端响应的请求总数,而tps最重要的指标之一,它代表一秒内服务端平均能响应的请求总数,程序线程并发协调有条不紊,效率自然就会越高;
物理计算机中的并发问题,物理机遇到的并发问题与虚拟机中的情况有不少相似之处,物理机对并发的处理方案对于虚拟机的实现也有相当大的参考意义。
12.3.5 原子性,可见性和有序性
介绍完java内存模型的相关操作和原则,回顾下java模型的特征。java内存模型是围绕着在并发过程中如何处理原子性,可见性和有序性三个特征来建立,
1、原子性(Atomicity):由java内存模型来直接保证的原子性变量操作包括read,load、assign、use,store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的,例外是long和double的飞原子性协定。满足更大范围的原子性保证,java内存模型还提供了lock和unlock操作来满足这种需求,synchronized
2、可见性 (Visibility):java内存模型是通过在变量修改后将新值同步回住内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能理解同步主内存,以及每次使用前立刻从主内存刷新。因此可以说volatile保证了多线程操作时变量的可见性
3、有序性(Ordering):Synchronized 表示“同一个变量在同一个时刻只允许一条线程对其进行lock操作”
12.3.6先行发生原则
先行发生原则(happens-before)的原则,这个原则非常重要,它是判断数据是否在竞争、线程是都安全的重要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作之间是否可能存在冲突的所有问题。
具体:先行发生时java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等
12.4 Java与线程
12.4.1 线程的实现
实现线程主要有3中方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。
1、内核线程实现:Kernel-Level Thread KLT)就是直接有操作系统内核(Kernel 下称内核)支持的线程。
2、用户线程实现:一个线程只要不是内核线程,就可以认为是用户线程(Used Thread UT)
12.4.2 java线程调度
线程调度室指系统为线程分配处理器的使用权的过程,主要调度方式有两种,分别是协同式线程调度(cooperative threading scheduling)和抢占式线程调度 Preemptive Threads scheduling,
如果使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程。协同式多线程最大的好处是现实简单,而且线程要把自己的事情干完才会进行线程切换,切换操作的最大好处就是现实简单,而且由于线程要把自己的事情干完才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步问题。缺点:线程执行时间不可控制;
如果是抢占式调度的多线程系统,那么每个线程由系统来分配执行时间,线程的切换不由线程本身决定(在java中,Thread。yeild())可以让出执行时间,但是要获取执行的时间的话,线程本身是没有办法的。在这种实现的线程调度方式下,线程的执行是时间是系统可控的,也不会有一个线程导致进程的堵塞问题。
java使用的线程调度方式是抢占式调度。
第13章 线程安全与锁优化
1、互斥同步,面临线程阻塞和唤醒所带来的性能问题,这种称为阻塞同步(blocking Synchronization),这是一种悲观的并发策略,悲观主要体现在只要不去做正确的同步措施(如加锁),那就肯定会出现问题,无论数据是否真的出现竞争,它都要进行加锁,典型的synchronized ReetrantLock 绑定多条件
2、非阻塞同步:基于冲突检测的乐观并发策略,通俗说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了。如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断的重试,知道成功为止),这种乐观的鬓发策略的许多实现都不需要把线程挂起,因此这种同步叫做非阻塞同步(non-blocking Synchronization)
通过以下方式实现:
1、测试设置(Test-and-Set)
2、获取并增加(Fetch-and-Increment)
3、交换(swap)
4、比较并交换(Compare-and-Swap,下文称CAS)。
5、加载链接/条件存储(load-linked/store-conditional LL/SC)
13.3锁优化
适应性自旋(Adaptive Spinning)
锁消除(Lock Eliminination)
锁粗化Lock Coarsening
轻量级锁(LightWeight Locking)
偏向锁 (Biased Locking)等