JVM入门——运行时数据区
jdk1.7.0_79
这张图我相信基本上对JVM有点接触的都应该很熟悉,可以说这是JVM入门的第一课。其中的“堆”和“虚拟机栈(栈)”更是耳熟能详。下面将围绕这张图对JVM的运行时数据区做一个简单介绍。
程序计数器(Program Counter Register)
这和计算机操作系统中的程序计数器类似,在计算机操作系统中程序计数器表示这个进程要执行的下个指令的地址,对于JVM中的程序计数器可以看做是当前线程所执行的字节码的行号指示器,每个线程都有一个程序计数器(这很好理解,每个线程都有在执行任务,如果线程切换后要能保证能恢复到正确的位置),重要的一点——程序计数器,这是JVM规范中唯一一个没有规定会导致OutOfMemory(内存溢出,下文简称OOM)的区域。换句话上图中的其余4个区域,都有可能导致OOM。
☆虚拟机栈(Java Virtual Machine Stacks)
这块内存区域就是我们常常说的“栈”,我们所熟知的是它用于存放变量,也就是说例如:
int i = 0;
虚拟机栈内存就会用4个字节来存储i变量。对于变量的内存空间是一开始就能确定的(对于引用型变量,它当然存储的就是一个地址引用,其大小也是固定),所以这块内存区域在编译器就能够确定下来,这块区域可能会抛出StackOverflowError或者OOM错误。设置JVM参数”-Xss228k”(栈大小为228k)。
1 package com.jvm; 2 3 /** 4 * -Xss228k,虚拟机栈大小为228k 5 * Created by yulinfeng on 7/11/17. 6 */ 7 public class Test { 8 private static int count = 0; 9 10 public static void main(String[] args) { 11 Test test = new Test(); 12 test.test(); 13 } 14 15 /** 16 * 递归调用 17 */ 18 private void test() { 19 try { 20 count++; 21 test(); 22 } catch (Throwable e) { //Exception已经捕获不了JVM抛出的StackOverflowError 23 System.out.println("递归调用次数" + count); 24 e.printStackTrace(); 25 } 26 } 27 }
这是一段没有终止条件的递归,执行结果如下图所示,JVM抛出StackOverflowError表示线程请求的栈深度大于JVM所允许的深度。
对于单线程情况下,无论如何抛出的都是StackOverflowError。如果要抛出OOM异常,导致的原因是不断地在创建线程,直到将内存消耗殆尽。
JVM的内存由堆内存 + 方法区内存 + 剩余内存,也就是剩余内存=操作系统分配给JVM的内存 - 堆内存 - 方法区内存。-Xss设置的是每个线程的栈容量,也就是说可以创建的线程数量 = 剩余内存 / 栈内存。此时如果栈内存越大,可以创建的线程数量就少,就容易出现OOM;如果栈内存越小,可以创建的线程数量就多,就不容易出现OOM。
要避免这种情况最好就是减少堆内存+方法区内存,或者适当减少栈内存。对于栈内存的配置,一般采用默认值1M,或者采用64位操作系统以及64位的JVM。
本地方法栈(Native Method Stack)
本地方法栈和虚拟机栈类似,不同的是虚拟机栈服务的是Java方法,而本地方法栈服务的是Native方法。在HotSpot虚拟机实现中是把本地方法栈和虚拟机栈合二为一的,同理它也会抛出StackOverflowError和OOM异常。
☆Java堆(Java Heap)
对于堆,Java程序员都知道对象实例以及数组内存都要在堆上分配。堆不再被线程所独有而是共享的一块区域,它的确是用来存放对象实例,也是垃圾回收GC的主要区域。实际上它还能细分为:新生代(Young Generation)、老年代(Old Generation)。对于新生代又分为Eden空间、From Survivor空间、To Survivor空间。至于为什么这么分,这涉及JVM的垃圾回收机制,在这里不做叙述。堆同样会抛出OOM异常,下面例子设置JVM参数” -Xms20M -Xmx20M“(前者表示初始堆大小20M,后者表示最大堆大小20M)。
1 package com.jvm; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 /** 7 * -Xms20M -Xmx20M 堆初始大小20M 堆最大大小20M 8 * Created by yulinfeng on 7/11/17. 9 */ 10 public class Test { 11 12 public static void main(String[] args) { 13 List<Test> list = new ArrayList<Test>(); 14 int count = 0; 15 try { 16 while (true) { 17 count++; 18 list.add(new Test()); //不断创建线程 19 } 20 } catch (Throwable e) { 21 System.out.println("创建实例个数:" + count); 22 e.printStackTrace(); 23 } 24 25 } 26 }
执行的结果可以清楚地看到堆上的内存空间溢出了。
☆方法区(Method Area)
对于JVM的方法区,可能听得最多的是另外一个说法——永久代(Permanent Generation),呼应堆的新生代和老年代。方法区和堆的划分是JVM规范的定义,而不同虚拟机有不同实现,对于Hotspot虚拟机来说,将方法区纳入GC管理范围,这样就不必单独管理方法区的内存,所以就有了”永久代“这么一说。方法区和操作系统进程的正文段(Text Segment)的作用非常类似,它存储的是已被虚拟机加载的类信息、常量(从JDK7开始已经移至堆内存中)、静态变量等数据。现设置JVM参数为”-XX:MaxPermSize=20M”(方法区最大内存为20M)。
1 package com.jvm; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 /** 7 * -XX:MaxPermSize=20M 方法区最大大小20M 8 * Created by yulinfeng on 7/11/17. 9 */ 10 public class Test { 11 12 public static void main(String[] args) { 13 List<String> list = new ArrayList<String>(); 14 int i = 0; 15 while (true) { 16 list.add(String.valueOf(i++).intern()); //不断创建线程 17 } 18 } 19 }
实际上对于以上代码,在JDK6、JDK7、JDK8运行结果均不一样。原因就在于字符串常量池在JDK6的时候还是存放在方法区(永久代)所以它会抛出OutOfMemoryError:Permanent Space;而JDK7后则将字符串常量池移到了Java堆中,上面的代码不会抛出OOM,若将堆内存改为20M则会抛出OutOfMemoryError:Java heap space;至于JDK8则是纯粹取消了方法区这个概念,取而代之的是”元空间(Metaspace)“,所以在JDK8中虚拟机参数”-XX:MaxPermSize”也就没有了任何意义,取代它的是”-XX:MetaspaceSize“和”-XX:MaxMetaspaceSize”等。