JVM运行数据区深度解析
运行数据区
字节码只是一个二进制文件存放在那里。要想在jvm里跑起来,先得有个运行的内存环境。
也就是我们所说的jvm运行时数据区。
1)运行时数据区的位置
运行时数据区是jvm中最为重要的部分,执行引擎频繁操作的就是它。类的初始化,以及后面我们讲的对象空间的分配、垃圾的回收都是在这块区域发生的。
2)区域划分
根据《Java虚拟机规范》中的规定,在运行时数据区将内存细分为几个部分
线程私有的:Java虚拟机栈(Java Virtual Machine Stack)、程序计数器(Program Counter Register)、本地方法栈(Native Method Stacks)
大家共享的:方法区(Method Area)、Java堆区(Java Heap)
接下来我们分块详细来解读,每一块是做什么的,如果溢出了会发生什么事情
1.1 程序计数器
1.1.1 概述
程序计数器(Program Counter Register)
每个线程一个。是一块较小的内存空间,它表示当前线程执行的字节码指令的地址。
字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,所以整个程序无论是分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
由于线程是多条并行执行的,互相之间执行到哪条指令是不一样的,所以每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果是native方法,这里为空
1.1.2 溢出异常
没有!
在虚拟机规范中,没有对这块区域设定内存溢出规范,也是唯一一个不会溢出的区域
1.1.3 案例
因为它不会溢出,所以我们没有办法给它造一个,但是从class类上可以找到痕迹。
回顾上面javap的反汇编,其中code所对应的编号就可以理解为计数器中所记录的执行编号。
1.2 虚拟机栈
1.2.1 概述
- 也是线程私有的!生命周期与线程相同。
- 它描述的是Java方法执行的当前线程的内存模型,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
1.2.2 溢出异常
1)栈深度超出设定
如果是创建的栈的深度大于虚拟机允许的深度,抛出
Exception in thread "main" java.lang.StackOverflowError
2)内存申请不足
如果栈允许内存扩展,但是内存申请不够的时候,抛出 OutOfMemoryError
注意!这一点和具体的虚拟机有关,hotspot虚拟机并不支持栈空间扩展,所以单线程环境下,一个线程创建时,分配给它固定大小的一个栈,在这个固定栈空间上不会出现再去扩容申请内存的情况,也就不会遇到申请不到一说,只会因为深度问题超出固定空间造成上面的StackOverflowError
如果换成多线程,毫无节制的创建线程,还是有可能造成OutOfMemoryError。但是这个和Xss栈空间大小无关。是因为线程个数太多,栈的个数太多,导致系统分配给jvm进程的物理内存被吃光。
这时候虚拟机会附带相关的提示:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread
ps: 每个线程默认分配1M空间(64位linux,hotspot环境)
疑问:是不是改小Xss的值就可以得到栈空间溢出呢?
答:根据上面的分析,hotspot下不可以,还是会抛出StackOverflowError,无非深度更小了。
1.2.3 案例一:进出栈顺序
1)代码
package com.itheima.jvm.demo; /** * 程序模拟进栈、出栈过程 * 先进后出 */ public class StackInAndOut { /** * 定义方法一 */ public static void A() { System.out.println("进入方法A"); } /** * 定义方法二;调用方法一 */ public static void B() { A(); System.out.println("进入方法B"); } public static void main(String[] args) { B(); System.out.println("进入Main方法"); } }
2)运行结果:
进入方法A 进入方法B 进入Main方法
3)栈结构:
main方法---->B方法---->A方法
1.2.4 案例二:栈深度溢出
1)代码
这个容易实现,方法嵌套自己就可以:
package com.itheima.jvm.demo; /** * 通过一个程序模拟线程请求的栈深度大于虚拟机所允许的栈深度; * 抛出StackOverflowError */ public class StackOverFlow { /** * 定义方法,循环嵌套自己 */ public static void B() { B(); System.out.println("进入方法B"); } public static void main(String[] args) { B(); System.out.println("进入Main方法"); } }
2)运行结果:
Exception in thread "main" java.lang.StackOverflowError at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12) at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12) at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12) at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12) at com.itheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)
3)栈结构:
1.2.5 案例三:栈内存溢出
一直不停的创建线程就可以堆满栈
但是!这个很危险,到32系统的winxp上勇敢的小伙伴可以试一试,机器卡死不负责!
package com.itheima.jvm.demo; /* * 栈内存溢出,注意!很危险,谨慎执行 * 执行时可能会卡死系统。直到内存耗尽 * */ public class StackOutOfMem { public static void main(String[] args) { while (true) { new Thread(() -> { while(true); }).start(); } } }
1.3 本地方法栈
1.3.1 概述
本地方法栈的功能和特点类似于虚拟机栈,均具有线程隔离的特点
不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法
虚拟机规范里对这块所用的语言、数据结构、没有强制规定,虚拟机可以自由实现它
甚至,hotspot把它和虚拟机栈合并成了1个
1.3.2 溢出异常
和虚拟机栈一样,也是两个:
如果是创建的栈的深度大于虚拟机允许的深度,抛出 StackOverFlowError
内存申请不够的时候,抛出 OutOfMemoryError
1.4 堆
1.4.1 概述
与上面的3个不同,堆是所有线程共享的!所谓的线程安全不安全也是出自这里。
在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存。
需要注意的是,《Java虚拟机规范》并没有对堆进行细致的划分,所以对于堆的讲解要基于具体的虚拟机,我们以使用最多的HotSpot虚拟机为例。
Java堆是垃圾收集器管理的内存区域,因此它也被称作“GC堆”,这就是我们做JVM调优的重点区域部分。
1.4.2 jdk1.7
jvm的内存模型在1.7和1.8有较大的区别,虽然1.7目前使用的较少了,但是我们也是需要对1.7的内存模型有所了解,所以接下里,我们将先学习1.7再学习1.8的内存模型。
Young 年轻区(代)
Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区
其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用
在Eden区间变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到下面的Tenured区间。
Tenured 年老区
Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区,一般如果系统中用了application级别的缓存,缓存中的对象往往会被转移到这一区间。
Perm 永久区
hotspot 1.6 才有这货,现在已经成为历史
Perm代主要保存class,method,filed对象,这部份的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这样就造成了大量的class对象保存在了perm中,这种情况下,一般重新启动应用服务器可以解决问题。另外一种可能是创建了大批量的jsp文件,造成类信息超出perm的上限而溢出。这种重启也解决不了。只能调大空间。
Virtual区:
jvm参数可以设置一个范围,最大内存和初始内存的差值,就是Virtual区。
1.4.3 jdk1.8
由上图可以看出,jdk1.8的内存模型是由2部分组成,年轻代 + 年老代。永久代被干掉,换成了Metaspace(元数据空间)
年轻代:Eden + 2*Survivor (不变)
年老代:OldGen (不变)
元空间:原来的perm区 (重点!)
需要特别说明的是:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是与1.7的永久代最大的区别所在。
1.4.4 溢出异常
内存不足时,抛出
java.lang.OutOfMemoryError: Java heap space
1.4.5 案例:堆溢出
1)代码
分配大量对象,超出jvm规定的堆范围即可
package com.itheima.jvm.demo; import java.util.ArrayList; import java.util.List; /** * 堆溢出 * -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()); } } }
2)启动
注意启动时,指定一下堆的大小:
2)输出
1 2 3 4 5 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at com.itheima.jvm.demo.HeapOOM.<init>(HeapOOM.java:7) at com.itheima.jvm.demo.HeapOOM.main(HeapOOM.java:13)
1.5 方法区
1.5.1 概述
同样,线程共享的。
它主要用来存储类的信息、类里定义的常量、静态变量、编译器编译后的代码缓存。
注意!方法区在虚拟机规范里这是一个逻辑概念,它具体放在那个区域里没有严格的规定。
所以,hotspot 1.7 将它放在了堆的永久代里,1.8+单独开辟了一块叫metaspace来存放一部分内容(不是全部!定义的类对象在堆里)
具体方法区主要存什么东西呢?粗略的分,可以划分为两类:
类信息:主要指类相关的版本、字段、方法、接口描述、引用等
运行时常量池:编译阶段生成的常量与符号引用、运行时加入的动态变量
(常量池里的类变量,如对象或字符串,比较特殊,1.6和1.8位置不同,下面会讲到)
小提示:
这里经常会跟上面堆里的永久代混为一谈,实际上这是两码事
永久代是hotspot在1.7及之前才有的设计,1.8+,以及其他虚拟机并不存在这个东西。
可以说,永久代是1.7的hotspot偷懒的结果,他在堆里划分了一块来实现方法区的功能,叫永久代。因为这样可以借助堆的垃圾回收来管理方法区的内存,而不用单独为方法区再去编写内存管理程序。懒惰!
同时代的其他虚拟机,如J9,Jrockit等,没有这个概念。后来hotspot认识到,永久代来做这件事不是一个好主意。1.7已经从永久代拿走了一部分数据,直到1.8+彻底去掉了永久代,方法区大部分被移到了metaspace(再强调一下,不是全部!)
结论:
方法区是一定存在的,这是虚拟机规定的,但是是个逻辑概念,在哪里虚拟机自己去决定
而永久代不一定存在(hotspot 1.7 才有),已成为历史
1.5.2 溢出异常
1.6:OutOfMemoryError: PermGen space
1.8:OutOfMemoryError: Metaspace
1.5.3 案例:1.6方法区溢出
1)原理
在1.6里,字符串常量是运行时常量池的一部分,也就是归属于方法区,放在了永久代里。
所以1.6环境下,让方法区溢出,只需要可劲造往字符串常量池中造字符串即可,这里用到一个方法:
/* 如果字符串常量池里有这个字符串,直接返回引用,不再额外添加 如果没有,加进去,返回新创建的引用 */ String.intern()
2)代码
/** * 方法区溢出,注意限制一下永久代的大小 * 编译的时候注意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()); } } }
3)创建启动环境
4)异常信息:
... 19118 19119 19120 Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method) at com.itheima.jvm.demo.ConstantOOM.main(ConstantOOM.java:19)
1.5.4 案例:1.8方法区溢出
1)到了1.8,情况发生了变化
可以测试一下,1.8下无论指定下面的哪个参数,常量池运行都不会溢出,会一直打印下去
-XX:PermSize=6M -XX:MaxPermSize=6M -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
2)配置运行环境
3)控制台信息
不会抛出异常,只要你jvm堆内存够,理论上可以一直打下去
4)为什么呢?
永久代我们加了限制,结果没意义,因为1.8里已经没有这货了
元空间也加了限制,同样没意义,那说明字符串常量池它不在元空间里!
那么,它在哪里呢?
jdk1.8以后,字符串常量池被移到了堆空间,和其他对象一样,接受堆的控制。
其他的运行时的类信息、基本数据类型等在元空间。
我们可以验证一下,对上面的运行时参数再加一个堆上限限制:
-Xms10m -Xmx10m
运行环境如下:
运行没多久,你会得到以下异常:
…… 84014 84015 84016 84017 84018 84019 Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded at java.lang.Integer.toString(Integer.java:403) at java.lang.String.valueOf(String.java:3099) at com.itheima.jvm.demo.ConstantOOM.main(ConstantOOM.java:18)
说明:1.8里,字符串inter()被放在了堆里,受最大堆空间的限制。
5)那如何才能让元空间溢出呢?
既然字符串常量池不在这里,那就换其他的。类的基本信息总在元空间吧?我们来试一下
cglib是一个apache下的字节码库,它可以在运行时生成大量的对象,我们while循环同时限制metaspace试试:
附:https://gitee.com/mirrors/cglib (想深入了解这个工具的猛击左边,这里不做过多讨论)
package com.itheima.jvm.demo; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * 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{ } }
6)运行设置
7)运行结果
Caused by: java.lang.OutOfMemoryError: Metaspace at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
结论:
jdk8引入元空间来存储方法区后,内存溢出的风险比历史版本小多了,但是在类超出控制的时候,依然会打爆方法区
1.6 一个案例
为便于大家理解和记忆,下面我们用一个案例,把上面各个区串通起来。
假设有个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.7 归纳总结
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操作,避免流在堆内外的拷贝。我们下一步的调优不会涉及到它,了解即可。
本文由
传智教育博学谷
教研团队发布。如果本文对您有帮助,欢迎
关注
和点赞
;如果您有任何建议也可留言评论
或私信
,您的支持是我坚持创作的动力。转载请注明出处!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
【数据库迁移系列】从Oracle迁移到openGauss实战分享
之前的迁移系列中我们介绍了Mysql到openGauss的迁移方法,本篇介绍使用Ora2og工具从Oracle到openGauss数据库的迁移。 文章目录 简介 迁移前准备 环境 软件安装 ora2og工具安装 创建迁移项目 配置ora2pg.conf 测试迁移 导出 导入 Ora2Pg不足 FAQ 简介 ora2pg 可以将 Oracle 或者 MySQL 数据库迁移到 PostgreSQL,应用场景小到 Oracle 数据库的反向工程,大到大型企业数据库迁移,或者简单地将一些 Oracle 数据复制到 PostgreSQL 数据库。Ora2Pg 由一个 Perl 脚本(ora2pg)以及一个 Perl 模块(Ora2Pg.pm)组成,唯一需要做的事情就是修改它的配置文件 ora2pg.conf,设置连接 Oracle 数据库的 DSN 和一个可选的模式名称。完成之后,只需要设置导出的类型:TABLE(包括约束)、VIEW、TABLESPACE、SEQUENCE、INDEXES、TRIGGER、FUNCTION、PROCEDURE、PACKAGE等等。 ora2og是一个将Orac...
- 下一篇
「案例分享」研发效能提升之第一性原理
作者:樊思国 一、引言 被埃隆·马斯克多次提及的第一性原理First principle thinking,是计算物理学领域的一个专业术语,在商业领域依然具有鲜活的生命力。读过《硅谷钢铁侠》这本书的知道,正是因为应用了第一性原理对问题进行分析,才使得马斯克在跨航天、汽车、能源和软件领域创新硕果累累,比如SpaceX的成功,就是从根本上找到运载火箭的成本重头在推进系统上并解决该问题,从而创造了可回收利用的火箭推进器,从根本上解决成本问题,对行业来说是颠覆性的创新模式。 第一性原理的本义是指在进行计算的时候除了告诉程序你所使用的原子和他们的位置外,没有其它的实验的,经验的或者半经验的参量,且具有很好的移植性。通俗理解,第一性原理就是基于客观事实进行的推导,其中不加入自己猜测和类比等经验性的东西。 在创新和研发效能提升的行业大背景下,作者认为在我们的日常研发工作中,也可以利用第一性原理思维帮助我们提升效能甚至是提升创新能力。比如,线上问题排查和分析解决,就可以很好的利用第一性原理指导我们实践。 下文将通过一个真实的线上问题排查经历,提炼出一种利用第一性原理进行问题排查和解决的通用流程框架,...
相关文章
文章评论
共有0条评论来说两句吧...