1. 运行时的数据区域
程序计数器
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。线程在执行时就是通过改变这个计数器的值来选择执行的下一条字节码指令。
在多线程环境中,程序计数器是线程私有的,每个线程为了在获得cpu时间片段时能够切换到正确的位置,所以他们都有一个独立的线程计数器,做到互不影响。
Java虚拟机栈
Java虚拟机栈就是我们常说的“栈内存”,它也是线程私有的,它的生命周期与线程相同,每个方法在执行时都会创建一个栈帧用于存储局部变量表、方法出口等信息,每个方法从调用直至执行完成的过程就对应着一个栈帧从入栈到出栈的过程。
局部变量表,顾名思义就是存放局部变量的,包括方法中的基本数据类型、引用数据类型(存放的只是指向真实对象起始地址的引用指针)。
若线程请求的栈深度大于虚拟机栈的深度则会抛出StackOverflowError异常,若在动态扩展时无法申请到足够的内存则会抛出OutOfMemoryError异常。
本地方法栈
本地方法栈就是为虚拟机使用到的Native方法服务的,该区域若溢出也会抛出OutOfMemoryError和StackOverflowError异常。
Java堆
这就是我们常说的“堆内存”,java堆是java虚拟机所管理的最大一块内存,java堆是被所有线程共享的一块内存区域,此内存的唯一目的就是存放对象实例,在java虚拟机规范中的描述:所有对象实例及数组都要在堆上分配。
该区域可以细分为新生代和老年代,该区域若没有足够的内存则会抛出OutOfMemoryError异常。
方法区
方法区是堆的一个逻辑部分,它也是线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
该区习惯上被称为“永久代”,它与java堆一起被GC所管理,当该区域内存不足时会抛出OutOfMemoryError异常。
运行时常量池
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用(String类型的变量就是在这个区间存放的),当内存不足时会抛出OutOfMemoryError异常。
2. 运行时数据存放区域
下面以一段java代码为例,来区分代码是如何在java内存区域中存放的。
/**
* java代码对应的内存存储区域
*/
//编译后的类信息,方法代码,即时编译器编译后的代码等都存放在方法区。
public class Code {
private static Listlist=new ArrayList();//类变量,存放在方法区常量池内
private static final int CONSTANT=1;//常量存放在方法区的常量池内
private List list1=new ArrayList();//实例变量,对象存放在堆区,list1引用指向对象在堆内存中的首地址,存放在对象内存空间中,也就是堆区。
private Class clazz=String.class;//Class类型的对象是存放在方法区
int number;//基本类型的变量存放在堆内存中。
int[] array={1,2,3};//数组对象存放在堆内存中。
public static void main(String[] args) {
// TODO Auto-generated method stub
}
void test(){
int number=0;//局部基本类型变量,存放在栈内存的局部变量表中。
String s="123";//局部字符串变量,存放在方法区的常量池中。
List list2=new ArrayList();//局部引用类型变量,对象放在堆内存中,list2引用向对象在堆内存中的首地址,存放在栈区中的局部变量表中。
}
}
JVM使用局部变量表来完成方法调用时的参数传递,当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot(局部变量表中的最小存储单元)上。访问索引为0的Slot一定存储的是与被调用实例方法相对应的对象引用(通过Java语法层面的“this”关键字便可访问到这个参数),而后续的其他方法参数和方法体内定义的成员变量则会按照顺序从局部变量表中索引为1的Slot位置处展开复制。
3. 内存分配策略
对象的内存分配主要是在堆上分配,对象主要分配在Eden(新生代)区,少数情况下也可能直接分配在老年代中,具体分配的规则取决于当前虚拟机采用的是哪一种垃圾收集器组合,还有相关的参数配置。
以下的讲解基于Parallel Scavenge加Parallel Old收集器,其它类型的收集器组合类同。
对象优先在Eden区分配
大多数情况下对象优先在新生代Eden区中分配,只有当Eden区没有足够的空间时,虚拟机将发生一次MinorGC。
代码清单1-2
* vm参数:
* -verbose:gc
-Xms20m
-Xmx20m
-Xmn10m
-XX:+UseSerialGC
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
private static final int _1MB=1024*1024;
public static void main(String[] args) {
byte[]a1,a2,a3,a4,a5,a6;
a1=new byte[2*_1MB];
a2=new byte[2*_1MB];
a3=new byte[2*_1MB];
a4=new byte[4*_1MB];
}
输出结果:
[GC [DefNew: 6487K->160K(9216K), 0.0661622 secs] 6487K->6304K(19456K), 0.0662172 secs] [Times: user=0.00 sys=0.02, real=0.07 secs]
Heap
def new generation total 9216K, used 4420K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
eden space 8192K, 52%used [0x00000000f9a00000, 0x00000000f9e28fd8, 0x00000000fa200000)
from space 1024K, 15%used [0x00000000fa300000, 0x00000000fa328278, 0x00000000fa400000)
to space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
tenured generation total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
the space 10240K, 60%used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)
compacting perm gen total 21248K, used 3012K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 14% used [0x00000000fae00000, 0x00000000fb0f1180, 0x00000000fb0f1200, 0x00000000fc2c0000)
No shared spaces configured.
代码清单1-2所示,尝试分配3个2MB大小和1个4MB大小的对象,在运行时通过-Xms20m,-Xmx20m,-Xmn10m 三个参数限制了java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。-XX:SurvivorRatio=8决定了新生代中的Eden区与Survivor区的空间比例是8:1,从输出结果中也可以看出eden space 8192K,from space 1024K的信息。
执行main方法后在分配a4对象的时候发生了一次MinorGC,这次GC的结果新生代6487K变成了160K,而总内存几乎没有减少(因为a1,a2,a3都是存活的),这次GC发生的原因是给a4分配内存的时候,发现Eden区已经被占用了6MB,剩余空间已不足够分配a4的4MB空间,因此发生了MinorGC,GC期间虚拟机又发现已有的3个2MB大小的对象无法全部放入Survivor空间(该空间只有1MB大小),所以通过分配担保机制提前转移到老年代中。
这次GC结束后,4MB的a4对象顺利分配在Eden中,因此程序执行完后的结果是Eden占用4MB(被a4占用),Survivor空闲,老年代被占用了6MB(a1,a2,a3占用),通过GC日志可以证实这点。
注意:新生代GC(MinorGC):指发生在新生代的垃圾回收动作,因为java对象大多都具备朝生夕死的特性,所以该区域垃圾回收非常频繁。
老年代GC(MajorGC/FullGC):发生在老年代的GC,出现FullGC至少会伴随一次的MinorGC,FullGC的速度一般会比MinorGC慢10倍以上。
大对象直接进入老年代
代码清单1-3
* vm参数:
* -verbose:gc
-Xms20m
-Xmx20m
-Xmn10m
-XX:+UseSerialGC
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:PretenureSizeThreshold=3145728 *
*/
public class Test {
private static final int _1MB=1024*1024;
public static void main(String[] args) {
byte[]a1,a2,a3,a4,a5,a6;
//a1=new byte[2*_1MB];
//a2=new byte[2*_1MB];
//a3=new byte[2*_1MB];
a4=new byte[4*_1MB];
}
输出结果:
Heap
def new generation total 9216K, used 507K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
eden space 8192K, 6%used [0x00000000f9a00000, 0x00000000f9a7eee0, 0x00000000fa200000)
from space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
tenured generation total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
the space 10240K, 40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)
compacting perm gen total 21248K, used 3008K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 14% used [0x00000000fae00000, 0x00000000fb0f0260, 0x00000000fb0f0400, 0x00000000fc2c0000)
No shared spaces configured.
所谓的对象是指需要连续内存空间的java对象,最典型的大对象就是那种很长的字符串和数组,大对象对于虚拟机来说是一个坏消息,它会不断触发老年代的垃圾回收,影响程序的性能,因此在日常项目中,应尽量避免。
虚拟机提供了一个PretenureSizeThreshold,令大于这个设置值的对象直接进入老年代分配。
执行代码清单1-3,会发现老年代的10MB直接被使用了40%,也就证实了我们的推断。
长期存活的对象将进入老年代分配
虚拟机采用分代手机策略来管理内存,它给每个对象定义了一个年龄计数器,如果对象在Eden区出生并经过第一次的MinorGC后仍然存活,并且能被Survivor区容纳的话,将被移动到Survivor区中,并且对象年龄设为1,对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定岁数(默认为15岁),就将晋升到老年代中,对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。
代码清单1-4
/**
* vm参数:
-verbose:gc
-Xms20m
-Xmx20m
-Xmn10m
-XX:+UseSerialGC
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=1
-XX:+PrintTenuringDistribution
* @author Administrator
*
*/
public class Test {
private static final int _1MB=1024*1024;
public static void main(String[] args) {
byte[]a1,a2,a3;
a1=new byte[_1MB/4];
a2=new byte[_1MB*4];
a3=new byte[_1MB*4];//第一次发生GC
a3=null;
a3=new byte[_1MB*4];//第二次发生GC
}
}
以MaxTenuringThreshold=1参数来运行的结果:
[GC[DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
: 5188K->721K(9216K), 0.0394671 secs] 5188K->4817K(19456K), 0.0538313 secs] [Times: user=0.00 sys=0.00, real=0.05 secs]
[GC[DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
: 4901K->0K(9216K), 0.0015850 secs] 8997K->4816K(19456K), 0.0016154 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4260K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
eden space 8192K, 52% used [0x00000000f9a00000, 0x00000000f9e28fd0, 0x00000000fa200000)
from space 1024K, 0%used [0x00000000fa200000, 0x00000000fa200088, 0x00000000fa300000)
to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
tenured generation total 10240K, used 4816K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
the space 10240K, 47%used [0x00000000fa400000, 0x00000000fa8b42c0, 0x00000000fa8b4400, 0x00000000fae00000)
compacting perm gen total 21248K, used 2573K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 12% used [0x00000000fae00000, 0x00000000fb083720, 0x00000000fb083800, 0x00000000fc2c0000)
No shared spaces configured.
以MaxTenuringThreshold=15参数来运行的结果:
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
: 4695K->417K(9216K), 0.0179032 secs] 4695K->4513K(19456K), 0.0179361 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
: 4513K->417K(9216K), 0.0005743 secs] 8609K->4513K(19456K), 0.0005953 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4841K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
eden space 8192K, 54%used [0x00000000f9a00000, 0x00000000f9e51f98, 0x00000000fa200000)
from space 1024K, 40%used [0x00000000fa200000, 0x00000000fa268560, 0x00000000fa300000)
to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
tenured generation total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
the space 10240K, 40%used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)
compacting perm gen total 21248K, used 3018K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 14% used [0x00000000fae00000, 0x00000000fb0f28b0, 0x00000000fb0f2a00, 0x00000000fc2c0000)
No shared spaces configured.
执行代码清单1-4,当设置MaxTenuringThreshold参数为1时,会发现第一次GC时a1进入了survivor区,所以新生代变成了721k,此时对象的年龄为1,在第二次执行GC时,我们发现新生代变成了0k,此时a1对象已经被分配到老年代中。
当设置MaxTenuringThreshold参数为15时,在第二次GC后,新生代仍然有417k,我们看到survivor区内存使用40%,说明a1仍然存在于该区。
注意:笔者开始用的是1.7版的jdk测试的,发现将该参数设置成15时,测试结果并不会发生变化,而且输出的age年龄仍然为1,并没有自增,说明在1.7版本后,对象已经不存在年龄的限制了,该参数实际上已经没有用了。
动态年龄判定
为了能更好的适应不同额内存状况,虚拟机并不是永远的要求对象年龄必须达到了设定的年龄才能晋升到老年代中,如果在Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代中,无需等到MaxTenuringThreshold要求的年龄。
代码清单1-5
/**
* vm参数:
-verbose:gc
-Xms20m
-Xmx20m
-Xmn10m
-XX:+UseSerialGC
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=15
-XX:+PrintTenuringDistribution
* @author Administrator
*
*/
public class Test {
private static final int _1MB=1024*1024;
public static void main(String[] args) {
byte[]a1,a2,a3,a4;
a1=new byte[_1MB/4];
a2=new byte[_1MB/4];
a3=new byte[_1MB*4];
a4=new byte[_1MB*4];//第一次GC
a4=null;
a4=new byte[_1MB*4];//第二次GC
}
测试结果:
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
: 4951K->673K(9216K), 0.0042349 secs] 4951K->4769K(19456K), 0.0042608 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
: 4933K->0K(9216K), 0.0007229 secs] 9029K->4769K(19456K), 0.0007410 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4260K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
eden space 8192K, 52%used [0x00000000f9a00000, 0x00000000f9e28fd8, 0x00000000fa200000)
from space 1024K, 0%used [0x00000000fa200000, 0x00000000fa200088, 0x00000000fa300000)
to space 1024K, 0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
tenured generation total 10240K, used 4769K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
the space 10240K, 46%used [0x00000000fa400000, 0x00000000fa8a8470, 0x00000000fa8a8600, 0x00000000fae00000)
compacting perm gen total 21248K, used 3018K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 14% used [0x00000000fae00000, 0x00000000fb0f28e8, 0x00000000fb0f2a00, 0x00000000fc2c0000)
No shared spaces configured.
执行代码清单1-5,并设置MaxTenuringThreshold=15,我们会发现运行结果中survivor区空间使用0%,而老年代比预期增长了6%,也就是说a1,a2在第二次GC时直接进入了老年代,而没有等到15岁的年龄阈值,因为这两个对象加起来已经达到了512k,并且他们是同年的,满足同年对象达到survivor空间一半的规则。
空间分配担保
在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立,那么MinorGC是安全的,如果不成立,则会查看HandlePromotionFailure设置值是否允许担保失败,如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次MinorGC,尽管这次MinorGC是有风险的,如果小于,或者HandlePromotionFailure设置为不允许冒险,这时会进行一次FullGC。
但在实际项目中,大部分HandlePromotionFailure是允许冒险的,避免FullGC过于频繁。
这点请读者自行思考设计验证,动手能力还是很重要的。