首页IT科技jvm运行过程(JVM运行数据区深度解析)

jvm运行过程(JVM运行数据区深度解析)

时间2025-06-14 03:39:41分类IT科技浏览3763
导读:运行数据区 字节码只是一个二进制文件存放在那里。要想在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操作                    ,避免流在堆内外的拷贝      。我们下一步的调优不会涉及到它       ,了解即可                    。

本文由传智教育博学谷教研团队发布             。

如果本文对您有帮助             ,欢迎关注和点赞;如果您有任何建议也可留言评论或私信                   ,您的支持是我坚持创作的动力      。

转载请注明出处!

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
火车头采集视频教程大全(火车头采集防盗链图片——打破图片保护的利器) 网站建设时需要注意的问题(从设计到维护,全面解析网站建设要点)