在前天我在公司内部做了一个分享,好久没有更新博客,主要是工作太忙,没有时间去总结,这篇博客也是这次分享时内容。
对于java堆内存设置,首先需要对java内存解构有所了解,对于linux平台,java的底层又是c写的,因此java内存结构又是在c的内存结构之上,所以我准备从c的内存结构讲起。
对于java的特点,如跨平台、支持多线程、自动内存回收等,我们也能分析到java到底怎么去运行,比如,如果需要跨平台,意味着java语言编译后的指令必须与平台无关,然后由一个解释执行引擎把平台无关的指令翻译成平台相关的指令,再由cpu执行。对于支持多线程,java必须有调度的功能,而且必须为每个线程维护一个pc信息(类似于系统进程信息一样),保存当前执行指令位置等相关信息。对于内存回收,java必须有自己的内存回收机制和策略,而内存回收对于每个java使用者来说,应该了解怎样去控制,针对业务调优。
首先我们了解一下c的内存结构。
以上几个块就是c运行主要的几个段,画的简单,还有很多其它的段,如rodata等,而且数量也有多个的,这里不作介绍,c在加载到内存时,text、data、bss这几个段在编译时,内存大小都已确定,运行时不能调整,唯一由程序员控制的内存区域就是堆(heap)段,而栈(stack)是运时保存局部内量,和跳转现场的区域,自身没有限制,但操作系统会对这个栈大小做限制,如linux限制在10M,freebsd限制512M,而c的运行当前指令由cpu寄存器的cs和pc决定,函数跳转需要的堆栈由cpu寄存器SP(栈顶),BP(栈底)去实现,刚才提到java底层也是c写的,而java底层也是这样的结构,因此java所有的数据都保存在底层结构的heap段中(PS,其实java结构我觉得也有点模拟底层的结构)。
然后我们再看看java的内存结构。
对于java的运行,需要以完成以下几步:1,class回载到内存(class loader)。2,数据区域,保存运行数据(runtime data area)。3,执行引擎,解析翻译执行指令,这个也是体现各个jdk的关键的地方(execution engine)。4,底层本地库的调用,在linux上就是so结尾的库文件(native interface)。
对于数据区域,必须有保存class的方法区(method area),保存对象的堆(heap),自身的栈(java stack),底层库调时的栈(native method stack),保证线程信息pc(program counter register)。从线程安全的角度,只能有方法区(method)和堆(heap)是可以线程共享的,其它的都是私有独立的。堆的设置就关系到内存回在收,也是这篇博客的重点。
在java中,对堆(heap)又进一步作了划分,分为新生代(二个s区一个E区),老生代(old区),永久区(perm),对于内存的分段的优点我觉得主要有二点,1,各个对象生命周期不一样,有些对象需要永久保存在内存,有些对象是临时使用的。不能对等对待。2,有些区域需要保护,而且只读,避免溢出覆盖,影响稳定性,最明显就是字节码区域和数据区域不能混合。对于内存回收,要为小回收(回收新生代)和大回收full gc(回收老生代和永久区)
首先是新生代,大小参数如-XX:MaxNewSize=2g -XX:NewSize=2g来指定,一个是指定是最大空间,一个是初始空间,而S区和E的比例则由-XX:SurvivorRatio来指定时,计算是要考虑S的二个,如-XX:SurvivorRatio=1时,意味着二个S的大小是整个新生代的2/3,不作任何配置,默认是单线程内存回收,也可以由-XX:+UseParNewGC指定多个线程回收,回收的线程数量由-XX:ParallelGCThreads=8(指定8个线程)设置。
对于永久区,配置如-XX:PermSize=512M -XX:MaxPermSize=512M,永久区的空间只有一个用途,保存方法和静态变量等,一般情况下,运行时,永久区的大小不会发生变化,通常大小我设置为所有jar包的总和的3/2,通过jstat查看时,保证比例为70%-80%就行了,但是有些情况,如有些java框架会动态生类,永久区的大小会不停的增涨,而这种框架有一种特点,他自身会不停的调用system.gc()触发full gc,上面的方法就不能用了,必须配置大一点的永久区,同时关闭system.gc()()添加-XX:+DisableExplicitGC。
对于老生代的大小,不需要去指定,剩余空间就是老生代,对内存回收默认是单线程,可以指定多线程-XX:+UseParallelOldGC,线程数量也是ParallelGCThreads指定,对于老生代回收,回收的方法不像新生使用的copy的方式,它使用的是mark的方式,而且空间大小通常比新生代大,即使是多线程回收,回收的时间也是比较长,在一些实时交互的程序,回收时间过长比较影响业务,java1.6中,提供了一种新的方法remark回收老生代-XX:+UseConcMarkSweepGC,它使用二mark的方式回收,每次mark的时间都非常短,在实时交互的程序内存回收时,效果比较好,也可以使用多线程如-XX:ParallelCMSThreads=8(8个线程),使用remark时,另一个参数非常重要-XX:CMSInitiatingOccupancyFraction=70,指定达到多大的比例触发回收,这通常要结合业务特点。
怎样的回收方式的合理的呢,我总结一下。1,新生代的回收,也就是小回收,一次回收的时间不能过长,因为新生代的回收比较频繁,只能通过控制单次回收的时间来保障性能,如下是一个合理的配置,约8秒触发一次小gc,每次的时间约为22毫秒,22毫秒的中断,我认为还是可以接受的:
但是有的时候,新生代单次回收的时间不长,但时会出现连续回收的情况,这种方式也是不合理的,通常发生E区和S区比例不对和-XX:MaxTenuringThreshold=设置不合理,如下就是连续回收的情况:
对于老生代和新生代,触发的都是大回收,应该尽量控制发生的次数,具体策略如减少从新生代拷贝到老生代的策略,如设置-XX:MaxTenuringThreshold=,让回收时新生对象在新生代多呆一段时间,把新生代设置适当的大一点,也可以使用remark的方式,笔者的生产环境全部使用的是remark的方式,当然还要注意一点,永久区也可以触发大回收,还有些框架也会显式调用system.gc(),主要避免是设置永久区不能过小,关闭system.gc()。另外还有重要一点,要定期查看大回收的情况,用大回收的总时间除以大回收的次数,算一下平均每次的时间,如果过大,意味你的业务出现长时间的停顿。如下:609/35=17.4,平均一次大回收耗时17.4秒,这种情况是绝对不能接受的: