关于Stack的默认大小对于不同机器有不同的大小,并且不同厂商或者版本号的jvm的实现其大小也不同,如下表是HotSpot的默认大小:
Platform
|
Default
|
Windows IA32
|
64 KB
|
Linux IA32
|
128 KB
|
Windows x86_64
|
128 KB
|
Linux x86_64
|
256 KB
|
Windows IA64
|
320 KB
|
Linux IA64
|
1024 KB (1 MB)
|
Solaris Sparc
|
512 KB
|
我们一般通过减少常量,参数的个数来减少栈的增长,在程序设计时,我们把一些常量定义到一个对象中,然后来引用他们可以体现这一点。另外,少用递归调用也可以减少栈的占用。
关于栈一般会发生以下两种异常:
1.当线程中的计算所需要的栈超过所允许大小时,会抛出StackOverflowError。
2.当Java栈试图扩展时,没有足够的存储器来实现扩展,JVM会报OutOfMemoryError。
我针对栈进行了实验,由于递归的调用可以致使栈的引用增加,导致溢出,所以设计代码如下:
我的机器是x86_64系统,所以Stack的默认大小是128KB,上述程序在运行时会报错:
而当我在eclipse中调整了-Xss参数到3M之后,该异常消失。
另外栈上有一点得注意的是,对于本地代码调用,可能会在栈中申请内存,比如C调用malloc(),而这种情况下,GC是管不着的,需要我们在程序中,手动管理栈内存,使用free()方法释放内存。
2.堆的管理
堆的管理要比栈管理复杂的多,我通过堆的各部分的作用、设置,以及各部分可能发生的异常,以及如何避免各部分异常进行了学习。
上图是 Heap和PermanentSapce的组合图,其中 Eden区里面存着是新生的对象,From Space和To Space中存放着是每次垃圾回收后存活下来的对象 ,所以每次垃圾回收后,Eden区会被清空。
存活下来的对象先是放到From Space,当From Space满了之后移动到To Space。当To Space满了之后移动到Old Space。
Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor复制过来的对象。
而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
Old Space中则存放生命周期比较长的对象,而且有些比较大的新生对象也放在Old Space中。
堆的大小通过-Xms和-Xmx来指定最小值和最大值,通过-Xmn来指定Young Generation的大小(一些老版本也用-XX:NewSize指定), 即上图中的Eden加FromSpace和ToSpace的总大小。
然后通过-XX:NewRatio来指定Eden区的大小,在Xms和Xmx相等的情况下,该参数不需要设置。
堆异常分为两种,一种是Out of Memory(OOM),一种是Memory Leak(ML)。Memory Leak最终将导致OOM。
实际应用中表现为:从Console看,内存监控曲线一直在顶部,程序响应慢,从线程看,大部分的线程在进行GC,占用比较多的CPU,最终程序异常终止,报OOM。
OOM发生的时间不定,有短的一个小时,有长的10天一个月的。关于异常的处理,确定OOM/ML异常后,一定要注意保护现场,可以dump heap,如果没有现场则开启GCFlag收集垃圾回收日志,然后进行分析,确定问题所在。
如果问题不是ML的话,一般通过增加Heap,增加物理内存来解决问题,是的话,就修改程序逻辑。
3.垃圾回收
JVM中会在以下情况触发回收:对象没有被引用,作用域发生未捕捉异常,程序正常执行完毕,程序执行了System.exit(),程序发生意外终止。
JVM中标记垃圾使用的算法是一种根搜索算法。简单的说,就是从一个叫GC Roots的对象开始,向下搜索,如果一个对象不能达到GC Roots对象的时候,说明它可以被回收了。
这种算法比一种叫做引用计数法的垃圾标记算法要好,因为它避免了当两个对象啊互相引用时无法被回收的现象。
JVM中对于被标记为垃圾的对象进行回收时又分为了一下3种算法:
1.标记清除算法,该算法是从根集合扫描整个空间,标记存活的对象,然后在扫描整个空间对没有被标记的对象进行回收,这种算法在存活对象较多时比较高效,但会产生内存碎片。
2.复制算法,该算法是从根集合扫描,并将存活的对象复制到新的空间,这种算法在存活对象少时比较高效。
3.标记整理算法,标记整理算法和标记清除算法一样都会扫描并标记存活对象,在回收未标记对象的同时会整理被标记的对象,解决了内存碎片的问题。
JVM中,不同的 内存区域作用和性质不一样,使用的垃圾回收算法也不一样,所以JVM中又定义了几种不同的垃圾回收器(图中连线代表两个回收器可以同时使用):
1.Serial GC。从名字上看,串行GC意味着是一种单线程的,所以它要求收集的时候所有的线程暂停。这对于高性能的应用是不合理的,所以串行GC一般用于Client模式的JVM中。
2.ParNew GC。是在SerialGC的基础上,增加了多线程机制。但是如果机器是单CPU的,这种收集器是比SerialGC效率低的。
3.Parrallel Scavenge GC。这种收集器又叫吞吐量优先收集器,而吞吐量=程序运行时间/(JVM执行回收的时间+程序运行时间),假设程序运行了100分钟,JVM的垃圾回收占用1分钟,那么吞吐量就是99%。Parallel Scavenge GC由于可以提供比较不错的吞吐量,所以被作为了server模式JVM的默认配置。
4.ParallelOld是老生代并行收集器的一种,使用了标记整理算法,是JDK1.6中引进的,在之前老生代只能使用串行回收收集器。
5.Serial Old是老生代client模式下的默认收集器,单线程执行,同时也作为CMS收集器失败后的备用收集器。
6.CMS又称响应时间优先回收器,使用标记清除算法。他的回收线程数为(CPU核心数+3)/4,所以当CPU核心数为2时比较高效些。CMS分为4个过程:初始标记、并发标记、重新标记、并发清除。
7.GarbageFirst(G1)。比较特殊的是G1回收器既可以回收Young Generation,也可以回收Tenured Generation。它是在JDK6的某个版本中才引入的,性能比较高,同时注意了吞吐量和响应时间。
对于垃圾收集器的组合使用可以通过下表中的参数指定:
默认的GC种类可以通过jvm.cfg或者通过jmap dump出heap来查看,一般我们通过jstat -gcutil [pid] 1000可以查看每秒gc的大体情况,或者可以在启动参数中加入:-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:./gc.log来记录GC日志。
GC中有一种情况叫做Full GC,以下几种情况会触发Full GC:
1.Tenured Space空间不足以创建打的对象或者数组,会执行FullGC,并且当FullGC之后空间如果还不够,那么会OOM:java heap space。
2.Permanet Generation的大小不足,存放了太多的类信息,在非CMS情况下回触发FullGC。如果之后空间还不够,会OOM:PermGen space。
3.CMS GC时出现promotion failed和concurrent mode failure时,也会触发FullGC。promotion failed是在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。
4.判断MinorGC后,要晋升到TenuredSpace的对象大小大于TenuredSpace的大小,也会触发FullGC。
可以看出,当FullGC频繁发生时,一定是内存出问题了。
三、JVM的数据格式规范和Class文件
1.数据类型规范
依据冯诺依曼的计算机理论,计算机最后处理的都是二进制的数,而JVM是怎么把java文件最后转化成了各个平台都可以识别的二进制呢?JVM自己定义了一个抽象的存储数据单位,叫做Word。
一个字足够大以持有byte、char、short、int、float、reference或者returnAdress的一个值,两个字则足够持有更大的类型long、double。它通常是主机平台一个指针的大小,如32位的平台上,字是32位。
同时JVM中定义了它所支持的基本数据类型,包括两部分:数值类型和returnAddress类型。数值类型分为整形和浮点型。
整形:
byte
|
值是8位的有符号二进制补码整数 |
short
|
值是16位的有符号二进制补码整数
|
int
|
值是32位的有符号二进制补码整数
|
long
|
值是64位的有符号二进制补码整数
|
char
|
值是表示Unicode字符的16位无符号整数
|
浮点:
float
|
值是32位IEEE754浮点数
|
double
|
值是64位IEEE754浮点数 |
returnAddress类型的值是Java虚拟机指令的操作码的指针。
对比java的基本数据类型,jvm的规范中没有boolean类型。这是因为jvm中堆boolean的操作是通过int类型来进行处理的,而boolean数组则是通过byte数组来进行处理。
至于String,我们知道它存储在常量池中,但他不是基本数据类型,之所以可以存在常量池中,是因为这是JVM的一种规定。如果查看String源码,我们就会发现,String其实就是一个基于基本数据类型char的数组。如图:
2.字节码文件
通过字节码文件的格式我们可以看出jvm是如何规范数据类型的。下面是ClassFile的结构:
magic:
魔数,魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的Class文件。魔数值固定为0xCAFEBABE,不会改变。
minor_version、major_version:
分别为Class文件的副版本和主版本。它们共同构成了Class文件的格式版本号。不同版本的虚拟机实现支持的Class文件版本号也相应不同,高版本号的虚拟机可以支持低版本的Class文件,反之则不成立。
constant_pool_count:
常量池计数器,constant_pool_count的值等于constant_pool表中的成员数加1。
constant_pool[]:
常量池,constant_pool是一种表结构,它包含Class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其它常量。常量池不同于其他,索引从1开始到constant_pool_count -1。
access_flags:
访问标志,access_flags是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。access_flags的取值范围和相应含义见下表:
this_class:
类索引,this_class的值必须是对constant_pool表中项目的一个有效索引值。constant_pool表在这个索引处的项必须为CONSTANT_Class_info类型常量,表示这个Class文件所定义的类或接口。
super_class:
父类索引,对于类来说,super_class的值必须为0或者是对constant_pool表中项目的一个有效索引值。
如果它的值不为0,那constant_pool表在这个索引处的项必须为CONSTANT_Class_info类型常量,表示这个Class文件所定义的类的直接父类。
当然,如果某个类super_class的值是0,那么它必定是java.lang.Object类,因为只有它是没有父类的。
interfaces_count:
接口计数器,interfaces_count的值表示当前类或接口的直接父接口数量。
interfaces[]:
接口表,interfaces[]数组中的每个成员的值必须是一个对constant_pool表中项目的一个有效索引值,它的长度为interfaces_count。每个成员interfaces[i] 必须为CONSTANT_Class_info类型常量。
fields_count:
字段计数器,fields_count的值表示当前Class文件fields[]数组的成员个数。
fields[]:
字段表,fields[]数组中的每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接口中某个字段的完整描述。
methods_count:
方法计数器,methods_count的值表示当前Class文件methods[]数组的成员个数。
methods[]:
方法表,methods[]数组中的每个成员都必须是一个method_info结构的数据项,用于表示当前类或接口中某个方法的完整描述。
attributes_count:
属性计数器,attributes_count的值表示当前Class文件attributes表的成员个数。
attributes[]:
属性表,attributes表的每个项的值必须是attribute_info结构。
四、一个java类的实例分析
为了了解JVM的数据类型规范和内存分配的大体情况,我新建了MemeryTest.java:
编译为MemeryTest.class后,通过WinHex查看该文件,对应字节码文件各个部分不同的定义,我了解了下面16进制数值的具体含义,尽管不清楚ClassLoader的具体实现逻辑,但是可以想象这样一个严谨格式的文件给JVM对于内存管理和执行程序提供了多大的帮助。
运行程序后,我在windows资源管理器中找到对应的进程ID.
并且在控制台通过jmap -heap 10016查看堆内存的使用情况:
输出结果中表示当前java进程启动的JVM是通过4个线程进行Parallel GC,堆的最小FreeRatio是40%,堆的最大FreeRatio是70%,堆的大小是4090M,新对象占用1.5M,Young Generation可以扩展到最大是1363M, Tenured Generation的大小是254.5M,以及NewRadio和SurvivorRadio中,下面更是具体给出了目前Young Generation中1.5M的划分情况,Eden占用1.0M,使用了5.4%,Space占了0.5M,使用了93%,To Space占了0.5M,使用了0%。
下面我们通过jmap dump把heap的内容打印打文件中:
使用Eclipse的MAT插件或者Jmater打开对应的文件:
选择第一项内存泄露分析报告打开test.bin文件,展示出来的是MAT关于内存可能泄露的分析。
从结果来看,有3个地方可能存在内存泄露,他们占据了Heap的22.10%,13.78%,14.69%,如果内存泄露,这里一般会有一个比值非常高的对象。打开第一个Probem Suspect,结果如下:
ShallowHeap是对象本身占用的堆大小,不包含引用,RetainedHeap是对象所持有的Shallowheap的大小,包括自己ShallowHeap和可以引用的对象的ShallowHeap。
垃圾回收的时候,如果一个对象不再引用后被回收,那么他的RetainedHeap是能回收的内存总和。
通过上图可以看出程序中并没有什么内存泄露,可以放心了。
如果还有什么不太确定的对象,则可以通过多个时间点的HeapDumpFile来研究某个对象的变化情况。