JVM调优之:内存模型

1. JVM内存模型

2.程序计数器

程序计数器是一个很小的内存空间。因为Java是支持多线程的语音,当线程数量超过cpu数量,线程之间根据时间片轮询抢夺CPU资源。对于单核CPU而言,每一时刻只能有一个线程在执行,而其余线程必须被切换出去。为此每个线程必须有一个独立的程序计数器,用于记录下一条要执行的指令。各个线程之间的计数器互不影响,独立工做。是一块线程私有的内存空间。java

若是当前线程正在执行一个Java方法,程序计数器中存放的就是正在执行的Java字节码地址,若是正在执行的是native方法,则程序计数器为空。数组

3.虚拟机栈

Java虚拟机栈也是线程的私有的内存空间,它和Java线程在同一时刻建立,它保存方法的局部变量、部分结果,并参与方法的调用和返回。数据结构

JAVA规范中容许Java栈的大小是动态的或者是固定不变的;Java虚拟机中定义了两种异常与栈空间有关多线程

StackOverflowError:若是线程在计算过程当中,请求的栈深度大于最大可用的栈深度,则抛出该错误。jvm

OutOfMemoryError:若是Java栈能够动态扩展,在扩展过程当中没有足够的内存空间来支持栈的扩展,则抛出该错误。函数

JVM中但是使用-Xss参数来设置栈大小,栈的大小直接决定了函数调用的可达深度。如下代码经过递归调用查看方法调用可达深度。在JDK5.0及其之前栈默认大小为256k,JDK5.0以后jvm栈默认大小为1m。工具

public class StackTest {
    private int count = 0;//记录栈可达深度
    public void recursion(){
        count ++;
        recursion();
    }

    public static void main(String[] args) {
        StackTest stackTest = new StackTest();
        try {
            stackTest.recursion();
        }catch (Throwable e){
            System.out.println("deep of stack is "+stackTest.count);
            e.printStackTrace();
        }
    }
}

默认执行结果:性能

deep of stack is 17127
java.lang.StackOverflowErrorthis

若是系统须要更深的栈调用,设置-Xss2mspa

执行结果:

deep of stack is 73391
java.lang.StackOverflowError

能够看到增长栈空间后,函数调用的栈深度明显增长。

虚拟机栈在运行时使用一种叫作栈帧的数据结构保存上下文数据,在栈帧中存放了方法局部变量表、操做数栈、动态链接方法和返回地址等信息。每一个方法的调用都伴随着入栈,方法的返回伴随着出栈。若是方法调用时方法的参数和局部变量越多那么栈帧中局部变量表就越大,栈帧占用的空间就越大,那么方法调用嵌套次数就越小。

以下方法和上面例子比较

public class StackTest {
    private int count = 0;//记录栈可达深度
    public void recursion(long a,long b,long c){
        long d = 0,f = 0,e = 0;
        count ++;
        recursion(d,f,e);
    }

    public static void main(String[] args) {
        StackTest stackTest = new StackTest();
        try {
            stackTest.recursion(1L,2L,3L);
        }catch (Throwable e){
            System.out.println("deep of stack is "+stackTest.count);
            e.printStackTrace();
        }
    }
}

默认栈大小-Xss1m时执行结果,能够看到当方法参数和局部变量增长时,调用深度明显减少。

deep of stack is 5445
java.lang.StackOverflowError

在栈帧中,与性能调优关系最为密切的的部分就是局部变量表。局部变量表存放方法参数和内部变量,非static方法虚拟机还会把当前对象(this)做为参数经过局部变量表传递给当前方法。

经过jclasslib工具能够查看class文件中每一个方法所分配的最大局部变量表的容量。打开StackTest.calss文件找到方法recursion(),将其展开后查看Code属性,选择Misc页面,能够查看该方法最大局部变量。局部变量表以“字”为单位进行划份内存空间。long/double类型占两个“字”,其余类型占一个“字”。

//共13个“字”
public void recursion(long a,long b,long c){
    long d = 0,e = 0, f = 0;
    count ++;
    recursion(d,e,f);
}

局部变量表中字空间是能够重用的,由于在一个方法体内,局部变量的做用范围并非必定是整个方法体。

public void test1(){
    {
        long a = 0;
    }
    long b = 0;
}

比较

public void test2(){
    long a = 0;
    long b = 0;
}

局部变量表的字对系统GC也有必定影响,若是与几个变量被保存在局部变量表中,那么GC根就能引用到这个局部变量所指向的内存空间,从而GC时没法回收这部分空间。

1. 

public static void test1(){
    {
        Byte[] b = new Byte[1024*1024*6];
    }
    System.gc();
    System.out.println("first explict gc over");
}

虽然系统GC时已经超出了b变量做用范围,可是不会被回收

[GC (System.gc())  26542K->25256K(62976K), 0.0043875 secs]
[Full GC (System.gc())  25256K->25191K(62976K), 0.0326676 secs]
first explict gc over

2. 

public static void test1(){
    {
        Byte[] b = new Byte[1024*1024*6];
        b = null;
    }
    System.gc();
    System.out.println("first explict gc over");
}

手动把b变量设置为null,可使GC时回收b所占用的内存空间。

[GC (System.gc())  26542K->25328K(62976K), 0.0020175 secs]
[Full GC (System.gc())  25328K->627K(62976K), 0.0074723 secs]
first explict gc over

3. 

public static void test1(){
    {
        Byte[] b = new Byte[1024*1024*6];
    }
    int a = 0;
    System.gc();
    System.out.println("first explict gc over");
}

更多的方法是新声明的变量,会复用变量b的字,使b所占的内存空间被GC回收。

[GC (System.gc())  26542K->25384K(62976K), 0.0015061 secs]
[Full GC (System.gc())  25384K->627K(62976K), 0.0077682 secs]
first explict gc over

4. 本地方法栈

本地方法栈和Java虚拟机栈的功能很类似,Java虚拟机栈用于管理Java函数的调用,而本地方法栈用于管理本地方法的调用。所以和Java虚拟机栈同样本地方法栈也会抛出StackOverflowError和OutOfMemoryError。

本地方法栈是使用C语言实现的,在SUN的Hot Spot虚拟机中不区分本地方法栈和Java虚拟中栈。

5. Java堆

Java堆能够说是Java运行时内存中最为重要的部分,几乎全部的对象和数组都在堆中分配空间。Java堆分为新生代和老年代两部分,新生代用来存放新产生的对象和年轻的对象,若是一个对象通过屡次GC都没有被回收,则该对象会被存放到老年代。

新生代

Eden:刚刚产生的对象存放该空间。

s0(from space)/s1(to space):至少通过一次GC没有被回收的对象存放在该空间。

老年代:屡次GC都没有被回收的对象最终会被存放在该空间。

6. 方法区(JDK6)

方法区也是JVM内存中很是重要的的一块内存区域。与堆空间相似,它也是被JVM中因此的线程共享。方法区中主要保存的信息是类的元数据。

方法区存放

类的类型信息:包括类的完整名称,父类的完整名称,类型修饰符,类型的直接接口类表。

常量池:包括这个类方法、域等信息所引用的常量信息。

域信息:包括域名称,域类型和修饰符。

方法信息:包括方法名称,返回类型,方法参数,方法修饰符,方法字节码,操做数栈和方法帧栈的局部变量区大小以及异常表。

在Hot Spot虚拟机中,方法区也成为永久区,是一块独立的内存空间。虽然叫作永久区,可是在永久区中的对象也是能够被回收的。对永久区的回收主要从两个方面分析:一是GC对永久区常量池的回收;二是永久区对类元数据的回收。Hot Spot虚拟机对常量池的回收,只要常量池中的常量没有被任何地方引用就能够被回收。

常量池演示:

String.intern():若是常量池存在当前String,返回常量池中的String;若是常量池中不存在当前String,将当前String添加到常量池,并返回池中对象。

public static void main(String[] args) {
    for (int i = 0;i< Integer.MAX_VALUE;i++){
        String t = String.valueOf(i).intern();
    }
}

使用JVM参数-XX:PermSize=2M -XX:MaxPermSize=4M -XX:+PrintGCDetails运行,每当常量池满时就进行回收,确保程序正常运行。

结果显示,当常量池空间不足时,没有被引用的常量会被回收。

元数据演示:

与常量池的回收相比,类的元数据回收,稍微复杂一些,使用javassist类库,产生大量类占用元数据。观察元数据的回收状况。

动态类父类,生成的子类都要继承给父类

//定义演示动态类的父类,后面使用Javassist产生的动态类都是该类的子类
public class JavaBeanObject{
    private String name="java";
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

动态类生成的

public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
    for (int i=0;i<Integer.MAX_VALUE;i++){//循环动态生成大量类
        CtClass c = ClassPool.getDefault().makeClass("Geym"+i);//定义类名
        c.setSuperclass(ClassPool.getDefault().get("com.eaju.jvm.JavaBeanObject"));//设置父类
        Class clz = c.toClass();//新建类
        JavaBeanObject v = (JavaBeanObject)clz.newInstance();
    }
}

运行参数:-XX:PermSize=2M -XX:MaxPermSize=4M -XX:+PrintGCDetails

以上代码运行会产生大量的JavaBeanObject类的子类,占用元数据,致使永久区空间不足,运行一段时间以后会抛出“java.lang.OutOfMemoryError:PermGen space”显示持久带溢出。

事实上类元数据也是能够被回收的,须要知足如下两个条件:1.该类的全部实例均已经被回收;2.该类的加载器ClassLoader也已经被回收。

7.  方法区(JDK8)

在Java7以前,HotSpot虚拟机中将GC分代收集扩展到了方法区,使用永久代来实现了方法区。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载。可是在以后的HotSpot虚拟机实现中,逐渐开始将方法区从永久代移除。

Java7中已经将运行时常量池从永久代移除,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。而在Java8中,已经完全没有了永久代,将方法区直接放在一个与堆不相连的本地内存区域,这个区域被叫作元空间。 

总之:jdk1,6常量池放在方法区,jdk1.7常量池放在堆内存,jdk1.8放在元空间里面,和堆相独立。

验证常量池

一样的方法验证元数据

//定义演示动态类的父类,后面使用Javassist产生的动态类都是该类的子类
public class JavaBeanObject{
    private String name="java";
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

动态建立类方法

public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
    for (int i=0;i<Integer.MAX_VALUE;i++){//循环动态生成大量类
        CtClass c = ClassPool.getDefault().makeClass("Geym"+i);//定义类名
        c.setSuperclass(ClassPool.getDefault().get("com.eaju.jvm.JavaBeanObject"));//设置父类
        Class clz = c.toClass();//新建类
        JavaBeanObject v = (JavaBeanObject)clz.newInstance();
    }
}

运行参数:-XX:MaxMetaspaceSize=8m  -XX:+PrintGCDetails

限制元空间大小,在不断建立元数据时元空间很快就会被占用完就会抛出异常

[GC (Last ditch collection) [PSYoungGen: 0K->0K(44544K)] 8631K->8631K(132608K), 0.0075906 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Last ditch collection) [PSYoungGen: 0K->0K(44544K)] [ParOldGen: 8631K->8631K(127488K)] 8631K->8631K(172032K), [Metaspace: 7758K->7758K(1056768K)], 0.0336065 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
Heap
 PSYoungGen      total 44544K, used 1662K [0x00000000eb180000, 0x00000000eee80000, 0x0000000100000000)
  eden space 41984K, 3% used [0x00000000eb180000,0x00000000eb31fab0,0x00000000eda80000)
  from space 2560K, 0% used [0x00000000edd00000,0x00000000edd00000,0x00000000edf80000)
  to   space 10240K, 0% used [0x00000000ee480000,0x00000000ee480000,0x00000000eee80000)
 ParOldGen       total 127488K, used 8631K [0x00000000c1400000, 0x00000000c9080000, 0x00000000eb180000)
  object space 127488K, 6% used [0x00000000c1400000,0x00000000c1c6de78,0x00000000c9080000)
 Metaspace       used 7790K, capacity 8098K, committed 8192K, reserved 1056768K
  class space    used 1936K, capacity 1997K, committed 2048K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
    at javassist.ClassPool.toClass(ClassPool.java:1099)
    at javassist.ClassPool.toClass(ClassPool.java:1042)
    at javassist.ClassPool.toClass(ClassPool.java:1000)
    at javassist.CtClass.toClass(CtClass.java:1224)

JDK8以后内存模型图

相关文章
相关标签/搜索