JVM 虚拟机将其内存数据分为程序计数器、虚拟机栈、本地方法栈、Java 堆和方法区等部分。
程序计数器用于存放下一条运行的指令;虚拟机栈和本地方法栈用于存放函数调用栈信息; Java堆用于存放Java 程序运行时所需的对象等数据;方法区用于存放程序的类元数据信息。
1. 程序计数器- Program Counter Register
是一块很小内存空间。 由于Java 是支持线程的语言, 当线程数量超过CPU 数量时,线程之间根据时间片轮询抢夺CPU 资源。对于单核CPU 而言, 每一时刻,只能有一个线程在运行,而其他线程必须被切换出去。为此,每一个线程都必须用一个独立的程序计数器, 用于记录下一条要运行的指令。各个线程之间的计数器互不影响,独立工作;是一块线程私有的内存空间。
如果当前线程正在执行一个Java 方法,则程序计数器记录正在执行的Java 字节码地址,如果当前线程正在执行一个Native 方法, 则程序计数器为空。
2. Java 虚拟机栈
Java 虚拟机栈也是线程私有的内存空间, 它和Java 线程在同一时间创建, 它保存方法的局部变量、部分结果,并参与方法的调用和返回。
Java虚拟机规范允许Java 栈的大小是动态的或是固定的。在Java 虚拟机规范中,定义了两种异常与栈空间有关: StackOverflowError 和OutOfMemoryError.
如果线程在计算过程中, 请求的栈深度大于最大可用的栈深度,则抛出StackOverflowError;
如果Java 栈可以动态扩展,而在扩展栈的过程中, 没有足够的内存空间来支持栈的扩展,则抛出OutOfMemoryError.
在HotSpot 虚拟机中, 可以使用-Xss参数来设置栈的大小。
看一段代码:
TestStack.java
package com.oscar999.performance.JVMTune;
import org.junit.Test;
public class TestStack {
private int count = 0;
public void recursion(){
count ++;
recursion();
}
@Test
public void testStatck(){
try{
recursion();
}catch(Throwable e){
System.out.println("deep of stack is "+count);
e.printStackTrace();
}
}
}
如果调整stack space 的大小, 在eclipse中, 如下设置
运行结果:
深度增加了不少。
虚拟机栈在运行时使用一种叫做栈帧的数据结构保存上下文数据。在栈帧中,存放了方法的局部变量表、操作数栈、动态连接方法和返回地址等信息。
每一个方法的调用都伴随着栈帧的入栈操作, 相应地,方法的返回则表示栈帧的出栈操作。如果方法调用时,方法的参数和局部变量相对较多,那么栈帧中的局部变量就会比较大,栈帧会膨胀以满足方法调用所需传递的信息, 因此, 单个方法调用所需的栈空间大小也会比较多。
栈帧的结果如下:
使用jclasslib 工具可以查看class文件中每个方法所分配的最大局部变量表的容量。
可以到 http://sourceforge.net/projects/jclasslib/files/jclasslib 中下载
可以使用jclasslib 查看一下以下类的class文件所需要的字
package com.oscar999.performance.JVMTune;
public class TestWordReuse {
public void test1() {
{
long a = 0;
}
long b = 0;
}
public void test2(){
long a = 0;
long b = 0;
}
}
结果一个是 3, 一个是5.
基于此, 看一下系统GC 回收的例子:
package com.oscar999.performance.JVMTune;
public class SystemGC {
/**
* GC 无法回收b, 因为b 还在局部变量中
*/
public static void test1() {
{
byte[] b = new byte[6 * 1024 * 1024];
}
System.gc();
System.out.println("first explict gc over");
}
/**
* GC 无法回收, 因为 赋值为null将销毁局部变量表中的数据
*/
public static void test2() {
{
byte[] b = new byte[6 * 1024 * 1024];
b=null;
}
System.gc();
System.out.println("first explict gc over");
}
/**
* GC 可以回收, 因为变量a 复用了变量b 的字,GC根无法找到b
*/
public static void test3() {
{
byte[] b = new byte[6 * 1024 * 1024];
}
int a=0;
System.gc();
System.out.println("first explict gc over");
}
/**
* GC 无法回收, 因为变量a 复用了变量c 的字,b 仍然存在
*/
public static void test4() {
{
int c = 0;
byte[] b = new byte[6 * 1024 * 1024];
}
int a=0;
System.gc();
System.out.println("first explict gc over");
}
/**
* GC 可以回收, 因为变量a 复用了变量c 的字,变量d 复用了变量b 的字
*/
public static void test5() {
{
int c = 0;
byte[] b = new byte[6 * 1024 * 1024];
}
int a=0;
int d=0;
System.gc();
System.out.println("first explict gc over");
}
/**
*
* 总是可以回收b , 因为上层函数的栈帧已经销毁
*/
public static void main(String args[]){
test1();
System.gc();
System.out.println("second explict gc over");
}
}
本地方法栈和Java 虚拟机栈的功能很相似, Java 虚拟机栈用于管理Java函数的调用,而本地方法栈用于管理本地方法的调用。 本地方法并不是用Java实现的,而是用C实现的。 在SUN 的Hot Spot虚拟机中,不区分本地方法栈和虚拟机栈。因此,和虚拟机栈一样,它也会抛出StackOverflowError 和OutOfMemoryError.
4. Java 堆
Java运行时内存中最为重要的部分, 几乎所有的对象和数组都是在堆中分配空间的。Java堆分为新生代和老年代两个部分。新生代用于存放刚刚产生的对象和年轻的对象,如果对象一直没有被回收,生存得足够长,老年对象就会被移入老年代。
新生代又分为:eden(伊甸园)、survivor space0(from space), survivor space1(to space)
看一下以下代码的执行情况:
package com.oscar999.performance.JVMTune;
public class TestHeapGC {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
byte[] b1 = new byte[1024*1204/2];
byte[] b2 = new byte[1024*1204*8];
b2 = null;
b2 = new byte[1204*1204*8];
//System.gc();
}
}
使用命令行运行:
java -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -Xms40M -Xmx40M -Xmn20M com.oscar999.performance.JVMTune.TestHeapGC
运行结果
把上面mark 的 System.GC 打开,再运行一下
可以看到,在Full GC之后, 新生代空间被清空,未被回收的对象全部被移入老生代。
5. 方法区
与堆空间类似,它也是被JVM中所有的线程共享的。方法去主要保存的信息是类的元数据。
方法区中作为重要的是类的类型信息、常量池、域信息、方法信息。
在Hot Spot 虚拟机中, 方法区也被称为永久区,是一块独立于Java堆的内存空间。虽然叫做永久区,但是在永久区的对象,同样也是可以被 GC回收的。
对永久区GC的回收,通常从两个方面进行分析: 1. GC 对永久区常量池的回收 2. 永久区对类元数据的回收。
看如下代码:
package com.oscar999.performance.JVMTune;
public class TestPermGenGC {
public void permGenGC(){
for(int i=0;i
java -XX:PermSize=2M -XX:MaxPermSize=4M -XX:+PrintGCDetails com.oscar999.performance.JVMTune.TestPermGenGC
会发现一直打印如下日志:
也就是说, 每当常量池饱和时,Full GC总能顺利回收常量池数据,确保程序稳定持续运行。
再来看看类元数据的回收情况,
这里要动态生成类的实例,要用到 javassist
可以到如下地址下载:
http://www.java2s.com/Code/Jar/j/Downloadjavassistjar.htm
JavaBeanObject.java
package com.oscar999.performance.JVMTune;
public class JavaBeanObject {
private String name = "java";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.oscar999.performance.JVMTune;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
public class TestOneClassLoad {
public void testOneClassLoad() throws CannotCompileException, NotFoundException, InstantiationException, IllegalAccessException {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
CtClass c = ClassPool.getDefault().makeClass("Geym"+i);
c.setSuperclass(ClassPool.getDefault().get("com.oscar999.performance.JVMTune.JavaBeanObject"));
Class clz = c.toClass();
JavaBeanObject v = (JavaBeanObject)clz.newInstance();
}
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
TestOneClassLoad test = new TestOneClassLoad();
try {
test.testOneClassLoad();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
java -classpath .;../lib/javassist.jar -XX:PermSize=2M -XX:MaxPermSize=4M -XX:+PrintGCDetails com.oscar999.performance.JVMTune.TestOneClassLoad
持久代溢出, Full GC在这种情况下不能回收类的元数据。
事实上,如果虚拟机确认该类的所有实例已经被回收,并且加载该类的ClassLoader已经被回收, GC就有可能回收该类型。
如果新增Class MyClassLoader
package com.oscar999.performance.JVMTune;
public class MyClassLoader extends ClassLoader {
}
TestOneClassLoad 修改成:
package com.oscar999.performance.JVMTune;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
public class TestOneClassLoad {
static MyClassLoader c1 = new MyClassLoader();
public void testOneClassLoad() throws CannotCompileException, NotFoundException, InstantiationException, IllegalAccessException {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
CtClass c = ClassPool.getDefault().makeClass("Geym"+i);
c.setSuperclass(ClassPool.getDefault().get("com.oscar999.performance.JVMTune.JavaBeanObject"));
Class clz = c.toClass(c1,null);
JavaBeanObject v = (JavaBeanObject)clz.newInstance();
if(i%10==0)
c1 = new MyClassLoader();
}
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
TestOneClassLoad test = new TestOneClassLoad();
try {
test.testOneClassLoad();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
1. 设置最大堆内存
使用 -Xmx 参数指定。 最大堆指的是新生代和老生代的大小之和的最大值, 它是Java 应用程序的堆上限。
看例子:
package com.oscar999.performance.JVMTune;
import java.util.Vector;
public class TestXmx {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Vector v = new Vector();
for(int i=1;i<=10;i++){
byte[] b = new byte[1024*1024];
v.add(b);
System.out.println(i+"M is allocated");
}
System.out.println("Max memory:"+Runtime.getRuntime().maxMemory()/1024/1024+"M");
}
}
使用如下命令行:
java -Xmx5M com.oscar999.performance.JVMTune.TestXmx
2. 设置最小堆内存
使用JVM参数 -Xms 可以用于设置系统的最小堆空间。 也就是JVM启动时,所占据的操作系统内存大小。
Java 应用程序在运行时,首先会被分配-Xms指定的内存大小, 并尽可能尝试在这个空间内运行程序。当-Xms 指定的内存大小确实无法满足应用程序时,JVM 才会向操作系统申请更多的内存,直到内存大小达到-Xmx指定的最大内存为止。若超过-Xmx的值,则抛出OutOfMemoryError异常。
如果-Xms的数值较小,那么JVM为了保证系统尽可能地在指定内存范围内运行,就会更加频繁地进行GC操作,以释放失效的内存空间,从而, 会增加Minor GC 和 Full GC的次数, 对系统性能产生一定的影响。
package com.oscar999.performance.JVMTune;
import java.util.Vector;
public class TestXms {
public static void main(String args[]) {
Vector v = new Vector();
for (int i = 1; i <= 10; i++) {
byte[] b = new byte[1024 * 1024];
v.add(b);
if (v.size() == 3)
v.clear();
}
}
}
java -Xmx11M -Xms4M -verbose:gc com.oscar999.performance.JVMTune.TestXms
结果:
3. 设置新生代
参数-Xmn 用于设置新生代的大小。 设置一个较大的新生代会减小老生代的大小,这个参数对系统性能以及GC行为有很大的影响。新生代的大小一般设置为整个堆空间的1/4 到1/3 左右。
在Hot Spot 虚拟机中, -XX:NewSize用于设置新生代的初始大小, -XX:MaxNewSize 用于设置新生代的最大值。但通常情况下,只设置-Xmn已经可以满足绝大部分应用的需要。 设置-Xmn 的效果等同与设置了相同的-XX:NewSize 和 -XX:MaxNewSize.
4. 设置持久代
持久代(方法区)不属于堆的一部分。在Hot Spot 虚拟机中, 使用-XX:MaxPermSize 可以设置持久代的最大值,使用-XX:PermSize可以设置持久代的初始大小。
持久代的大小直接决定了系统可以支持多个类定义和多少常量。对于使用CGLIB或者Javassist 等动态字节码生成工具的应用程序而言,设置合理的持久代大小有助于维持系统稳定。
5. 设置线程栈
线程栈是线程的一块私有空间。
在JVM中, 可以使用 -Xss参数设置线程栈的大小。
栈大小与线程数的关系。
6. 堆的比例分配
-XX:SurvivorRatio 是用来设置新生代中, eden 空间和s0空间的比例关系。 s0和s1空间又分别被称为from空间和to空间。
参数总结: