浅入JVM一--JVM内存管理
1 jvm内存模型
?
??? jvm规范将jvm用到的内存分成如下几个不同的功能区:
程序计数器虚拟机栈本地方法栈方法区(运行时常量池)堆??? 当然,这只是逻辑上的划分,不同的虚拟机在实现时可能会略有不同,例如有些虚拟机将虚拟机栈和本地方法栈合在了一起(Hotspot就是)。因为这些区域在java程序运行时的作用不同,因此每个区块可能发生的问题也不同,下表描述了这些区域的作用和可能发生的问题:
区域作用作用域可能的问题HotSpot虚拟机中控制其大小的参数程序计数器线程当前所执行的字节码的行号指示器线程私有无虚拟机栈存储java方法执行时的局部变量表、操作栈、动态链接和方法出口等信息其中局部变量表存放了编译器可知的各种基本数据类型、对象引用,它的大小在编译器确定
线程私有栈深度大于虚拟机允许的最大深度时候,抛出StackOverflowError
动态扩展时无法申请足够的内存时将抛出OutOfMemoryError
-Xss10M本地方法栈其作用与虚拟栈类似,只是它是在虚拟机执行Native方法时起作用线程私有StackOverflowError和OutOfMemoryError-Xss10M HotSpot虚拟机两个栈合二为一-Xms10M
-Xmx20M
-Xmn10M,新生代用10M
方法区存放被虚拟机加载的类信息、常量、静态变量和jti编译器编译的代码等;jvm规范上,方法区是堆的一个逻辑区域,并且Hotspot虚拟机还把它称为永久代(与新生代、老生代对应)。线程共享OutOfMemoryError-XX:PermSize=10M
-XX:MaxPermSize=20M
运行时常量池方法区的一部分,存放编译期生成的各种字面量和符号引用,以及运行时生成的新的常量;线程共享OutOfMemoryError-XX:PermSize=10M
-XX:MaxPermSize=20M?
??? 除了上述虚拟机运行时的数据区外,jkd1.4后包含的NIO还会用到虚拟机外的内存,在块区域叫做直接内存(本机直接内存),具体的实现原理可以参考NIO。虽然,它不在虚拟机内存之列,但是因为受到本机总内存的限制,所以也有可能产生OutOfMemoryError.
?
2 内存分配策略
?
??? 虚拟机的堆内存分配策略根据使用的垃圾收集器的不同(下文再讲)可能会有不同,但是如下几条原则应该说是几条最基本的原则:
对象优先在Eden区分配:当Eden不够在为新的对象分配时,会触发一次Minor GC,如果GC之后还是Eden还是没有足够的空间,则可能直接将对象分配到老生代去;大对象直接进入老年代:避免因为需要进行Minor GC而在Eden区和Survivor区中复制;对于超大的对象往往Minor GC之后Eden区还是没有足够的空间或者说在Minor GC时,survivor区空间小于大对象,这时大对象还是会被移到老年代,所以对于大对象可以直接进入老年代;-XX:PretenureSizeThreshold参数设置大对象的阀值,大于这个值的对象直接进入老年代;长期存活的对象进入老年代:对象在Eden区出生,经过一次Minor GC后仍然存活并且能被Survivor容纳(如果不能被Survivor容纳则会被直接移到老年代),它的年龄就增加1岁,当它的年龄增加到一定大小时(默认为15岁),则会被移到老年代中。可以用-XX:MaxTenuringThreshod=n参数设置这个年龄的阀值。动态对象年龄判断:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,则年龄大于等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshod要求的年龄;空间分配担保:每次进行Minor GC之前都会先进行检测之前每次Minor GC晋升到老年代的平均大小是否大于当前老年代的剩余空间大小,如果是,则直接改为进行一次Full GC;否则,则看HandlePromotionFailure设置是否允许空间分配担保失败,如果允许失败,那么只进行Minor GC,否则还是要进行一次Full GC;?
3 垃圾回收算法与垃圾回收器
?
??? 相比内存分配,垃圾回收是一个更加复杂的问题,处理不当也是更危险的问题。研究垃圾回收问题往往需要研究以下几个问题:
哪些内存需要回收什么时候回收如何回收3.1 垃圾回收处理的区域
?
??? 第一节在介绍jvm对内存区域的划分时,就介绍了每个区域的作用域和可能发生的问题。除了程序计数器之外,其他的区域都有可能发生OutOfMemoryError(Oom),那是不是所有会发生Oom问题的区域都需要进行垃圾回收呢?答案是否定的,对于虚拟机栈和本地方法栈因为下面的两个原因其实不需要额外的去进行复杂的垃圾回收的:
其内存使用都是线程级别的,随着线程的结束所使用的内存也自然的会被回收;栈中的内存基本都是在编译期就已知的,现代jvm在栈中发生Oom的可能性不大;??? 而剩下的堆和方法区(包括运行时常量池)才是垃圾回收的重点区域,另外因为方法区存在的主要是类信息和运行时常量,通常这两部分可回收的垃圾较少,并且想要回收类信息的条件还非常的苛刻。所以说堆内存的回收才是垃圾回收关注的重点的重点,当然堆中垃圾回收的机制、算法也都适用方法区(jvm规范上,方法区本身就是堆的一个逻辑部分)。
?
3.2 垃圾回收的内容
?
??? 因为堆是垃圾回收的重点区域,所以主要已堆上存储的对象实例作为垃圾回收讨论的对象。那么,垃圾回收到底回收些什么呢?答案就是堆中已“死”的对象。如何判断对象已“死”呢?主要有两种方法:
引用计数法:对象有N个地方引用了,其计数就是N,如果计数为0,则说明是可回收的对象;引用计数法实现简单,效率也高,但它无法解决循环引用的问题;根搜索算法:已"GC Root"对象为起点,向下建立引用链,不在任何引用链中的对象即是可回收的对象。根搜索算法能解决循环依赖问题。??? 任何可以被回收的对象在真正被回收之前,都会调用其finalize()方法,如果对象在finalize()中又将自己和外部的引用进行了关联,则该对象又称为不可回收的对象了。但是一个对象的finalize()方法只会执行一次,下次再被标记为可回收时,将不再执行,所以就会被回收了。所有对象的finalize()方法都是在一个叫F-QUEUE的队列中执行的。finalize()方法一定会被触发,但不保证会被执行完,这是为了防止某些对象的finalize()方法执行出问题时,会拖垮整个F-QUEUE。
?
3.3 垃圾回收算法
?
??? 知道了哪些对象是可回收的垃圾,下一步就是对这些对象进行回收。目前,回收的算法主要有以下几种:
标记--清除算法:先按3.2节介绍的方法标记出需要回收的对象,然后统一清除掉可以回收的对象的内存;这种方法效率不高,并且因为只是简单的将垃圾对象占用的内存清除,因此会产生很多不连续的内存碎片,这样在需要为大对象分配内存时就会出现堆的总内存还剩很多,但是没有连续的内存分给这个大对象而导致又一次垃圾回收甚至是是导致Omm的问题。复制算法:将可用内存划分为一块较大的Eden块和两块相同大小的较小的survivor块,每次适用时只使用Eden块和一块survivor块,但进行垃圾回收时,先将Eden块和使用的survivor块中有用的对象复制到没有使用的survivor块中,再整体清空Eden块和先前使用的survivor块,接下去的运行将使用Eden块和第二块survivor块。当空闲的survivor块不够存放所有的有用的对象时,就需要依赖其他内存(老生代)进行内存分配担保,对象直接进入老生代。标记--整理算法:标记阶段与标记--清除算法相同,只是清理阶段不是简单的清除可回收的对象的内存区域,而是将所有有用的对象都移到内存区的前面来,形成连续的内存空间,而整体清理剩下的内存空间。3.4 垃圾收集器
?
??? 垃圾收集器只是垃圾搜索算法的一个实现,只是不同的收集器可能在追求的目标上和实现方式上有所不同而已。不同的JVM带有的垃圾收集器不尽相同,1.6 Update 22版的HotSpot虚拟机带有的垃圾收集器如下:
名称使用的算法作用的区域实现方式追求的目标可共用的收集器Serial复制算法新生代单线程用户响应优先CMSParNew复制算法新生代多线程用户响应优先CMS,Serial OldParallel Scavenge复制算法新生代多线程吞吐量优先Serial Old,Parallel OldCMS标记-清除算法老生代多线程用户响应优先Serial,ParNewSerial Old标记-整理算法老生代单线程用户响应优先Serial,ParNew,Parallel ScavengeParallel Old标记-整理算法老生代多线程用户响应优先Parallel Scavenge?
一般虚拟机会在启动时,为新生代和老生代设置不同的默认的垃圾收集器,我们可以根据硬件的情况、系统的实现特性和追求的目标在不同的收集器组合中选择最合适的组合。
?
4 总结
?
??? 关于jvm内存分配和回收,看起来更多的是理论上内容,没有一些实际的技术。为什么要去学习和研究呢?应该有下面3个原因:
为系统开发做指导:知道了jvm会如何处理对象,就可以知道什么杨的代码编写方式可能会导致性能问题:例如批量产生大量长时间存在内存的大对象可能就会频繁触发Full GC而导致性能问题;知道了这些之后,就可以为解决问题设计更好的方案,而非仅仅是解决问题而已;优化系统性能,特别是当垃圾回收成为系统达到更高并发量的瓶颈时:优化可以考虑从以下几个方面入手: