Tag Archives: JVM

JVM内存详解

最近与同事讨论了一下现在各大流行的语言及平台,有一个比较一致的观点,JVM平台在目前众多虚拟机平台中,已经是最成熟,最稳定的了。

JVM平台在内存管理,包括分配,GC等,已经非常完善。下面是关于JVM的内存的一些总结,包括逻辑视角上看来JVM的内存,从物理视角上来了解JVM内存的管理,最后再
探讨一下JVM作为一个应用在Linux主机下与整个系统的内存之间关系。
1.逻辑视角
从逻辑视角上看,JVM中的内存可以分为下面6个部分,每个部分存储哪些信息在《深入Java虚拟机》一书中有非常详尽的说明。http://www.colorfuldays.org/program/java/java_constants_in_vm/
Program Counter Register
Java Virtual Machine Stack
Heap
Method Area
Runtime Constant Pool
Native Method Stack
2.物理视角
在物理层面上JVM将内存分成几个区进行管理,它们只管理对应于上述逻辑层面上的Heap,Method Area。JVM stack的大小也是可调整的,但是它并没有包括在下面讨论的内存管理中。
具体各个区的分配及作用如下:
Young Generation 通常空间较小,因此收集算法需要快速,频繁且有效,若干次收集后仍然存活的对象,则移入Old Generation。
Old Generation   占用空间通常比Young要大很多,存放经过若干次收集后仍然存活的对象,某些大对象也直接在此区域分配。
Permanent Genetation   Objects describing classes and methods,这个区域我认为对应于逻辑视角的Method Area.
其中Young区又分成几个区进行管理,各个区的作用如下:
Eden  初始化时产生的对象都存放在这个区,一些大对象除外,它们被直接分配的Old区。
From  存放至少经常一次收集仍然存活的对象,
To 与From 一个时间内只有一个区有对象。在收集过程中不断将仍然存活的对象移到另外一个空的区及Old区后,然后释放整个区的空间。具体机制,下文再讲。
3.GC机制
Sun的Hotspot JVM中已经有非常多可选的GC算法了,不少GC算法已经非常智能。比如可以通过设置一个目标值,然后GC会自动调整适应以达到这个设定值,和Oracle 10g中的调优选项类似。
在Young 区达到某一阀值的时候启动minor gc收集,当Old及 Permanent达到某一个阀值的时候,启动major GC即full gc.
GC在对Young区和Old区会采用不同的算法,对于CPU个数的不同,又有串行收集和并行收集的不同。Young区较小,而且需要频繁运行,因此算法需要快速,有效。
对Young区的收集算法如下图示,其中标红X的是垃圾对象.
对于Old区收集算法在串行及并行时上都是采用Mark-Sweep-Compact方式,如下图所示
还有一种很特别的CMS(Current-Mark-Sweep)收集算法,它对Young区的收集与前面所讲的都一样,它对Old区的收集比一般的方法相比少了Compact这一操作,因此程序在Full GC时中断时间会缩短,缺点是收集完之后的内存块是不连续了,在minor GC时可能要花费更多一点的时间。
GC调优的策略在memoryManager_whitepaper一文中讲得非常清楚,在这里就不详细讲述了。下面给出一个通过jmap工具导出的JVM内存snopshoot的例子。
例子中有上述提到的各个区的大小等信息,还有其它一些GC使用的信息,这些可以在whitepaper中找到其对应的解释。
[admin@vm153f ~]$ /opt/java1/bin/jmap -heap 2457
Attaching to process ID 2457, please wait…
Debugger attached successfully.
Server compiler detected.
JVM version is 10.0-b23
using thread-local object allocation.
Mark Sweep Compact GC
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize      = 1342177280 (1280.0MB)
NewSize          = 335544320 (320.0MB)
MaxNewSize       = 335544320 (320.0MB)
OldSize          = 4194304 (4.0MB)
NewRatio         = 8
SurvivorRatio    = 8
PermSize         = 100663296 (96.0MB)
MaxPermSize      = 268435456 (256.0MB)
Heap Usage:
New Generation (Eden + 1 Survivor Space):
capacity = 301989888 (288.0MB)
used     = 41946376 (40.00318145751953MB)
free     = 260043512 (247.99681854248047MB)
13.889993561638725% used
Eden Space:
capacity = 268435456 (256.0MB)
used     = 41946376 (40.00318145751953MB)
free     = 226489080 (215.99681854248047MB)
15.626242756843567% used
From Space:
capacity = 33554432 (32.0MB)
used     = 0 (0.0MB)
free     = 33554432 (32.0MB)
0.0% used
To Space:
capacity = 33554432 (32.0MB)
used     = 0 (0.0MB)
free     = 33554432 (32.0MB)
0.0% used
tenured generation:
capacity = 1006632960 (960.0MB)
used     = 153565120 (146.45111083984375MB)
free     = 853067840 (813.5488891601562MB)
15.255324045817057% used
Perm Generation:
capacity = 100663296 (96.0MB)
used     = 49601000 (47.303199768066406MB)
free     = 51062296 (48.696800231933594MB)
49.274166425069176% used
一般讲JVM内存的文章在讲述完上述内部时也就结束了,但是在这里还有一个同样重要的东西要讲。JVM作为一个运行在操作系统上的应用,它的内存使用还与其运行的主机有关。
在这里说一下JVM与主机内存的关系。JVM实际使用的内存包括堆内存,JVM stack使用的内存及java.nio.ByteBuffer等对象使用的内存。其中ByteBuffer使用的内存分为两种形式,如下图示。

JVM平台在内存管理,包括分配,GC等,已经非常完善。下面是关于JVM的内存的一些总结,包括逻辑视角上看来JVM的内存,从物理视角上来了解JVM内存的管理,最后再探讨一下JVM作为一个应用在Linux主机下与整个系统的内存之间关系。

1.逻辑视角

从逻辑视角上看,JVM中的内存可以分为下面6个部分,每个部分存储哪些信息在《深入Java虚拟机》一书中有非常详尽的说明。以前写的一篇文章对这些区存储Java程序中的哪些元素有些简要的说明。

Program Counter Register

Java Virtual Machine Stack

Heap

Method Area

Runtime Constant Pool

Native Method Stack

2.物理视角

在物理层面上JVM将内存分成几个区进行管理,它们只管理对应于上述逻辑层面上的Heap,Method Area。JVM stack的大小也是可调整的,但是它并没有包括在下面讨论的内存管理中。

具体各个区的分配及作用如下:

Young Generation 通常空间较小,因此收集算法需要快速,频繁且有效,若干次收集后仍然存活的对象,则移入Old Generation。

Old Generation   占用空间通常比Young要大很多,存放经过若干次收集后仍然存活的对象,某些大对象也直接在此区域分配。

Permanent Genetation   Objects describing classes and methods,这个区域我认为对应于逻辑视角的Method Area.

其中Young区又分成几个区进行管理,各个区的作用如下:

Eden  初始化时产生的对象都存放在这个区,一些大对象除外,它们被直接分配的Old区。

From  存放至少经常一次收集仍然存活的对象,

To 与From 一个时间内只有一个区有对象。在收集过程中不断将仍然存活的对象移到另外一个空的区及Old区后,然后释放整个区的空间。

jvm memory

3.GC机制

Sun的Hotspot JVM中已经有非常多可选的GC算法了,不少GC算法已经非常智能。比如可以通过设置一个目标值,然后GC会自动调整适应以达到这个设定值,和Oracle 10g中的调优选项类似。

在Young 区达到某一阀值的时候启动minor gc收集,当Old及 Permanent达到某一个阀值的时候,启动major GC即full gc.

GC在对Young区和Old区会采用不同的算法,对于CPU个数的不同,又有串行收集和并行收集的不同。Young区较小,而且需要频繁运行,因此算法需要快速,有效。

对Young区的收集算法如下图示,其中标红X的是垃圾对象.

young gcyoung gc after

对于Old区收集算法在串行及并行时上都是采用Mark-Sweep-Compact方式,如下图所示

old gc

还有一种很特别的CMS(Current-Mark-Sweep)收集算法,它对Young区的收集与前面所讲的都一样,它对Old区的收集比一般的方法相比少了Compact这一操作,因此程序在Full GC时中断时间会缩短,缺点是收集完之后的内存块是不连续的,在minor GC时将Young区的对象移到Old区时,查找合适的内存块,需要花费更多一点的时间。它收集后的效果如下:

cms old gc

GC调优的策略在memoryManager_whitepaper一文中讲得非常清楚,在这里就不详细讲述了。下面给出一个通过jmap工具导出的JVM内存snopshoot的例子。

例子中有上述提到的各个区的大小等信息,还有其它一些GC使用的信息,这些可以在whitepaper中找到其对应的解释。

[admin@vm153f ~]$ /opt/java1/bin/jmap -heap 2457

Attaching to process ID 2457, please wait…

Debugger attached successfully.

Server compiler detected.

JVM version is 10.0-b23

using thread-local object allocation.

Mark Sweep Compact GC

Heap Configuration:

MinHeapFreeRatio = 40

MaxHeapFreeRatio = 70

MaxHeapSize      = 1342177280 (1280.0MB)

NewSize          = 335544320 (320.0MB)

MaxNewSize       = 335544320 (320.0MB)

OldSize          = 4194304 (4.0MB)

NewRatio         = 8

SurvivorRatio    = 8

PermSize         = 100663296 (96.0MB)

MaxPermSize      = 268435456 (256.0MB)

Heap Usage:

New Generation (Eden + 1 Survivor Space):

capacity = 301989888 (288.0MB)

used     = 41946376 (40.00318145751953MB)

free     = 260043512 (247.99681854248047MB)

13.889993561638725% used

Eden Space:

capacity = 268435456 (256.0MB)

used     = 41946376 (40.00318145751953MB)

free     = 226489080 (215.99681854248047MB)

15.626242756843567% used

From Space:

capacity = 33554432 (32.0MB)

used     = 0 (0.0MB)

free     = 33554432 (32.0MB)

0.0% used

To Space:

capacity = 33554432 (32.0MB)

used     = 0 (0.0MB)

free     = 33554432 (32.0MB)

0.0% used

tenured generation:

capacity = 1006632960 (960.0MB)

used     = 153565120 (146.45111083984375MB)

free     = 853067840 (813.5488891601562MB)

15.255324045817057% used

Perm Generation:

capacity = 100663296 (96.0MB)

used     = 49601000 (47.303199768066406MB)

free     = 51062296 (48.696800231933594MB)

49.274166425069176% used

在这里说一下JVM与主机内存的关系。JVM实际使用的内存包括堆内存,JVM stack使用的内存及java.nio.ByteBuffer等对象使用的内存。其中ByteBuffer使用的内存分为两种形式,如下图示。

bytebuffer

总的来讲,JVM在管理内存上花费了很大的功夫,在10多年的演进中JVM得到了不断地完善,现在已经达到了非常高的可用性了。下一步JVM的主要演进方向应该是在多核时代如何达到更高的性能。如果说Java语言太老,不够灵活,已经进入晚年的话,JVM正处在青壮年时期,现在正是它的黄金年代。

Java代码的执行过程

在上一家公司离职前,Leader让我最后再讲一次关于JVM的东西,于是便做了这样一个PPT。

JDK1.5致命Bug

今天发现了JDK1.5存在的一个致命bug,能导致JVM崩溃,做过多次测试,百发百中,无一幸免。该故障是JVM远程debug存在的缺陷,只有在开启远程debug端口时才会出现 。原因是由于接收到不符合JDWP协议的数据包,导致JVM崩溃。 这个原因看起来非常荒谬,可笑,但是却是真实的存在着。记录下发现问题的过程,以备以后可查。

早上到公司后,发现正在测试中的吉林联通ota无法访问,查看web状态时,发现这个服务挂 了,通过查看web服务的日志,发现原因是JVM崩溃导致。JVM崩溃时打印出来的日志如下:

ERROR: transport error 202: handshake failed – connection prematurallyclosed ["transport.c",L41]JDWP exit error JVMTI_ERROR_NONE(0): could not connect, timeout or fatalerror

在Google上通过“JDWP exit error JVMTI_ERROR_NONE(0)”关键字搜索信息,发现反应该错误的信息非常多。在Sun官方bug列表中,对某人对bug的描述如下:A DESCRIPTION OF THE PROBLEM :When starting a java process with the argument: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8453

If that port (8453) is “pinged” by something other than JDWP, then theJVM crashes with:

ERROR: transport error 202: handshake failed – connection prematurallyclosed ["transport.c",L41]JDWP exit error JVMTI_ERROR_NONE(0): could not connect, timeout or fatalerror

and exits with a success status (0).

This has happened for servers running tests with debugging turned on.

通过查看proc/pid/cmdline文件可以看到web服务启动的脚本,找到如上所述的启动选项:-Xdebug-Xnoagent -Djava.compiler=NONE-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5900

为了证明上述描述是真实的,我进行了相关的测试。重启了web服务,通过telnet到debug端口进行测试,随意发送几个字符,web的jvm崩溃了。操作及日志如下:[root@ota01 /product/abc/conf]# telnet ota01 5900Trying 192.168.0.1…Connected to ota01 (192.168.0.1).Escape character is ‘^]’.bafff

在web服务日志中出现如下异常:ERROR: transport error 202: handshake failed – received >bafff

< – excepted >JDWP-Handshake< ["transport.c",L41]JDWP exit error JVMTI_ERROR_NONE(0): could not connect, timeout or fatalerror

该问题应该不算太严重,一般正式运行的产品都不会开启远程Debug模式,而debug的端口也会有防火墙保护,外网访问不到。但是根据“黑天鹅”理论,事故总会在意料之外产生,知道有这个JVM的问题之后,采用相应的措施,便可以保证它一定不会发生。在发现这个故障后,公司的TSE们今天将会对所有在线运行的项目进行检查,杜绝类似意外的发生。

一次奇特的Troubleshoot经历(II)

接上篇。 为了分析为什么程序会出现缰死的现象,首先需要在线下测试环境中模拟线上的环境。于是将线上数据导到本地数据库中,启动服务测试后发现与线上现象一样,于是开始想办法来分析原因。首先想到的是打印出GC的日志,但是这有个问题摆在面前:公司的所有java服务(即JVM),都是通过一个用Python写成的启动框架来启动,很难通过手动直接写java 这种方式来启动服务。这个框架通过分析一个配置文件,得到该JVM启动是需要的各种设置,如classpath encoding -Xmx -Xms ,jvm mode等。但是有个问题是,这个启动框架并不支持-Xloggc:file -Xprof这些很少使用的扩展参数设置。怎么办? 之前看过这个用python写的程序,于是我想既然支持-Xmx这样的扩展参数设置,应该可以通hack启动框架,让它支持-Xloggc:file -Xprof这种设置。非常幸运的时,看过处理-Xmx那段代码后,我发现只要做很少的修改就能够支持这种方式。修改好启动框架之后,先测试了一下是否能够正常工作,一切都顺利。于是修改服务配置的server.xml中增加上自己新增的参数设置,启动服务,服务一切正常。在执行到缰死的语句之后,出现了之前没有的日志:
Unloading class sun.reflect.GeneratedMethodAccessor
找到loggc生成的文件,通过HPjtune工具进行分析,发现内存/时间图差不多是一条45%的直线,而随时间增长,GC频率非常频繁,另外Full GC起来越多,最终变成全部是Full GC,每次回收到的内存越来越少,直到接近于0。现在可以发现问题是由于程序内存不够,不断进行Full GC导致。由于FullGC是所有的线程都暂停,因此会出现程序缰死的情况。通过HPTune生成的图如下:
memery_used
memery_summrize
其中第一张图中在详细的内存使用详情,128M的内存,最终使用了126.625M,peak Ultization为100%。第二张图显示最终JVM不断进行Full GC。

到这里程序缰死的原因基本明白了,但是还有个问题没弄清楚:程序出现不断Full GC时,JVM的内存变化情况是怎么样的,程序的线程运行情况怎么样? 为什么不抛出OutOfMemory的异常信息? 这些问题需要借助更强大的工具来分析了,这时JProfile就派上了用场。安装完之后通过JProfile进行观察,程序在运行一段时间之后,除了Full GC线程,其它所有的线程都被挂起,而Full GC始终无法回收足够的内存以使其它线程能够正常运行。最终程序只能在不断的进行Full GC,从而完全缰死。

解决方法:由于项目要求特别紧,没办法重构,目前使用最简单的办法,增加内存。但是增加内存并不是好的方法,需要处理的数据增加时,内存仍然会被耗尽。最好的办法是进行重新设计,或许在生成线程的地方采用线程池来处理会比较好。

一次奇异的JVM TroubleShoot经历 (I)

最近遇到一个问题,一个线上运行的程序在运行到某条SQL之后便缰死,重启服务之后程序又一次缰死在那条语句上。昨天通过增加JVM内存将问题解决,但是并没有找到原因。今天下午通过各种手段分析出这是由于JVM full GC导致程序假死。在分析过程中将自己所学的东西用得淋漓尽致,第一次在分析实际问题中使用到JVM 的-Xloggc,-Xprof选项,并使用HPTune分析GC产生的日志,通过JProfile查看分析JVM中更详细的信息。在这次troubleshoot中,还hack了公司用Python写的启动框架。在解决问题完之后,我甚至在想,公司里或许只有我一个人会这样去分析这个问题。下面记录一下这次Troubleshoot的“传奇”经历。

这个程序刚上线不久,目前属于测试阶段,是用来分析用户的操作日志,为每一条有效的日志生成相应的事件投递到另外一个系统中。 现在遇到的问题是日志打印出一条SQL之后,程序一直卡死了,没有任何程序异常日志,也没有OutOfMemory相关的信息。
第一阶段: 问题分析及解决
碰到这个问题后,首先我觉得这个SQL有问题,原始SQL如下:

select e.userid,t.username,t.service_name,t.count from e_users e,(
 SELECT /*+index(l log_operation)  use_hash(l b) index(b) */
   l.username ,service_name,COUNT(opt_cmd) as count FROM log_operation l
 WHERE  to_char(l.datetime,'yyyyMMdd') >='20090714' AND to_char(l.datetime,'yyyyMMdd') <='20090721'
   opt_cmd = 'receive' GROUP BY l.service_name, l.username
 ) t where e.username=t.username;

执行计划如下:


Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=9 Card=37 Bytes=12025)
   1    0   SORT (GROUP BY) (Cost=9 Card=37 Bytes=12025)
   2    1     TABLE ACCESS (BY INDEX ROWID) OF 'E_USERS' (Cost=2 Card=722 Bytes=18050)
   3    2       NESTED LOOPS (Cost=6 Card=37 Bytes=12025)
   4    3         PARTITION RANGE (ALL)
   5    4           TABLE ACCESS (FULL) OF 'LOG_OPERATION' (Cost=4 Card=1 Bytes=300)
   6    3         INDEX (RANGE SCAN) OF 'E_USERS2_IDX' (UNIQUE) (Cost=1 Card=722)

这个SQL中最大的问题在于to_char(),这样的SQL在Oracle执行时无法使用createtime字段上的索引,SQL中加的hint根本就无效,只能进行全表扫描。连上Oracle之后,查看了一下这个程序的执行计划,不出所料,果然是全表扫描。查了一下那张表的数据量,有300多万条的记录,另外一张连结表中也有70多万条的记录。执行了一下那条语句,发现需要70多秒。于是将SQL修改成如下形式:

select e.userid,t.username,t.service_name,t.count from e_users e,(
 SELECT /*+index(l log_operation)
  use_hash(l b) index(b) */ l.username ,service_name,COUNT(opt_cmd) as count FROM log_operation l
 WHERE  l.datetime between to_date('20090714','yyyyMMdd')  AND to_date('20090721','yyyyMMdd')
opt_cmd = 'receive' GROUP BY l.service_name, l.username
 ) t where e.username=t.username;

执行计划如下:

Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=15 Card=18058 Bytes=1390466)
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'E_USERS' (Cost=11 Card=18058 Bytes=451450)
   2    1     NESTED LOOPS (Cost=15 Card=18058 Bytes=1390466)
   3    2       VIEW (Cost=4 Card=1 Bytes=52)
   4    3         SORT (GROUP BY) (Cost=4 Card=1 Bytes=300)
   5    4           TABLE ACCESS (BY LOCAL INDEX ROWID) OF 'LOG_OPERATION' (Cost=1 Card=1 Bytes=300)
   6    5             INDEX (RANGE SCAN) OF 'LOG_OPERATION_DATETIME_1' (NON-UNIQUE) (Cost=2 Card=1)
   7    2       INDEX (RANGE SCAN) OF 'E_USERS2_IDX' (UNIQUE) (Cost=1 Card=18058)

再一次查看修改后的SQL执行计划,这次使用到了createtime字段上建的索引,执行时间也变成了6秒多。于是我认为问题出在SQL上,现在回想起来当时得出这个结论过于草率了。由于在那张300多万条记录的表中,有许多数据是可以清除的,需要清除。于是让相关人员清除部分记录,然后重启服务,但是很快发现问题依然。

第一次猜想失败,于是再次查看该SQL,发现能找出48万多条记录,因此想起来要看一下代码中的实现。程序的实现如下:通过关联查找两张表,得到需要处理的记录,然后针对每一条记录生成一个Runnable对象,在该对象中生成该记录对应的Event,然后通过线程调度器将Event以WebService方式投递到另外的系统中进行处理。很明显这种处理方式有很严重的问题,一旦需要处理的记录很多时,JVM没有足够的内存来生成这么多的线程对象,甚至有可能超过JVM所有生成线程的上限。这时想起或许增加JVM内存可以解决问题,于是将该程序运行的JVM内存由原来的128M增加到256M。重新启动服务后,程序在运行到那条语句后,一切正常。问题解决了! 但是有个疑问,如果是内存不足,为什么程序没有打出OutOfMemory的日志呢?

JVM的内存管理

JVM的内存管理模型
JVM对于内存是通过分成不同的部分来管理的,主要包括方法区(Method Area),堆(Heap),Java栈(Stack)这几个部分。
方法区用来存放装载到JVM中的类的元信息(或者Meta信息),这些元信息通过java bytecode获得。堆中存放的都是对象,即类的实例对象,Java棧以帧为单位保存线程运行的状态,存放每一个线程在程序执行过程中需要的数据信息。
对应于Java文件中的各种类变量,实例变量,类本身等内容,下面说明一下它们在JVM中的存放位置。
public static final int i =1;
这个 i 在编译时就可以确认它的值,因此它作为一个常量被直接写在.class文件中,其它类用到这个i的时候,在编译过程中会拷贝i的值到该类的.class文件中。
public static final int i = (int)(Math.random() * 4);
如果是上面这种形式定义的变量,因为其在编译时无法确认其值,需要到运行过程中才会有确定值,因此它会当作一个类变量在类初始化时存放在方法区中。
static int i = 9;
以上这种形式定义的变量在类初始化时被初始化为正确的值,并将其与类信息一起存放在方法区。
int i = 9;
这种形式定义的变量是实例变量,在类被实例化时赋于正确的初始值,和类实例对象一起存放在堆区。
void getXX(int a){
int i = 1;
}
上面这种形式定义的变量a是一个参数,i是一个局部变量,只在运算过程在有效,因此会存放在Java棧中棧帧中。其中参数a在执行过程中会拷贝其值为另外一个局部变量,到这个方法在帧中,并在方法退出前,将其值写回到调用的地方。方法返回后i的值就会无效,等待GC回收。
通过上面的论述可以推断出有哪些情况可能会导致OutOfMemory的错误了。
1、过多的类信息被装载到方法区,导致该区域OutOfMemory错误,在一些框架中通过反射机制生成大量的类信息可能导致该问题。
2、使用过多的static类型的类变量,特别是将大的数组,字符串等,导致方法区占用内存过多,造成OutOfMemory错误。由于JVM中并没有提供设置方法区大小的参数,因此只能通过加大JVM的内存来解决。
3、程序创建了过多的线程,导致Java棧使用内存过多,导致OutOfMemory。可通过-Xss来设置每一个Java thread stack 的大小来解决该问题。在JVM的多线程模型中,每一个Java线程对应一个操作系统级的用户线程,有些操作系统所支持的用户线程是有限的,如果超出其最大线程数,也会抛出OutOfMemory,这时并不是真正的内存不足,而是线程过多。
如:在Linux2.4内核中 kernel.threads-max = 8192
而在Linux2.6内核中 kernel.threads-max = 48890
4、程序生成了过多的实例对象,使得堆占用大量内存,最终出现OutOfMemory错误。 可通过下面的参数来配置JVM堆的大小,更精细的调优就需要对GC进行调优了:
-Xms set initial Java heap size
-Xmx set maximum Java heap size

JVM的内存管理模型

JVM对于内存是通过分成不同的部分来管理的,主要包括方法区(Method Area),堆(Heap),Java栈(Stack)这几个部分。

方法区用来存放装载到JVM中的类的元信息(或者Meta信息),这些元信息通过java bytecode获得。堆中存放的都是对象,即类的实例对象,Java棧以帧为单位保存线程运行的状态,存放每一个线程在程序执行过程中需要的数据信息。

对应于Java文件中的各种类变量,实例变量,类本身等内容,下面说明一下它们在JVM中的存放位置。

public static final int i =1;

这个 i 在编译时就可以确认它的值,因此它作为一个常量被直接写在.class文件中,其它类用到这个i的时候,在编译过程中会拷贝i的值到该类的.class文件中。

public static final int i = (int)(Math.random() * 4);

如果是上面这种形式定义的变量,因为其在编译时无法确认其值,需要到运行过程中才会有确定值,因此它会当作一个类变量在类初始化时存放在方法区中。

static int i = 9;

以上这种形式定义的变量在类初始化时被初始化为正确的值,并将其与类信息一起存放在方法区。

int i = 9;

这种形式定义的变量是实例变量,在类被实例化时赋于正确的初始值,和类实例对象一起存放在堆区。

void getXX(int a){

int i = 1;

}

上面这种形式定义的变量a是一个参数,i是一个局部变量,只在运算过程在有效,因此会存放在Java棧中棧帧中。其中参数a在执行过程中会拷贝其值为另外一个局部变量,到这个方法在帧中,并在方法退出前,将其值写回到调用的地方。方法返回后i的值就会无效,等待GC回收。

通过上面的论述可以推断出有哪些情况可能会导致OutOfMemory的错误了。

1、过多的类信息被装载到方法区,导致该区域OutOfMemory错误,在一些框架中通过反射机制生成大量的类信息可能导致该问题。

2、使用过多的static类型的类变量,特别是将大的数组,字符串等,导致方法区占用内存过多,造成OutOfMemory错误。由于JVM中并没有提供设置方法区大小的参数,因此只能通过加大JVM的内存来解决。

3、程序创建了过多的线程,导致Java棧使用内存过多,导致OutOfMemory。可通过-Xss来设置每一个Java thread stack 的大小来解决该问题。在JVM的多线程模型中,每一个Java线程对应一个操作系统级的用户线程,有些操作系统所支持的用户线程是有限的,如果超出其最大线程数,也会抛出OutOfMemory,这时并不是真正的内存不足,而是线程过多。每个Java棧所使用的内存如下:512kb on Sparc and 256kb on Intel for 1.3 and 1.4 32-bit VMs, 1mb with the 64-bit Sparc 1.4 VM

如:在Linux2.4内核中 kernel.threads-max = 8192

而在Linux2.6内核中 kernel.threads-max = 48890

4、程序生成了过多的实例对象,使得堆占用大量内存,最终出现OutOfMemory错误。 可通过下面的参数来配置JVM堆的大小,更精细的调优就需要对GC进行调优了:

-Xms set initial Java heap size

-Xmx set maximum Java heap size

reference:http://java.sun.com/docs/hotspot/threads/threads.html

http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp