当前位置: 首页 > 后端技术 > Java

JVM运行数据区深入分析

时间:2023-04-02 00:31:06 Java

运行数据区的字节码只是存放在那里的一个二进制文件。要想在jvm中运行,首先要有运行的内存环境。也就是我们所说的jvm运行时数据区。1)运行时数据区的位置运行时数据区是jvm最重要的部分,也是执行引擎频繁操作的地方。类的初始化,以及我们后面要讲的对象空间的分配和垃圾回收,都发生在这个区域。2)区域划分按照《Java虚拟机规范》中的规定,运行时数据区被细分为几个线程私有的部分:Java虚拟机栈(JavaVirtualMachineStack)、程序计数器寄存器(ProgramCounterRegister)、本地方法栈(NativeMethodStacks)是大家共享的:MethodArea,JavaHeap。接下来我们将分块详细讲解,每个块是干什么的,溢出了会怎样1.1程序计数器1.1.1概述程序计数器(ProgramCounterRegister)是每个线程一个。是一小块内存空间,代表当前线程执行的字节码指令的地址。字节码解释器工作时,通过改变这个计数器的值来选择下一条要执行的字节码指令,所以整个程序的分支、循环、跳转、异常处理、线程恢复等基本功能都需要依赖于此。柜台完成。由于多个线程是并行执行的,执行的指令是不同的,所以每个线程都需要有一个独立的程序计数器。线程间的计数器互不影响,独立存储。我们称这种内存区域为“线程私有”内存。如果是native方法,这里为空1.1.2没有溢出异常!在虚拟机规范中,这个区域没有内存溢出规范,是唯一不会溢出的区域。1.1.3的情况,因为不会溢出,我们没有办法为它创建一个,只能从class类中创建。寻找痕迹。回顾上面javap的反汇编,代码对应的数字可以理解为计数器记录的执行次数。1.2虚拟机栈1.2.1概述也是线程私有的!生命周期与线程相同。它描述了Java方法执行的当前线程的内存模型。每个方法执行时,Java虚拟机都会同步创建一个栈帧,用于存放局部变量表、操作数栈、动态连接、方法出口等。每个方法被调用到执行完成的过程对应于一个栈帧在虚拟机栈中从入栈到出栈的过程。1.2.2溢出异常1)栈深度超过设置如果创建的栈深度大于虚拟机允许的深度,则抛出Exceptioninthread"main"java.lang.StackOverflowError2)内存申请不足如果栈允许内存扩展,但是当内存申请不够时,会抛出OutOfMemoryError。笔记!这个和具体的虚拟机有关。hotspot虚拟机不支持栈空间扩展,所以在单线程环境下,一个线程在创建时,会被分配一个固定大小的栈,它不会出现在这个固定的栈空间上。在扩展应用程序内存的情况下,不会出现应用程序不足的情况。上面的StackOverflowError只会是深度超出固定空间的问题导致的。但这与Xss栈空间的大小无关。就是因为线程太多,栈太多,导致系统分配给jvm进程的物理内存被吃光了。这时虚拟机自带相关提示:Exceptioninthread"main"java.lang.OutOfMemoryError:unabletocreatenativethreadssp:Eachthreadareallocated1Mspacebydefault(64-bitlinux,hotspotenvironment)问题:是否可以更改Xss?值可以得到堆栈空间溢出?答:根据上面的分析,热点是不允许的,仍然会抛出StackOverflowError,只是深度更小了。1.2.3案例一:进出栈顺序1)代码包com.iheima.jvm.demo;/***程序模拟入栈出栈的过程*先进后出*/publicclassStackInAndOut{/***定义方法1*/publicstaticvoidA(){System.out.println("进入方法A");}/***定义方法二;调用方法一*/publicstaticvoidB(){A();System.out.println("进入方法B");}publicstaticvoidmain(String[]args){B();System.out.println("进入Main方法");}}2)运行结果:进入方法A进入方法B进入Main方法3)栈结构:main方法---->B方法---->A方法1.2.4案例2:栈深度溢出1)The代码实现简单,方法可以自己嵌套:packagecom.iheima.jvm.demo;/***通过程序模拟线程请求的栈深度大于virtual允许的栈深度机器;*ThrowStackOverflowError*/publicclassStackOverFlow{/***定义方法,循环嵌套self*/publicstaticvoidB(){B();System.out.println("进入方法B");}publicstaticvoidmain(String[]args){B();System.out.println("进入Main方法");}}2)运行结果:Exceptioninthread"main"java.lang.StackOverflowErr或者在com.iheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)在com.iheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)在com.iheima.jvm.demo.StackOverFlow。B(StackOverFlow.java:12)在com.iheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)在com.iheima.jvm.demo.StackOverFlow.B(StackOverFlow.java:12)3)堆栈结构:1.2.5案例3:栈内存溢出如果一直创建线程,可以把栈填满但是!这个很危险,你可以在32位系统的winxp上试试,卡机概不负责!packagecom.iheima.jvm.demo;/**堆栈溢出,小心!非常危险,谨慎执行*执行时可能会卡死系统直到内存耗尽**/publicclassStackOutOfMem{publicstaticvoidmain(String[]args){while(true){newThread(()->{while(true);}).start();}}}1.3本地方法栈1.3.1概述本地方法栈的作用和特点与虚拟机栈类似,都具有线程隔离的特点。不同的是,本地方法栈服务的对象是JVM执行的native方法,而虚拟机栈服务的是JVM执行的java方法。在虚拟机规范中,对这块使用的语言和数据结构没有强制规定。虚拟机可以自由实现。甚至,hotspot将它和虚拟机栈合并为一个1.3.2溢出异常和虚拟机栈一样,有两种:如果创建的栈深度大于虚拟机允许的深度,和内存申请不够的时候抛出一个StackOverFlowError,一个OutOfMemoryError1.4heap1.4.1概述和上面三个不同的是,heap是所有线程共享的!所谓的线程安全和不安全也是从这里来的。在虚拟机启动时创建。这块内存区的唯一用途就是存放对象实例,Java世界中“几乎”所有的对象实例都在这里分配内存。需要注意的是《Java虚拟机规范》并没有详细划分堆,所以堆的解释要根据具体的虚拟机。我们以最常用的HotSpot虚拟机为例。Java堆是垃圾收集器管理的内存区域,所以也称为“GC堆”,是JVM调优的重点区域。1.4.2jdk1.7jvm的内存模型在1.7和1.8有很大的不同。虽然1.7目前用的比较少,但是我们也需要了解1.7的内存模型,所以接下来先学习1.7再学习1.8的内存模型。Young区(代)Young区分为三部分,Eden区和两个大小相同的Survivor区。其中,在Survivor区,某个时间只使用其中一个,另一个留作垃圾回收。当对象在Eden区间被使用并变满时,GC会将存活的对象移至空闲的Survivor区间。根据JVM策略,经过几次垃圾回收后,Survivor对象会被移到下面的TheTenured区间。TenuredOldareaTenuredarea主要存放生命周期比较长的对象,一般是一些比较老的对象。当某些对象在Young中被复制和转移一定次数后,这些对象就会被转移到Tenured区域。一般情况下,如果系统使用了应用级Cache,缓存中的对象往往会被转移到这个范围内。Perm永久区域热点1.6只有这个产品,现在已经成为历史。Perm代主要存储类、方法和归档对象。这部分的空间一般不会溢出,除非一次加载很多类。但是,当涉及到热部署应用服务器时,有时会遇到java.lang.OutOfMemoryError:PermGenspace的错误。这个错误很大的原因可能是你每次都重新部署,但是重新部署之后,并没有卸载该类的类,这样一来perm中保存了大量的类对象。这种情况一般重启应用服务器即可解决。另一种可能是创建了大量的jsp文件,导致类信息超过perm上限而溢出。这次重新启动也没有解决它。你只能增加空间。虚拟区域:jvm参数可以设置一个范围,最大内存和初始内存的差就是虚拟区域。1.4.3jdk1.8从上图可以看出,jdk1.8的内存模型由两部分组成,年轻代+老年代。永久代被淘汰,取而代之的是Metaspace(元数据空间)年轻代:Eden+2*Survivor(不变)老年代:OldGen(不变)Metaspace:原来的perm区(重要!)需要特别说明Yes:占用的内存空间byMetaspace不在虚拟机内部,而是在本地内存空间,这是与1.7的永久代最大的区别。1.4.4内存不足时溢出异常,抛出java.lang.OutOfMemoryError:Javaheapspace1.4.5。案例:堆溢出1)代码分配了大量对象,超出了jvm指定的堆范围。包com.itheima.jvm.demo;importjava.util.ArrayList;importjava.util.List;/***堆溢出*-Xms20m-Xmx20m*/publicclassHeapOOM{Byte[]bytes=newByte[1024*1024];publicstaticvoidmain(String[]args){Listlist=newArrayList();诠释我=0;while(true){System.out.println(++i);list.add(新的HeapOOM());}}}2)start注意启动时,指定堆的大小:2)Output12345Exceptioninthread"main"java.lang.OutOfMemoryError:Javaheapspaceatcom.iheima.jvm.demo.HeapOOM.(HeapOOM.java:7)atcom.iheima.jvm.demo.HeapOOM.main(HeapOOM.java:13)1.5方法区1.5.1概述同样,线程也是共享的。主要用来存放类信息,类中定义的常量,静态变量,以及编译器编译后的代码缓存。注意!方法区在虚拟机规范中是一个逻辑概念,对于应该放在什么地方并没有严格的规定。所以hotspot1.7把它放到了堆的永久代中,1.8+开辟了一个单独的一块叫做metaspace来存放一些内容(不是全部!定义的类对象都在堆中)。具体方法区主要存放什么?大致可以分为两类:类信息:主要是指类相关的版本、字段、方法、接口描述、引用等。运行时常量池:编译时产生的常量和符号引用,运行时添加的动态变量(常量池中的类变量,比如objects或者strings,比较特殊,1.6和1.8的位置不一样,下面会讲)Tips:这个经常和上面heap中的permanentgeneration混淆。其实,这是两种不同的东西。永久代是hotspot在1.7及之前的设计,1.8+,其他虚拟机不存在。可以说永久代是hotspot1.7偷懒的结果。它在堆中划分一个块来实现方法区的功能,称为永久代。因为这样可以利用堆的垃圾回收来管理方法区的内存,而不用单独为方法区写一个内存管理程序。懒惰的!同时代的其他虚拟机,如J9、Jrockit等,没有这个概念。后来hotspot意识到用永久代来做这件事并不是一个好主意。1.7已经从永久代中取出了部分数据,直到1.8+完全去掉了永久代,大部分方法区被移到了元空间(再次声明,不是全部!)结论:方法区必须存在,这是virtualmachine是有规定的,但是是一个逻辑概念。凡是虚拟机自己决定,永久代不一定存在的地方(仅限hotspot1.7),已经成为历史1.5.2溢出异常1.6:OutOfMemoryError:PermGenspace1.8:OutOfMemoryError:Metaspace1.5.3案例:1.6方法区溢出1)原理在1.6中,字符串常量是运行时常量池的一部分,即属于方法区,放在永久代中。所以在1.6环境下,要让方法区溢出,只需要在字符串常量池中创建一个字符串即可。这里有一个方法:/*如果字符串常量池中有这个字符串,则直接返回引用,不再额外添加如果没有,则添加并返回新创建的引用*/String.intern()2)代码/***方法区溢出,注意限制永久代的大小*编译时注意pom中的版本,要设置1.6,否则启动会出问题*jdk1.6:-XX:PermSize=6M-XX:MaxPermSize=6M*/publicclassConstantOOM{publicstaticvoidmain(String[]args){ConstantOOMoom=newConstantOOM();SetstringSet=newHashSet();诠释我=0;while(true){System.out.println(++i);stringSet.add(String.valueOf(i).intern());}}}3)创建启动环境4)异常信息:...191181911919120Exceptioninthread"main"java.lang.OutOfMemoryError:PermGenspaceatjava.lang.String.intern(NativeMethod)atcom.iheima.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=10M2)配置运行环境3)控制台信息不会抛出异常,只要你有足够的jvm堆内存,理论上4)为什么??替我们永久添加了限制,但是结果是没有意义的,因为1.8中没有这个东西。元空间也加了限制,也是没有意义的,也就是说字符串常量池不在元空间中!那么它在哪里呢?jdk1.8之后,字符串常量池被移到了堆空间。和其他对象一样,它接受堆的控制,其他运行时类信息、基本数据类型等都在元空间中。我们可以通过在上面的运行时参数中加入堆上限来验证:-Xms10m-Xmx10m运行环境如下:运行一段时间后,会得到如下异常:...840148401584016840178401884019Exceptioninthread"main"java.lang.OutOfMemoryError:在com.iheima.jvm.demo.ConstantOOM的java.lang.String.valueOf(String.java:3099)的java.lang.Integer.toString(Integer.java:403)超出了GC开销限制.main(ConstantOOM.java:18)解释:在1.8中,字符串inter()是放在堆中的,受限于最大堆空间。5)元空间如何溢出?既然这里没有字符串常量池,那就改成别的吧。类的基本信息总在元空间里吧?试一下cglib是apache下的一个字节码库,可以在运行时生成大量的对象,我们试一下while循环同时限制元空间:附:https://gitee.com/mirrors/cglib(想深入了解左边这个工具的一面,这里不做过多讨论).MethodProxy;importjava.lang.reflect.Method;/***jdk8方法区溢出*-XX:MetaspaceSize=10M-XX:MaxMetaspaceSize=10M*/publicclassConstantOOM8{publicstaticvoidmain(finalString[]args){while(true){增强器增强器=newEnhancer();enhancer.setSuperclass(OOM.class);enhancer.setUseCache(false);enhancer.setCallback(newMethodInterceptor(){@OverridepublicObjectintercept(Objecto,Methodmethod,Object[]objects,MethodProxymethodProxy)throwsThrowable{returnmethodProxy.invokeSuper(objects,args);}});增强器.create();}}staticclassOOM{}}6)运行设置7)运行结果:763)结论:jdk8引入元空间存放方法区后,内存溢出的风险比历史版本小很多,但是当类失控时,方法区还是会被炸飞。1.6一个案例让大家理解和记忆,下面我们用一个案例将以上几个方面串联起来。假设有一个Bootstrap类,执行main方法。在jvm中,从class文件到运行要经过以下几个步骤:首先,JVM会先加载Bootstrap。机器栈,本地方法栈然后,JVM会在Heap堆上为Bootstrap.class创建一个Bootstrap.class的类实例。JVM开始执行main方法。这时候在虚拟机栈中为main方法创建了一个栈帧。main方法在执行调用greeting方法的过程中,JVM会为greeting方法创建另一个栈帧,并将其压入虚拟机栈顶。在main之上,一次只有一个堆栈帧处于活动状态。目前是greeting作为greeting方法执行完成后greeting方法出栈,当前活动帧指向main,方法继续运行1.7总结1)独占/共享视角:exclusive:program计数器,虚拟机栈,本地方法栈共享:堆,方法区2)报错的角度:程序计数器:不会溢出,比较特殊,其他会有两个栈:可能会发生两种溢出,一种是深度超出,报StackOverflowError,空间不足:OutOfMemoryErrorheap:onlyinthespace不足时,会报OutOfMemoryError,heapSpace方法区会提示:空间不足时,会报OutOfMemoryError,提示不同,1.6是permspace,1.8是metaspace,跟在哪有关系3)归属:计数器,虚拟机栈,本地方法栈:线程创建必须申请支持,realphysicalspaceheap:真实物理空间,但是划分内部结构变了,1.6有了永久代,1.8被kill了方法区:最没有归属感的,原因是它是一个逻辑概念。1.6是放在堆的永久代,1.8是拆分出来的,一部分在元空间,一部分(方法区运行时常量池中的类对象,包括字符串常量,设计成放在theheap)directmemory:这个块实际上不是运行时数据区的一部分,而是直接操作物理内存。在nio操作中,DirectByteBuffer类可以对native进行操作,避免了堆内外流的复制。我们下一步调优就不涉及了,了解一下即可。本文由传智教育博学谷教研组发布。如果本文对您有帮助,请关注并点赞;有什么建议也可以留言或私信。您的支持是我坚持创作的动力。转载请注明出处!