1. JVM内存模型
2.程序计数器
程序计数器是一个很小的内存空间。由于Java是支持多线程的语音,当线程数量超过cpu数量,线程之间根据时间片轮询抢夺CPU资源。对于单核CPU而言,每一时刻只能有一个线程在执行,而其他线程必须被切换出去。为此每一个线程必须有一个独立的程序计数器,用于记录下一条要执行的指令。各个线程之间的计数器互不影响,独立工作。是一块线程私有的内存空间。
如果当前线程正在执行一个Java方法,程序计数器中存放的就是正在执行的Java字节码地址,如果正在执行的是native方法,则程序计数器为空。
3.虚拟机栈
Java虚拟机栈也是线程的私有的内存空间,它和Java线程在同一时刻创建,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
JAVA规范中允许Java栈的大小是动态的或者是固定不变的;Java虚拟机中定义了两种异常与栈空间有关
StackOverflowError:如果线程在计算过程中,请求的栈深度大于最大可用的栈深度,则抛出该错误。
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.StackOverflowError
如果系统需要更深的栈调用,设置-Xss2m
执行结果:
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之后内存模型图