运行数据区
字节码只是一个二进制文件存放在那里。要想在jvm里跑起来,先得有个运行的内存环境。
也就是我们所说的jvm运行时数据区。
运行时数据区是jvm中最为重要的部分,执行引擎频繁操作的就是它。类的初始化,以及后面我们讲的对象空间的分配、垃圾的回收都是在这块区域发生的。
关于区域划分
根据《Java虚拟机规范》中的规定,在运行时数据区将内存细分为几个部分
线程私有的:Java虚拟机栈(Java Virtual Machine Stack)、程序计数器(Program Counter Register)、本地方法栈(Native Method Stacks)
共享的:方法区(Method Area)、Java堆区(Java Heap)
程序计数器
- 每个线程一个。是一块较小的内存空间,它表示当前线程执行的字节码指令的地址。
- 字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,所以整个程序无论是分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
- 由于线程是多条并行执行的,互相之间执行到哪条指令是不一样的,所以每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
- 如果是native方法,这里为空
!!!在虚拟机规范中,没有对这块区域设定内存溢出规范,也是唯一一个不会溢出的区域
虚拟机栈
概述
- 它描述的是Java方法执行的当前线程的内存模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
溢出异常
1.栈深度超出设定
如果是创建的栈的深度大于虚拟机允许的深度,抛出Exception in thread "main" java.lang.StackOverflowError
2.内存申请不足
如果栈允许内存扩展,但是内存申请不够的时候,抛出 OutOfMemoryError
注意!这一点和具体的虚拟机有关,hotspot虚拟机并不支持栈空间扩展,所以单线程环境下,一个线程创建时,分配给它固定大小的一个栈,在这个固定栈空间上不会出现再去扩容申请内存的情况,也就不会遇到申请不到一说,只会因为深度问题超出固定空间造成上面的StackOverflowError
到这里,那有小伙伴开始问了是不是改小Xss的值就可以得到栈空间溢出呢?
根据上面的分析,hotspot下不可以,还是会抛出StackOverflowError,无非深度更小了。
本地方法栈
- 本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点
- 不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法
- 虚拟机规范里对这块所用的语言、数据结构、没有强制规定,虚拟机可以自由实现它
- 甚至,hotspot把它和虚拟机栈合并成了1个
溢出异常
如果是创建的栈的深度大于虚拟机允许的深度,抛出 StackOverFlowError
内存申请不够的时候,抛出 OutOfMemoryError
堆
与上面的3个不同,堆是所有线程共享的!所谓的线程安全不安全也是出自这里。
在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。
Java堆是垃圾收集器管理的内存区域,因此它也被称作“GC堆”,这就是我们做JVM调优的重点区域部分。
!!!需要注意的是,《Java虚拟机规范》并没有对堆进行细致的划分,所以对于堆的讲解要基于具体的虚拟机,我们以使用最多的HotSpot虚拟机为例。
提到这个我们就要说一下jdk7和jdk8的区别了
jdk1.7
-
Young 年轻区(代)
Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区
其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用
在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到下面的Tenured区间。
-
Tenured 年老区
Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。
-
Perm 永久区
现在已经成为历史,Perm代主要保存类信息,class,method,filed等对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。另外一种可能是创建了大批量的jsp文件,造成类信息超出perm的上限而溢出。这种重启也解决不了。只能调大空间。
jdk1.8
jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。永久代被干掉,换成了Metaspace(元数据空间)
需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是与1.7的永久代最大的区别所在。
溢出异常
内存不足时,抛出java.lang.OutOfMemoryError: Java heap space
下面举个例子:
/** * 堆溢出 * -Xms20m -Xmx20m */ public class HeapOOM { Byte[] bytes = new Byte[1024*1024]; public static void main(String[] args) { List list = new ArrayList(); int i = 0; while (true) { System.out.println(++i); list.add(new HeapOOM()); } } }
方法区
同样,线程共享的。
它主要用来存储类的信息、类里定义的常量、静态变量、编译器编译后的代码缓存。
具体方法区主要存什么东西呢?粗略的分,可以划分为两类:
-
类信息:主要指类相关的版本、字段、方法、接口描述、引用等
-
运行时常量池:编译阶段生成的常量与符号引用、运行时加入的动态变量
溢出异常
1.6:OutOfMemoryError: PermGen space
1.8:OutOfMemoryError: Metaspace
举个例子
/** * 方法区溢出,注意限制一下永久代的大小 * 编译的时候注意pom里的版本,要设置1.6,否则启动会有问题 * jdk1.6 : -XX:PermSize=6M -XX:MaxPermSize=6M */ public class ConstantOOM { public static void main(String[] args) { ConstantOOM oom = new ConstantOOM(); Set<String> stringSet = new HashSet(); int i = 0; while (true) { System.out.println(++i); stringSet.add(String.valueOf(i).intern()); } } }
/** * jdk8方法区溢出 * -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M */ public class ConstantOOM8 { public static void main(final String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOM.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { return methodProxy.invokeSuper(objects,args); } }); enhancer.create(); } } static class OOM{ } }
总结:jdk8引入元空间来存储方法区后,内存溢出的风险比历史版本小多了,但是在类超出控制的时候,依然会打爆方法区
为了方便理解,我们举个例子
假设有个Bootstrap的类,执行main方法。在jvm里,它从class文件到跑起来,大致经过如下步骤:
- 首先JVM会先将这个Bootstrap.class 信息加载到内存中的方法区
- 接着,主线程开辟一块内存空间,准备好程序计数器pc,虚拟机栈、本地方法栈
- 然后,JVM会在Heap堆上为Bootstrap.class 创建一个Bootstrap.class 的类实例
- JVM开始执行main方法,这时在虚拟机栈里为main方法创建一个栈帧
- main方法在执行的过程之中,调用了greeting方法,则JVM会为greeting方法再创建一个栈帧,推到虚拟机栈顶,在main的上面,每次只有一个栈帧处于活动状态,当前为greeting
- 当greeting方法运行完成后,则greeting方法出栈,当前活动帧指向main,方法继续往下运行
接下来我们总结一下
1)独享/共享的角度:
- 独享:程序计数器、虚拟机栈、本地方法栈
- 共享:堆、方法区
2)error的角度:
- 程序计数器:不会溢出,比较特殊,其他都会
- 两个栈:可能会发生两种溢出,一是深度超了,报StackOverflowError,空间不足:OutOfMemoryError
- 堆:只会在空间不足时,报OutOfMemoryError,会提示heapSpace
- 方法区:空间不足时,报OutOfMemoryError,提示不同,1.6是permspace,1.8是元空间,和它在什么地方有关
3)归属:
- 计数器、虚拟机栈、本地方法栈:线程创建必须申请配套,真正的物理空间
- 堆:真正的物理空间,但是内部结构的划分有变动,1.6有永久代,1.8被干掉
- 方法区:最没归属感的一块,原因就是它是一个逻辑概念。1.6被放在了堆的永久代,1.8被拆分,一部分在元空间,一部分(方法区的运行时常量池里面的类对象,包括字符串常量,被设计放在了堆里)
- 直接内存:这块实际上不属于运行时数据区的一部分,而是直接操作物理内存。在nio操作里DirectByteBuffer类可以对native操作,避免流在堆内外的拷贝。我们下一步的调优不会涉及到它,了解即可。
类加载