作者去年6月第一次遇到OOM现象,当时非常焦急。看见别人就想抱大腿,最后运维说他不懂java,最后怀着紧绷的心态查阅日志,最后在tomcat的启动脚本中发现有人将java虚拟机的堆内存设为1024M,而引发OOM的原因是获取公司员工信息的大JSON,3000人的大Json,最后成功引发堆溢出。因为机器上部署的应用很多,内存较多。当时设置堆大小为4G,至今该项目再没出现过类似的问题。
然而最近云服务版本,却出现了OOM现象。目前累计出现两次。每次出现问题,作者还是采用同样的办法。增加堆大小,并设置堆初始化和最大空间相同,防止消耗。并设置年轻的和老年代比例3:8,为什么如此设置的原因还是作者水平不太够,所以还是按照官方推荐进行。其实jvm调优有很多参数,包括垃圾回收算法和一些上限条件。这块作者还没有做深入的学习。虽然还没读过JVM调优的书籍,但是这块肯定是相当重要的。
这里对JVM的内存结构进行简单的说明。
1.堆
我们知道java创建对象是保存在堆上的,堆是线程共享的。也就是多线程问题的根源,而我们在内存调优的时候设置的年轻代和老年代就是堆的划分。其中程序创建的对象就是在年轻代的Eden,随着Eden的逐渐填充,直到触发了ygc,这里的ygc的回收算法也是可以指定的,还有回收触发上限条件。触发之后就会将eden中的对象移动到s0,下次ygc将幸存的对象换到s1中。经过一定次数或者限制条件之后,会将对象移动到老年代。老年代的回收也是有相关的策略和上限条件,这也是调优的方向。老年代的垃圾回收叫做fullgc,fullgc一开动,地动山摇。所以要经量让fullgc不频繁出现。
2.元空间,永久代,方法区、运行时常量
我们知道我们代码运行时,往往会加载很多类,这些类就是存在于元空间,java7叫做永久带,是独立的。但因为潜在的oom,所以将永久代变成元空间,并部分存在堆中,这样就可以在垃圾回收时进行空间释放,防oom。该部分空间是线程共享的。
3.虚拟机栈
我们的java代码运行其实是通过方法调用的,我们都知道方法执行完毕,里边的参数和数据就没了。这里数据没了的原因就是虚拟机栈。当执行一个方法时,会将该方法的起始地址返回地址还有局部变量等信息以栈帧的形式插入到虚拟机栈,执行完毕就出栈,所以说如果发生死循环或者递归可能会引发虚拟机栈的OOM现象。虚拟机栈是线程的局部变量,各个线程是相互隔离的。
4.本地方法栈
在java底层都是通过native调用的c ,那么本地方法栈就是用来保存底层调用的方法执行状况的。本地方法栈也是线程独有。因为多线程是代码的副本的多核运行。
5.程序计数器
我们每个代码的运行都是字节码,所以在线程中断或者阻塞之后都需要恢复现场,那么每个副本代码也就是线程的执行位置需要记录。所以程序计数器也是线程独占的。当然对于虚拟机栈来说程序计数器是有值的,但是本地方法栈因为不是我们自己编写的代码,所以也没办法进行记录其数值,所以为空。