本节解说:本小节首先按照数据存储方式对程序计数器、虚拟机栈、本地方法栈、堆和方法区等做了一定程度的介绍,接着结合 JVM 各种参数配置对 Java 应用程序进行优化,并通过具体实例帮助读者找到优化 Java 代码的方法。
Java 虚拟机内存模型是 Java 程序运行的基础。为了能使 Java 应用程序正常运行,JVM 虚拟机将其内存数据分为程序计数器、虚拟机栈、本地方法栈、Java 堆和方法区等部分。
程序计数器 (Program Counter Register) 是一块很小内存空间,由于 Java 是支持线程的语言,当线程数量超过 CPU 数量时,线程之间根据时间片轮询抢夺 CPU 资源。对于单核 CPU 而言,每一时刻只能有一个线程在运行,而其他线程必须被切换出去。为此,每一个线程都必须用一个独立的程序计数器,用于记录下一条要运行的指令。各个线程之间的计数器互不影响,独立工作,是一块线程独有的内存空间。如果当前线程正在执行一个 Java 方法,则程序计数器记录正在执行的 Java 字节码地址,如果当前线程正在执行一个 Native 方法,则程序计数器为空。
虚拟机栈用于存放函数调用堆栈信息。Java 虚拟机栈也是线程私有的内存空间,它和 Java 线程在同一时间创建,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
Java 虚拟机规范允许 Java 栈的大小是动态的或者是固定的。在 Java 虚拟机规范中定义了两种异常与栈空间有关:StackOverflowError 和 OutOfMemoryError。如果线程在计算过程中,请求的栈深度大于最大可用的栈深度,则抛出 StackOverflowError;如果 Java 栈可以动态扩展,而在扩展栈的过程中没有足够的内存空间来支持栈的发展,则抛出 OutOfMemeoryError。可以使用-Xss 参数来设置栈的大小,栈的大小直接决定了函数调用的可达深度。
下面的例子展示了一个递归调用的应用。计数器 count 记录了递归的层次,这个没有出口的递归函数一定会导致栈溢出。程序则在栈溢出时,打印出栈的当前深度。
清单 1. 递归调用显示栈的最大深度
public class TestStack {
private int count = 0 ;
//没有出口的递归函数
public void recursion(){
count++; //每次调用深度加 1
recursion(); //递归
}
public void testStack(){
try {
recursion();
} catch (Throwable e){
System.out.println( "deep of stack is " +count); //打印栈溢出的深度
e.printStackTrace();
}
}
public static void main(String[] args){
TestStack ts = new TestStack();
ts.testStack();
}
}
|
清单 2. 清单 1 运行结果
java.lang.StackOverflowError
at TestStack.recursion(TestStack.java: 7 )
at TestStack.recursion(TestStack.java: 7 )
at TestStack.recursion(TestStack.java: 7 )
at TestStack.recursion(TestStack.java: 7 )
at TestStack.recursion(TestStack.java: 7 )
at TestStack.recursion(TestStack.java: 7 )
at TestStack.recursion(TestStack.java: 7 )deep of stack is 9013
|
虚拟机栈在运行时使用一种叫做栈帧的数据结构保存上下文数据。在栈帧中,存放了方法的局部变量表、操作数栈、动态连接方法和返回地址等信息。每一个方法的调用都伴随着栈帧的入栈操作。相应地,方法的返回则表示栈帧的出栈操作。如果方法调用时,方法的参数和局部变量相对较多,那么栈帧中的局部变量表就会比较大,栈帧会膨胀以满足方法调用所需传递的信息。因此,单个方法调用所需的栈空间大小也会比较多。
函数嵌套调用的次数由栈的大小决定。栈越大,函数嵌套调用次数越多。对一个函数而言,它的参数越多,内部局部变量越多,它的栈帧就越大,其嵌套调用次数就会减少。
本地方法栈和 Java 虚拟机栈的功能很相似,本地方法栈用于存放函数调用堆栈信息。Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈用于管理本地方法的调用。本地方法并不是用 Java 实现的,而是使用 C 实现的。在 SUN 的 HotSpot 虚拟机中,不区分本地方法栈和虚拟机栈。因此,和虚拟机栈一样,它也会抛出 StackOverflowError 和 OutofMemoryError。
堆用于存放 Java 程序运行时所需的对象等数据。几乎所有的对象和数组都是在堆中分配空间的。Java 堆分为新生代和老生代两个部分,新生代用于存放刚刚产生的对象和年轻的对象,如果对象一直没有被回收,生存得足够长,老年对象就被移入老年代。新生代又可进一步细分为 eden、survivor space0 和 survivor space1。eden 即对象的出生地,大部分对象刚刚建立时都会被存放在这里。survivor 空间是存放其中的对象至少经历了一次垃圾回收,并得以幸存下来的。如果在幸存区的对象到了指定年龄仍未被回收,则有机会进入老年代 (tenured)。下面例子演示了对象在内存中的分配方式。
清单 3. 进行一次新生代 GC
public class TestHeapGC {
public static void main(String[] args){
byte [] b1 = new byte [ 1024 * 1024 / 2 ];
byte [] b2 = new byte [ 1024 * 1024 * 8 ];
b2 = null ;
b2 = new byte [ 1024 * 1024 * 8 ]; //进行一次新生代 GC
System.gc();
}
}
|
清单 4. 清单 3 的配置
-XX:+PrintGCDetails -XX:SurvivorRatio= 8 -XX:MaxTenuringThreshold= 15 -Xms40M
-Xmx40M -Xmn20M
|
清单 5. 清单 3 的输出
[GC [DefNew: 9031K->661K(18432K), 0.0022784 secs]
9031K->661K(38912K),
0.0023178 secs] [Times: user= 0.02 sys= 0.00 , real= 0.02 secs]
Heap
def new generation total 18432K, used 9508K [ 0x34810000 , 0x35c10000 ,
0x35c10000 )
eden space 16384K, 54 % used [ 0x34810000 , 0x350b3e58 , 0x35810000 )
from space 2048K, 32 % used [ 0x35a10000 , 0x35ab5490 , 0x35c10000 )
to space 2048K, 0 % used [ 0x35810000 , 0x35810000 , 0x35a10000 )
tenured generation total 20480K, used 0K [ 0x35c10000 , 0x37010000 ,
0x37010000 )
the space 20480K, 0 % used [ 0x35c10000 , 0x35c10000 , 0x35c10200 ,
0x37010000 )
compacting perm gen total 12288K, used 374K [ 0x37010000 , 0x37c10000 ,
0x3b010000 )
the space 12288K, 3 % used [ 0x37010000 , 0x3706db10 , 0x3706dc00 ,
0x37c10000 )
ro space 10240K, 51 % used [ 0x3b010000 , 0x3b543000 , 0x3b543000 ,
0x3ba10000 )
rw space 12288K, 55 % used [ 0x3ba10000 , 0x3c0ae4f8 , 0x3c0ae600 ,
0x3c610000 )
|
上述输出显示 JVM 在进行多次内存分配的过程中,触发了一次新生代 GC。在这次 GC 中,原本分配在 eden 段的变量 b1 被移动到 from 空间段 (s0)。最后分配的 8MB 内存被分配在 eden 新生代。
方法区用于存放程序的类元数据信息。方法区与堆空间类似,它也是被 JVM 中所有的线程共享的。方法区主要保存的信息是类的元数据。方法区中最为重要的是类的类型信息、常量池、域信息、方法信息。类型信息包括类的完整名称、父类的完整名称、类型修饰符和类型的直接接口类表;常量池包括这个类方法、域等信息所引用的常量信息;域信息包括域名称、域类型和域修饰符;方法信息包括方法名称、返回类型、方法参数、方法修饰符、方法字节码、操作数栈和方法栈帧的局部变量区大小以及异常表。总之,方法区内保持的信息大部分来自于 class 文件,是 Java 应用程序运行必不可少的重要数据。
在 Hot Spot 虚拟机中,方法区也称为永久区,是一块独立于 Java 堆的内存空间。虽然叫做永久区,但是在永久区中的对象同样也可以被 GC 回收的。只是对于 GC 的表现也和 Java 堆空间略有不同。对永久区 GC 的回收,通常主要从两个方面分析:一是 GC 对永久区常量池的回收;二是永久区对类元数据的回收。Hot Spot 虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。
清单 6 所示代码会生成大量 String 对象,并将其加入常量池中。String.intern() 方法的含义是如果常量池中已经存在当前 String,则返回池中的对象,如果常量池中不存在当前 String 对象,则先将 String 加入常量池,并返回池中的对象引用。因此,不停地将 String 对象加入常量池会导致永久区饱和。如果 GC 不能回收永久区的这些常量数据,那么就会抛出 OutofMemoryError 错误。
清单.6 GC 收集永久区
public class permGenGC {
public static void main(String[] args){
for ( int i= 0 ;i
String t = String.valueOf(i).intern(); //加入常量池
}
}
}
|
清单 7. 清单 6 的配置
-XX:PermSize=2M -XX:MaxPermSize=4M -XX:+PrintGCDetails
|
清单 8. 清单 6 的输出
[Full GC [Tenured: 0K->149K(10944K), 0.0177107 secs]
3990K->149K(15872K),
[Perm : 4096K->374K(4096K)], 0.0181540 secs] [Times: user= 0.02 sys= 0.02 ,
real= 0.03 secs]
[Full GC [Tenured: 149K->149K(10944K), 0.0165517 secs]
3994K->149K(15936K),
[Perm : 4096K->374K(4096K)], 0.0169260 secs] [Times: user= 0.01 sys= 0.00 ,
real= 0.02 secs]
[Full GC [Tenured: 149K->149K(10944K), 0.0166528 secs]
3876K->149K(15936K),
[Perm : 4096K->374K(4096K)], 0.0170333 secs] [Times: user= 0.02 sys= 0.00 ,
real= 0.01 secs]
|
每当常量池饱和时,FULL GC 总能顺利回收常量池数据,确保程序稳定持续进行。
2015职称计算机考试书PowerPoint2007中 .. 定价:¥45 优惠价:¥42 更多书籍 | |
2015年全国职称计算机考试教材(2007模 .. 定价:¥225 优惠价:¥213 更多书籍 |