JVM: 如何避免出现GC失败
JVM是Java语言可以跨平台、保持高发展的根本,没有了JVM. Java语言将失去运行环,境。针对Java程序的性能优化一定不可能避免针对JVM的调优,随着JVM的不断发展,我们的应对措施也在不断地跟随、变化,内存的使用逐渐变得越来越复杂。所有高级语言都需要垃圾回收机制的保护,所以GC就是这么重要。
JVM标准结构
类的加载机制
一:装载(load)
由ClassLoader负责加载; (ClassNotFoundException)
二:链接(Link)
校验(verify)、准备(Prepare)、初始化静态变量赋默认值;
(NoClassDefFoundError)
三:初始化
执行静态初始化代码、赋值静态变量。
ClassLoader
一:BootStrap ClassLoader
由Sun用C++实现此类,JDK启动时负责加载jre/lib/rt.jar里的所有
class,及java规范中定义的接口及实现;
二:Extension ClassLoader
JVM用于加载扩展功能的jar包,如DNS.jar
Jdk中的名称:sun.misc.Launcher$ExtClassLoader
三:System ClassLoader
加载启动参数中指定ClassPath的Jar包
Jdk中的名称: sun.misc.Launcher$AppClassLoader
一般的程序使用的都是AppClassLoader
四:自定义ClassLoader
用户也可以自定ClassLoader用于加载其他路径的Jar如网络上加载
类的执行机制
虽然类已加载成功且静态属性及实例对象皆以创建,但是,执行静态方法或者实例方法时仍需要对JVM字节码进行处理。
JVM有以下三种执行方式:
(1):解释执行
(2):编译执行
(3):反射执行
解释执行
采用经典的冯诺依曼FDX循环方式,即获取下一条指令,解码并分派,然后执行。
解释执行的优点:简单、占资源少、启动速度快
解释执行的缺点:效率低
编译执行
JDK将字节码编译成机器码,编译在运行时进行,故称作JIT编译器(Just-in-time)•策略:对执行频率频繁的代码使用编译执行,对执行频率不高的仍采用解释执行
编译执行的两种方式:
(1)ClinetCompiler:又称C1较轻量级,java -client
(2)ServerCompiler: 又称C2 较重量级,java -server
Client Compiler
占用内存较少,适合于桌面应用•优化方法:
一:方法内联,将调用的方法指令直接植入到当前方法中;
二:去虚拟化,针对只有一个实现类的方法;
三:消除冗余,在编译时进行代码清理;
Server Compiler
C2采用了大量优化技巧,占内存较多,适合于服务器应用;
优化方法:标量替换、栈上分配、同步削除;
默认当CPU个数超过两个且内存超过2G自动采用Server模式,否则为Client模式,但在32位的windows上始终都是Client模式,可通过java-server强制使用Server模式,或者java-client强制使用client模式。
JVM内存管理
JVM内存结构图:
JVM方法区
用于存放类信息、类的属性、方法等信息。又称为持久代PermanentGeneration,默认最小值16MB,最大值64MB。持久代在一定条件下也会被GC(垃圾回收),当空间不够时,会抛出OutOfMemory错误信息。可通过-XX:PermSize -XX:MaxPermSize来指定最小最大值。
PC寄存器与方法栈每个线程均会创建自己的PC寄存器和方法栈。PC寄存器存放每条指令的地址。方法栈为线程私有,当方法执行完毕时,其栈帧所用内存会自动被回收。当栈空间不足时会抛出StackOverflowError,可通过设置-Xss来指定其大小,以避免空间不够用。
JVM堆
堆用于存储对象实例和数组值。在32位机上最大2GB,在64位机则无限制可通过-Xms设置最小值,默认为物理内存的1/64但小于1GG通过-Xmx设置最大值,默认为物理内存的1/4但小于1GB.默认当空余堆内存小于40%时,JVM会增大Heap到最大值,可通过-XX:MinHeapFreeRatio=来设定这个比例。当空余空间大于70%时,JVM会减小到最小值,可通过-XX:MaxHeapFreeRatio=来设定这个比例。为避免运行时JVM频繁调整Heap大小,通常将-Xms与-Xmx设成相同值。
JVM堆结构
新生代 new generation
大多数情况下Java创建的对象都从新生代分配。
新生代由Eden Space和两块大小相同的Survivor Space(又称S0和S1或者From和To)构成。可通过-Xmn指定新生代的大小。
-XX:SurvivorRatio来调整Eden和Survivor的大小比例。
旧生代 Old Generation
用于存放在新生代中多次回收仍然存活的对象。
有两种情况新建的对象会直接在老生代分配:
通过设置-XX:PretenureSizeThreshold,当新对象大小超过设定值时直接在老生代分配,但是当新生代采用Parallel Scavenge GC时该设置无效。另一种是大的数组对象,且数组中无引用外部对象。
旧生代的大小为-Xmx减去-Xmn的值。
内存回收-GC
所谓内存回收就是我们所熟知的GC(Garbage Collection)垃圾回收。
JVM通过GC来回收堆和方法区的内存,GC的原理为首先找到内存中不再被引用的对象,然后回收。
通常采用收集器的方式来实现GC,主要的收集器有引用计数收集器和跟踪收集器。
引用计数收集器
引用计数收集器通过记录对象的引用次数,当次数为零时,可进行回收。
但当出现循环引用时该收集方法则无法回收:
所以引用计数方式不适合面向对象这种有复杂引用关系的语言,SunJDK在实现GC时也未使用过此方式。
跟踪收集器
跟踪收集器采用集中式的管理方式,全局记录数据的引用状态。基于一定的条件触发例如:空间不足,定时。
执行时需要从根对象来扫描对象间的引用关系,这会造成应用的暂停。
主要实现算法有:赋值(Copying)、标记-清除(Mark-Sweep)、标记-压缩(Mark-Compact)。
复制算法-Copying
复制算法采用的方式为从根集合扫描出存活的对象,并将存活的对象复制到一块新的完全未使用的空间。
当存活对象少时,Copying算法是很高效的,其代价是需要一块新的存储区和进行对象移动。
标记-清除 Mark-Sweep
标记-清除采用的方式为从根集合开始扫描,对存活的对象进行标记,标记完成后,对未被标记的对象进行扫描并回收。
Mark-Sweep不需进行对象移动,且仅对不存活的对象进行处理。在空间存活对象较多的情况下较为高效,但会形成内存碎片。
标记-压缩 Mark-Compact
标记-压缩与标记-清除的不同在于,对回收的内存空间进行压缩,即所有存活的对象都往左端空闲的空间进行移动,并更新引用对象的指针。
该算法的好处在于不产生内存碎片,减少OutOfMemory的风险,缺点是移动对象的成本较高。
新生代可用GC
新生代的大多数对象存活时间较短,故采用了Copying算法。
新生代的GC又叫Minor GC。
Minor GC有三种回收方式:串行GC(Serial GC)、并行回收GC(Parallel Scavenge)、并行GC(ParNew)。
串行GC(Serial GC)
串行GC从根集合扫描存活的对象。JVM认为根对象为当前线程栈上引用的对象、常量、静态变量、传到本地方法还未被释放的引用。
串行GC扫描存活的对象,然后将存活的对象复制到S0或S1中(S0与S1同时只能有一个被使用,另一个为空)。
为了避免扫描过程中引用关系的改变,JDK采用了暂停应用的方式。
通常只有经过几次MinorGC仍存活的对象才放入旧生代中。该次数可通过-XX:MaxTenuringThreshold设置(只在串行与ParNew方式下生效默认值为15)。但该项并不是唯一的规则。串行和ParNew在每次GC后计算可存活的次数,规则为累计每个age对象占用的内存,如果累计超过SurvivorSpace的一半,则以age为准,否则,以MaxTenuringThreshold为准。
如果ToSpace空间满则直接转入旧生代。
SerialGC采用单线程方式,适用于单CPU,对暂停时间要求不高的应用,也是Client级别(CPU小于2个或物理内存小于2GB,或32位Windows机器上)的GC方式,可通过-XX:+UseSerialGC来强制执行。
并行回收GC(Parallel Scavenge)
当采用PSGC时,默认ServivorRatio比使用-XX:InitialSurvivorRatio来设置,如果不设置该项,则默认为6:1。
一般情况下PSGC会自动调整Survivor比例,可通过-XX:-UseAdaptiveSizePolicy来固定Survivor比例。
PSGC不是根据-XX:PretenureSizeThreshold来决定对象是否直接在旧生代分配,而是当,EdenSpace空间不够的情况下,而此对象的大小大于等于EdenSpace一半的大小,则直接在旧生代分配(该情况应该比较少见)。
并行回收适合多CPU、对暂停时间要求较短的应用。是Server级别的默认GC方式,也可通过-XX:+UseParallelGC来强制指定。
默认的执行线程数与CPU的核数相同,但当CPU核数大于8时,其计算公式为:3+(CPU核数*5)/8,也可通过-XX:ParallelGCThreads来强制指定线程数。
并行GC(ParNew)
在SurvivorRatio分配上与串行GC的策略一样
并行GC的不同之处在于须配合旧生代的CMS GC使用,由于CMS是并发进行的,若此时发生MinorGC需要做相应的处理。
当采用CMSGC时,新生代默认采用并行GC,也可使用+XX:+UseParNewGC来强制指定。
旧生代与持久代GC
JDK提供三种旧生代GC方式:串行、并行、并发。
串行基于Mark-Sweep-Compact实现,首先从根集合对象扫描,然后按照三色着色算法标识对象;扫描未标识的对象并将其回收;执行滑动压缩,将存活的对象向旧生代的开始处滑动,最后留出一块连续的到结尾处的空间。
旧生代串行GC
串行GC是Client级别机器和32位Windows机器上采用的方式,可通过-XX:+UseSerialGC来强制指定;
串行的整个执行过程需要暂停应用,且是单线程进行,通常会花费很长时间,可通过-XX:+PrintGCApplicationStoppedTime来查看GC造成的应用停止时间。
旧生代并行Compacting
旧生代的并行Compacting通过以下三步来完成:
首先将旧生代分为并行线程个数个区regions并行地进行着色;
然后从左边扫描regions,直到找到第一个值得进行压缩的region,并将此region左边的region作为高密区(dense prefix),对这些区域不进行回收,继续向右扫描,找到需要进行压缩的源region和目标region。此过程为单线程进行。
最后基于regions上的分析,并行地进行压缩和region回收。
较之串行并行大部分时间是并行进行,故造成的应用暂停时间会缩短。
并行GC是Server级别机器上默认采用的GC方式,可通过-XX:+UseParallelOldGC指定使用ParallelCompacting, -XX:+UseParallelGC指定Parallel-Mark-Sweep。
并发(CMS:Concurrent Mark-Sweep)
CMS主要采用Mark-Sweep方式,因为会产生较多内存碎片,故不能用bump-the-pointer分配,而采用free list分配。
Free List方式导致MinorGC速度下降。
CMSGC的回收过程为以下四步:
1.第一次标记(Initial Making)
该步骤暂停整个应用,从根集合到旧生代扫描可直接访问的对象,并着色,将着色的对象用一个外部的bit数组进行记录。
2.并发标记(Concurrent Making)
该步骤恢复所有应用,对着色过的对象进行轮询,标记这些对象可访问的对象。
为解决MinorGC造成的旧生代引用关系的改变,CMS采用Mod Union Table记录每次MinorGC后修改的Card信息。(旧生代的根对象)
采用Card Table记录与应用并发时的dirty对象
3.重新标记(Final Marking)
该步骤暂停整个应用,主要任务是对ConcurrentSweeping时新建的对象及Mod Union Table和Card Table中的对象进行扫描,并重新着色。
4.并发收集(Concurrent Sweeping)
恢复所有应用线程,将没有标记的对象回收,回收时,CMS会将相邻的两个区域合并成一个新的大的区域。
总结:
进群:721985904 即可获得《JVM&GC》整本电子书获取方式
进群:721985904 即可获得《JVM&GC》整本电子书获取方式