本节解说:本小节首先按照数据存储方式对程序计数器、虚拟机栈、本地方法栈、堆和方法区等做了一定程度的介绍,接着结合 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 总能顺利回收常量池数据,确保程序稳定持续进行。
一级建造师二级建造师消防工程师造价工程师土建职称房地产经纪人公路检测工程师建筑八大员注册建筑师二级造价师监理工程师咨询工程师房地产估价师 城乡规划师结构工程师岩土工程师安全工程师设备监理师环境影响评价土地登记代理公路造价师公路监理师化工工程师暖通工程师给排水工程师计量工程师
执业药师执业医师卫生资格考试卫生高级职称护士资格证初级护师主管护师住院医师临床执业医师临床助理医师中医执业医师中医助理医师中西医医师中西医助理口腔执业医师口腔助理医师公共卫生医师公卫助理医师实践技能内科主治医师外科主治医师中医内科主治儿科主治医师妇产科医师西药士/师中药士/师临床检验技师临床医学理论中医理论