常用内存选项

-Xmx: 最大堆大小

-Xms:最小堆大小

-Xss :线程堆栈大小,默认1M

生产环境最好保持 Xms = Xmx

java内存研究

内存布局

可见:

  • 堆大小 = 新生代 + 老年代,新生代=E+From Survivor+To Survivor。新生代和老年代的比例通过-XX:NewRatio=2选项指定,新生代内部E/S0/S1的比例用-XX:SurvivorRatio=8选项指定。
  • -Xmx和-Xms设置的是堆大小,即设置的是新生代和老年代
  • M(即永久代)不算在堆里面。永久代存放的是类的元数据信息(比如类名、属性、方法、访问限制等)。动态代理生成的类定义也会存放在永久代,所以如果动态生成的类太多,永久代空间就会不够,这一点需要注意(参看这里)

对于某个服务,我们通过

-Xmx256m -Xms256m

设置堆大小为256M,但服务跑起来后,通过top命令查看,它占了500M的物理内存。不要奇怪,剩下的200多M是被方法区、线程堆栈等占用了。

E/O的比例

新生代和老年代的比例默认是1:2,通过-XX:NewRatio=2选项指定,我们可通过jstat -gc命令查看结果来印证:

 S0CS1CS0US1UEC EUOC OU MC MUCCSC CCSU YGC YGCTFGCFGCT GCT 16384.0 16384.0 3776.00.0 54272.028511.2 175104.0 100739.473816.0 70718.7 9344.0 8572.74084.010 71.6665.676

EC是Eden区大小,则新生代总大小为S0+S1+E=87040。

OC为老年代大小:175104,我们看到,刚好1:2的比例。

新生代到老年代的转化

新生代是 GC 收集垃圾的频繁区域。
当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC 后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳
( 上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden 以及 Survivor 区域 ( 即 from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。
但这也不是一定的,对于一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。

From Survivor区域与To Survivor区域是交替切换空间,在同一时间内两者中只有一个不为空。jstat -gc结果里的S0和S1就是这2个survivor区。

方法区

方法区存放类定义和元信息,像字符串池和类的静态成员不在方法区里,而是放在堆上

Major GC 和 Full GC 的区别

Full GC:收集young gen、old gen、perm gen

Major GC:有时又叫 old gc ,只收集老年代( old gen )

Minor GC:只收集新生代(young gen)。

生产问题案例

JVM堆使用率居高不下

现象:JVM堆使用率过很久才能降下来。

原因是:程序内未做分批处理,一次性分配了大片连续内存(常见于ArrayList中),导致新生代区域不够分,所以分到了老年代。老年代只能在执行频度更低、执行速度更慢的major gc里清理,而不会在频繁执行、速度更快的minor gc里清理,所以导致JVM堆内存占用要过一段较长的时间才能看到下降。实际中遇到过2万条记录在8G内存的容器里就占用了2G堆内存的情况。

线程间歇性发呆

现象:一个函数内的两个步骤间并无耗时操作,但线程却仿佛发呆了几秒不做事

可能的原因:

  • 服务内的数据库、redis连接池耗尽,线程挂起等可用的连接;
  • cpu和内存占用率很高
  • 日志打印过多,写日志操作阻塞了线程,比如在一个2C接口里去打印异常堆栈

定位手段:

既然是线程阻塞,就用jstack命令dump出线程堆栈,查看可疑的阻塞。这跟早期C++里用gdb的thread apply all bt 命令查看堆栈一样。