<![CDATA[viLuo的世界]]> http://viluo.com/ zh-cn www.emlog.net JVM内存结构浅析 http://viluo.com/post/26 JVM 包含两个子系统和两个组件。两个子系统分别是ClassLoader和ExecutionEngine,两个组件则是Runtime Data Area和Native Interface,其中Runtime Data Area中就包含了我们常说的JVM内存。

点击查看原图

但仅仅将内存分成这么一大块,JVM是啥也干不了的。为此Runtime Data Area中又将内存划分成不同的区域,用于存储不同的数据。

Method Area——方法区,被Class Loader所装载的class文件以及相关的方法信息、域信息、静态变量等都存放在这个区域内。该区域是所有Java线程所共享的。

Heap——堆区,这个区域就是用来存放java对象的,通常GC也是针对该区域。一个Java虚拟机实例只有一个堆,并直接由java虚拟机进行管理,在虚拟机启动时创建。该区域可以被所有Java线程所共享。

Stack——栈区,用来存放JVM的内存指令。通常虚拟机对它的操作比较简单(以帧为单位的压栈和出栈),速度也很快。每个线程都有自己的栈,且栈可以不连续。

Program Counter Register——每一个线程都有自己的一个PC寄存器,用于存放下一条被执行的指令的地址。每个线程的PC寄存器在线程启动时产生。

Native Method Stack——保存本地方法进入区域的地址。

通常来说,也可以将JVM的内存区域划为堆和非堆两个区域。堆即上面的Heap区域,剩下的都是非堆区域。而jvm 对内存垃圾的回收,即GC,通常都是针对堆区域的。

]]>
Fri, 31 Aug 2012 08:27:46 +0000 viLuo http://viluo.com/post/26
JVM对大对象分配内存的特殊处理 http://viluo.com/post/25 前段日子在和leader交流技术的时候,偶然听到jvm在分配内存空间给大对象时,如果young区空间不足会直接在old区切一块过去。对于这个结论很好奇,也比较怀疑,所以就上网搜了下,发现还真有这么回事。以下给出具体代码来说明:

首先定义好jvm内存各个区域的大小。我设定的是eden区8M,from和to各1M,old区10M,总共20M的空间,参数如下:

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8

紧接着,开始写程序。很简单,就是初始化一个9M的程序,然后用jstat命令看jdk的内存使用情况。

public class App {
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public static void main(String[] args) {
byte[] allocation = new byte[9*_1MB];
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

然后打成jar,执行。结果如下:

S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT
0.00   0.00  18.04  90.00  23.08      0    0.000    20    0.027    0.027

果然,当对象大小大于eden区的时候会直接扔到old区。但我还不满足与此,于是将对象改大了些,改成了11M。再次尝试发现结果如下:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.taobao.jdkmem.App.main(App.java:17)

到这里结束了么?当然没有:)这个是一个大的完整的对象,当大对象本身是由一连串的小对象组成的时候,会不会不再OOM呢?于是改了代码再次尝试:

public class App {
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public static void main(String[] args) {
byte[][] allocation;
allocation = new byte[11][_1MB]; // 直接分配在老年代中
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

再次运行,结果如下:

S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT
0.00  38.06  67.68  60.02  23.10      1    0.007    14    0.012    0.019

果然,这次居然又被jvm给生吃下去了。不过这次并非所有的都在old区,而是有一部分还在young区里活着。看来jvm还是足够彪悍的。

由此可见,当出现大对象的时候,jvm用牺牲部分宝贵的old区的方式来保证了整个jvm的正常运转。所以,程序中尽量要避免大对象,如果实在不行,就让大对象活的尽量久些,莫要new一个然后gc掉再new一个再gc,这么爆jvm可不太友好。

到这里结束了吧?你猜对了,还没有:P既然知道jvm会对大对象申请内存做特殊处理,那么就在琢磨程序员有没有方法干预这个过程呢?答案是有的,就是使用这个参数-XX:PretenureSizeThreshold。这个参数的单位是Byte,其作用是当新对象申请的内存空间大于这个参数值的时候,直接扔到old区。做个试验就证明了:

public class App {
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public static void main(String[] args) {
byte[] allocation = new byte[4*_1MB];
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

运行命令如下:

java -jar -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728 memtest-1.0-SNAPSHOT.jar 

我设置的阈值是3M,意味着超过3M的对象会被直接扔到old区。结果是皆大欢喜,新对象直接被扔到了old区:

S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT
0.00   0.00  18.04  40.00  23.08      0    0.000     0    0.000    0.000

试验有了结果,自然而然心情愉悦。但这个参数使用时需要慎重,因为fullgc的代价很高,因此old区就显得非常宝贵。除非你真的清楚你在干什么,否则莫要轻易玩这个参数,万一搞个频繁fullgc就玩大了。ok,到此打完收工。

]]>
Fri, 31 Aug 2012 08:12:44 +0000 viLuo http://viluo.com/post/25
JVM内存结构浅析 http://viluo.com/post/24 jvm中的内存可以分为堆内存和非堆内存,其中堆内存用于存储虚拟机运行中产生的各种对象,而通常所说的GC也就是针对这一块内存而言的。作为开发者,我们并不需要去关心到底什么时候该调用GC去清理内存,因为jvm会帮我们打理好这一块。但是如果想深入了解java内存管理,就不得不关心下这块对程序员“透明”的区域了。

要了解GC过程,首先要说说堆内存是怎么工作的。java所创建的对象会都放在堆内存中,但随着新对象的逐渐加入,堆内存的剩余空间会越来越少。当无法给一个新的对象继续分配内存空间时,程序就会发生out of memory。我们知道,java程序员在编写时无需关心何时释放对象的内存,所以会有相当一部分的“垃圾”对象,即再也不会引用到的对象存在于内存中。当剩余空间不够时,jvm就会扫描堆内存找出垃圾对象,将其占用的内存释放掉,这就是GC。

jvm中的GC采用了generation算法,认为内存中的对象有这样的情形:大多数的对象存活的时间比较短,而少部分的对象才能够长时间存活。因此,jvm将堆内存划分为年轻代(young generation)和年老代(old generation)。年轻代中的对象通常建立时间不久,且大部分生命周期也很短;年老代中的对象则已经创建比较久了,其声明周期也相对年轻代比较长。按照上面的划分,jvm在做GC时也进行了区别对待,对年轻代GC会相对比较频繁,且采用了copying算法;年老代的GC相对比较少,且采用的是tracing算法的一种,是标记-清除-压缩。

具体结构如下图:

点击查看原图

Young Generation——当一个对象被创建时,需要给其分配内存。由于堆内存是所有java线程共享的,所以如果要分配一段内存空间就必须先对内存加锁,这样是很耗性能的,故如果对象所需内存小于某个值(好像是512个字节,我也忘了)直接从线程自己的缓存里分配。如果在Young Generation中分配内存的话,就通常是放在Eden(伊甸园区,不知道为啥取这个名字)中。这里就要介绍下Young Generation中内存的划分和使用了:

Eden——所有新创建的对象都被放置在这里。

Survivor——当Eden区空间不足时,会将其中依旧存活的对象拷贝到两块Survivor区域(From Space和 ToSpace)中的一个,如果此时这个Survivor区域也空间不足,则将该块区域中存活的对象拷贝到另一块区域中。 注意,总有一个Survivor区域是空的。

对Young Generation的垃圾回收叫minor GC,通常很多的对象都活不过一次GC。

Old Generation——但一个Survivor区域满了的时候,会将该区域中已经历一定次数GC而依旧存活的对象放到Old Generation中。如果Old Generation也满了,那就要Full GC了。Full GC很耗性能,当Full GC进行时,应用程序会暂停。由于大部分对象都活不过一次GC,所以如果服务器上频繁的发生Full GC,就要关注下是不是出问题了。

Permanent Generation——这块区域我认为就是方法区,其作用就是用来存放java类、方法信息和常量池等。有的观点也将方法区看做是Permanent Generation,并看做堆内存一部分,这里就顺便介绍下。通常这块内存不会有什么太大的变动,当然如果采用了动态生成一些类的方式来设计应用程序,那么这块也需要设置较大一些用来放置新增的类。另外,由于常量池也放在这块区域里,所以如果常量池无限增大,理论上也会出现oom。也正因为这样,这块区域也会有GC,而且方式是Full GC。

上面就是jvmGC的一些信息。当然,开发人员也可以通过设置参数来影响jvm对内存的管理,下面就是一些常用的参数:

-Xmx——Heap区域最大值。默认为系统内存的1/4,不超过1G。

-Xms——Heap区域初始化时候的内存值。默认为系统的1/64,不超过1G。如果当前的空余堆内存比例小于40%(见-XX:MinHeapFreeRatio)系统会将堆内存扩大到最大值。通常-Xmx和-Xms设置为一样的值,免得内存来回变动损耗性能。

-XX:PermSize——设置方法区的内存初始值,通常为系统内存的1/64。

-XX:MaxPermSize——设置方法区的最大内存值,通常为系统内存的1/4。

-XX:MinHeapFreeRatio——设置Heap区域最小空闲值,用于控制何时将堆内存扩大至最大值,通常是40%

-XX:MaxHeapFreeRatio——设置Heap区域最大空闲值,用于控制何时将对内存缩小至初始值,通常是70%

-XX:NewRatio——设置Heap区域中new和old代大小的比例,值就是年轻代内存大小/年老代内存大小。

-XX:SurvivorRatio——设置Survivor区域和Eden区域的比例,值就是Eden区域大小/Survivor区域大小。

]]>
Fri, 31 Aug 2012 08:06:06 +0000 viLuo http://viluo.com/post/24
JVM对象内存分配方式总结 http://viluo.com/post/23 通常来说关于JVM对于对象的内存分配,只要到堆内分配一般就over了,但是在很多人的博客或者帖子中又说了一些其他的分配,比如栈内分配等等,搞的自己有点眼晕,索性就将JVM内存分配的方式统统查了一遍,然后总体上给缕一下。一来是方便自己,权当读书笔记,二来希望有人能一起讨论下。

JVM在内存区域中专门划分出一块区域来,用于存储对象的相关数据,这块区域就叫做堆。堆内的对象数据是各个线程所共享的,所以当再堆内创建新的对象时,就要进行锁操作。而众所周知锁操作是比较耗费性能的,因此针对每个线程,JVM给它在堆上分配了一块“自留地”——TLAB。TLAB全称是ThreadLocal Allocation Buffer,处于堆内存的年轻区,也就是Eden这个区域里。每个线程在创建新的对象时,会首先尝试在自己的TLAB里进行分配,如果成功就返回,失败了再到共享的Eden区里去申请空间。在自己的TLAB区域创建对象失败一般有两个原因:一个是对象太大,第二个是自己的TLAB区剩余空间不够。

这里就涉及到TLAB区域大小的问题了。通常默认的TLAB区域大小是Eden区域的1%,当然也可以手工进行调整,对应的JVM参数是-XX:TLABWasteTargetPercent。

JVM在进行了上面的优化之后,发现创建对象还有可以优化的空间。空间在哪里?在于对象的生存周期。大部分所创建的对象都无法逃脱一次GC的,其中很多对象更是在一个线程、乃至一个方法调用结束后就over了。针对那些只在一次方法调用内生存的对象,JVM通过server方式的优化对其分配策略进行了改进。首先server方式的优化是可以进行复杂的逃逸分析,而后JVM根据逃逸分析的结果,将未逃逸的对象,直接在栈内分配内存空间。什么?栈内??没错!!这个分配方式就是栈内分配,当线程结束时,栈空间被收回,对象也就直接被回收了。由于栈的操作非常快,所以这种对于对象的操作也更加快速。那么栈内分配时对象数据放在哪里?这块我还没看到相关资料,我分析栈帧有三个区域,局部变量区、操作数区和剩余区域,所以对象数据应该是放在剩余的区域里,当然这块是猜测。

以上就是两种额外的JVM内存分配方式。总结起来就是:在server端优化下,如果对象未逃逸,则直接在栈内分配;逃逸之后先尝试在TLAB中分配,失败后再在堆内分配,还失败的话,那么就GC吧。

]]>
Fri, 31 Aug 2012 07:35:42 +0000 viLuo http://viluo.com/post/23