《深入理解jvm》day2:自动内存管理机制之jvm运行时数据区
这一部分,可以分为4章,分别:
C2:java内存区域与内存溢出异常
C3:垃圾收集器与内存分配策略
C4:虚拟机性能监控与故障处理工具
C5:调优案例分析与实战
1.java内存区域与内存溢出异常
java把内存管理的权限,完全交给jvm来管理,无需程序员来操心内存问题。
1.1.概述
但是,一旦,出现内存泄漏和溢出问题,如果不了解jvm是如何使用内存的话,排查错误就是一件无从下手的事。
因此,需要好好学习jvm内存区域。
1.2.jvm运行时数据区域
1.2.1.程序计数器Program Counter Register
这和计算机操作系统中的程序计数器类似,在计算机操作系统中程序计数器表示这个进程要执行的下个指令的地址,对于JVM中的程序计数器可以看做是当前线程所执行的字节码的行号指示器,每个线程都有一个程序计数器(这很好理解,每个线程都有在执行任务,如果线程切换后要能保证能恢复到正确的位置),重要的一点——程序计数器,这是JVM规范中唯一一个没有规定会导致OutOfMemory(内存泄露,下文简称OOM)的区域。换句话上图中的其余4个区域,都有可能导致OOM。
分支,循环,跳转,异常处理,线程恢复等功能都需要这个计数器来完成。
如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是native方法,这个计数器值为空(undefined)。
1.2.2.虚拟机栈(Java Virtual Machine Stacks)
它也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是java方法执行的内存模型,每个方法执行的同时都会创建一个栈帧。,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。方法从执行到结束,对应一个栈帧在栈中从入栈到出栈的过程。
局部变量表所需内存空间在编译期间完成分配。
栈区可能出现两种异常,第一种:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
如果虚拟机可以动态扩展(大部分VM支持,只不过JVM规范中也允许固定长度的虚拟机栈),如果扩展时无法申请足够的内存,就会抛出OutOfMemoryError异常。
这块内存区域就是我们常常说的“栈”,我们所熟知的是它用于存放变量,也就是说例如:
int i = 0;
虚拟机栈内存就会用4个字节来存储i变量。对于变量的内存空间是一开始就能确定的(对于引用型变量,它当然存储的就是一个地址引用,其大小也是固定),所以这块内存区域在编译期就能够确定下来,这块区域可能会抛出StackOverflowError或者OOM(内存泄漏)错误。设置JVM参数”-Xss228k”(栈大小为228k)。
下面编写一个程序,来说明jvm抛出StackOverflowError。
package com.jvm; /** * * @author wangfei * 测试jvm抛出异常 * -Xss228k,虚拟机栈大小为228k */ public class Test { private static Integer count = 0; public static void main(String[] args) { Test test = new Test(); test.test(); } /** * 递归方法:该方法没有递归结束条件 */ private void test() { try { count ++; test(); }catch(Throwable e) { System.out.println("递归调用次数" + count); e.printStackTrace(); } } }
运行结果,如下图:
说明:
对于单线程情况下,无论如何抛出的都是StackOverflowError。如果要抛出OOM异常,导致的原因是不断地在创建线程,直到将内存消耗殆尽。
JVM的内存由堆内存 + 方法区内存 + 剩余内存,也就是剩余内存=操作系统分配给JVM的内存 - 堆内存 - 方法区内存。-Xss设置的是每个线程的栈容量,也就是说可以创建的线程数量 = 剩余内存 / 栈内存。此时如果栈内存越大,可以创建的线程数量就少,就容易出现OOM;如果栈内存越小,可以创建的线程数量就多,就不容易出现OOM。
要避免这种情况最好就是减少堆内存+方法区内存,或者适当减少栈内存。对于栈内存的配置,一般采用默认值1M,或者采用64位操作系统以及64位的JVM。
1.2.3.本地方法栈Native Method Stack
本地方法栈服务的是Native方法,也会抛出StackOverflowError和OOM异常。
1.2.4.Java堆(Java Heap)
jvm规范中描述:几乎所有的对象实例和数组都要在堆上分配内存。是为所有线程所共享的一块区域。
java堆是垃圾收集器管理的主要区域,也被称为“GC堆”。
java堆还可以细分为新生代和老生代。
对于新生代又分为Eden空间、From Survivor空间、To Survivor空间。至于为什么这么分,这涉及JVM的垃圾回收机制。堆同样会抛出OOM异常,下面例子设置JVM参数” -Xms20M -Xmx20M“(前者表示初始堆大小20M,后者表示最大堆大小20M)。
下面编写代码,进行说明:
package com.jvm; import java.util.ArrayList; import java.util.List; /** * 测试heap内存溢出 * @author wangfei * -Xms20M -Xmx20M 堆初始大小20M 堆最大大小20M */ public class HeapTest { public static void main(String[] args) { List<HeapTest> list = new ArrayList<HeapTest>(); int count = 0; try { while(true) { count ++; list.add(new HeapTest());//不断创建对象实例 } }catch(Throwable e) { System.out.println("创建实例个数:" + count); e.printStackTrace(); } } }
一段时间后,抛出异常:
1.2.5.方法区(Method Area)
也是各个线程共享的内存区域,它用于存储已被jvm加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它有一个别名叫做Non-Heap(非堆),与java堆区分开来。
永久代:HotSpot 虚拟机把 GC 分代收集扩展至方法区,或者说用永久代来实现方法区,这样就可以像管理 Java 堆一样管理这部分代码,能够省去专门为方法区编写内存管理代码的工作。
使用永久代,来实现方法区,可能会出现内存泄露的问题。
这个区域内存回收的目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回收成绩比较难以令人满意,尤其是类型的卸载,条件相当苛刻。
1.2.6.运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。
jvm对Class文件的格式要求严格,但对运行时常量池则要求不多,不同提供商实现的VM可以自己实现这个内存区域。
运行时常量池具备动态性,运行期也可能将新的常量放入池中,如String类的intern()方法。
1.2.7.直接内存(Direct Memory)
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分。
但是,会受到本机总内存(包括RAM及SWAP区)大小,以及处理器寻址空间的限制。