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

Java虚拟机内存结构及编码实践

时间:2023-04-01 16:14:58 Java

理解JVM内存结构的目的在Java的开发过程中,由于JVM自动内存管理机制,不再需要像in这样手动释放对象的内存空间C和C++开发。容易出现内存泄漏和内存溢出问题。但是,正是因为内存管理权交给了JVM。一旦出现内存泄漏和内存溢出问题,就很难理解JVM是如何使用内存的,JVM的内存结构是什么样子的。一旦找到问题的根源,修复它就变得更加困难。JVM内存结构介绍在JVM管理的内存中,大致分为以下几个运行时数据区:程序计数器:当前线程执行的字节码的行号指示符。虚拟机栈:Java方法执行的内存模型,用来存放局部变量表、操作数栈、动态链接、方法出口等信息。本地方法栈:本地方法执行的内存模型和虚拟机栈非常相似,不同的是本地方法栈是为JVM使用的Native方法服务的。堆:用于存放对象实例,是垃圾收集器管理的主要区域。方法区:用于存放JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。其中,程序计数器、虚拟机栈、本地方法栈在黄色区域是线程私有的,红色区域的堆和方法区是线程共享的。下面我们详细分析每个区域。程序计数器程序计数器(ProgramCounterRegister)是一块很小的内存空间,记录了当前线程执行的字节码的行号。在JVM的概念模型中,字节码解释器通过改变它的值来选择下一条要执行的字节码指令来工作。分支、循环、跳转、异常处理、线程恢复等基本功能都依赖Itcomesdone。通过轮流切换线程,分配处理器执行时间,实现了JVM的多线程运行。在任何给定时刻,一个处理器(在多核处理器的情况下是一个内核)仅在一个线程中执行指令。因此,为了在线程切换后回到正确的执行位置,每个线程都需要有一个独立的程序计数器。线程间的计数器互不影响,独立存储。这种类型的内存区域称为“线程私有”。的记忆。如果线程正在执行Java方法,则记录正在执行的虚拟机字节码指令的地址;如果它正在执行Natvie方法,则其值为空(未定义)。此内存区域是唯一未在Java虚拟机规范中指定任何OutOfMemoryError条件的内存区域。虚拟机堆栈与程序计数器相同。Java虚拟机栈(JavaVirtualMachineStacks)也是线程私有的。如上图所示,每个线程都有自己的虚拟机栈。它的生命周期与线程相同。线程创建的时候,虚拟机栈也同时创建;当线程被销毁时,虚拟机栈也同时被销毁。在线程内部,每个方法执行时,都会同时创建一个栈帧(StackFrame),用于存放局部变量表、操作数栈、动态链接、方法出口等信息,如如上图所示。每个方法被调用到执行完成的过程对应着虚拟机栈中一个栈帧从入栈到出栈的过程。其中,栈帧中的局部变量表存储了各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(referencetype)和returnAddress类型(指向对象的地址)字节码指令)。其中64位长度的long和double类型数据会占用2个局部变量空间(Slots),其余数据类型只会占用1个slot。局部变量表所需的内存空间是在编译时分配的。当进入一个方法时,该方法需要在帧中分配多少局部变量空间是完全确定的,在方法运行过程中不会改变局部变量表的大小。在Java虚拟机规范中,针对这块区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常。让我们写一段代码让它抛出这个异常:/***VMArgs:-Xss128k*/publicclassJVMStackSOF{privateintstackLength=1;publicvoidstackLeak(){stackLength++;堆栈泄漏();}publicstaticvoidmain(String[]args){JVMStackSOFsof=newJVMStackSOF();尝试{sof.stackLeak();}catch(Throwablee){System.out.println("堆栈长度:"+sof.stackLength);扔e;}}}运行前设置JVM的参数为-Xss128k,运行结果如下:Stacklength:1002Exceptioninthread"main"java.lang.StackOverflowErroratOneMoreStudy.JVMStackSOF.stackLeak(JVMStackSOF.java:10)在OneMoreStudy.JVMStackSOF.stackLeak(JVMStackSOF.java:11)在OneMoreStudy。JVMStackSOF.stackLeak(JVMStackSOF.java:11)……当栈深度达到1002时,抛出StackOverflowError异常。如果虚拟机栈可以动态扩容,扩容时无法申请到足够的内存就会抛出OutOfMemoryError异常,或者让我们写一段代码让它抛出这个异常:/***VMArgs:-Xss2M*/publicclassJVMStackOOM{privatevoiddontStop(){while(true){}}publicvoidstackLeakByThread(){while(true){Threadt=newThread(newRunnable(){publicvoidrun(){dontStop();}});t.开始();}}publicstaticvoidmain(String[]args){JVMStackOOMoom=newJVMStackOOM();oom.stackLeakByThread();}}这段代码会创建无限多个线程,因为Java线程会映射到系统的内核线程,所以会造成CPU占用100%,系统假死等现象,请谨慎运行。运行前,将JVM参数设置为-Xss2M。运行了很久,结果如下:Exceptioninthread"main"java.lang.OutMemoryError:unabletocreatenewnativethreadatjava.lang.Thread.start0(NativeMethod)atjava.lang.Thread。start(UnknownSource)atOneMoreStudy.JVMStackOOM.stackLeakByThread(JVMStackOOM.java:18)atOneMoreStudy.JVMStackOOM.main(JVMStackOOM.java:24)NativeMethodStacks虚拟机栈起到的作用非常相似。不同的是,虚拟机栈服务于虚拟机执行的Java方法(即字节码),而本地方法栈服务于虚拟机使用的Native方法。.本地方法栈中方法的语言、用法、数据结构在虚拟机规范中没有强制要求,具体虚拟机可以自由实现。甚至有些虚拟机(如SunHotSpot虚拟机)直接将本地方法栈和虚拟机栈合二为一。和虚拟机栈一样,native方法栈区也会抛出StackOverflowError和OutOfMemoryError异常。堆Java堆(JavaHeap)是Java虚拟机管理的最大的一块内存。它是所有线程共享的内存区域,在虚拟机启动时创建。它用于存放对象实例,几乎所有的对象实例都在这里分配内存。堆是垃圾收集器管理的主要区域。从内存回收的角度来看,由于当前收集器基本采用分代收集算法,因此Java堆也可以细分为:新生代和老年代;有Eden空间,FromSurvivor空间,ToSurvivor空间等。从内存分配的角度来看,线程共享堆中可能会划分出多个线程私有分配缓冲区(ThreadLocalAllocationBuffer,TLAB)。根据Java虚拟机规范,Java堆可以在一个物理上不连续的内存空间,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现的时候,可以实现为固定大小,也可以是可伸缩的,但是目前主流的虚拟机都是按照可伸缩性来实现的(由-Xmx和-Xms控制)。如果堆中没有内存来完成实例分配,堆不能再扩展,就会抛出OutOfMemoryError异常。让我们写一段代码让它抛出这个异常:/**VMArgs:-Xms20M-Xmx20M*/publicclassHeapOOM{staticclassOOMObject{}publicstaticvoidmain(String[]args){Listlist=newArrayList();while(true){//将对象实例放入列表中,//使其始终被引用而不被垃圾回收list.add(newOOMObject());}}}运行前设置JVM的参数为-Xms20M-Xmx20M,运行结果如下:Exceptioninthread"main"java.lang.OutOfMemoryError:Javaheapspaceatjava.util.Arrays.copyOf(未知来源)在java.util.Arrays.copyOf(未知来源)在java.util.ArrayList.grow(未知来源)在java.util。ArrayList.ensureExplicitCapacity(UnknownSource)atjava.util.ArrayList.ensureCapacityInternal(UnknownSource)atjava.util.ArrayList.add(UnknownSource)atOneMoreStudy.HeapOOM.main(HeapOOM.java:18)方法区方法区(MethodArea)和Java堆一样,是各个线程共享的一块内存区域。用于存放已经被JVM加载的类信息、常量、静态变量等。对于习惯于在HotSpot虚拟机上开发部署程序的开发者来说,很多人愿意将方法区称为“永久代”(PermanentGeneration)。不等价,只是因为HotSpot虚拟机的设计团队选择了将GC分代收集扩展到方法区,或者使用永久代来实现方法区。在JDK7的HotSpot中,原来在永久代中的字符串常量池被去掉了。在JDK8的HotSpot中,没有永久代,而是一个新的内存空间:元空间(Metaspace)。JVM规范对这方面的限制非常宽松。除了不需要像Java堆那样连续的内存,可以选择固定大小或者可扩展,也可以选择不实现垃圾回收。相对来说,该区域的垃圾回收行为比较少见,但并不代表数据进入方法区就一直存储。该区域内存回收的目标主要是常量池的回收和类型的卸载。总的来说,这方面的恢复“得分”是比较差的,尤其是类型的卸载。条件相当苛刻,但在这部分地区回收利用确实是必要的。根据Java虚拟机规范,当方法区不能满足内存分配要求时,会抛出OutOfMemoryError异常。让我们再写一段代码让它抛出这个异常:/**VMArgs:-XX:PermSize=2M-XX:MaxPermSize=2M*/publicclassRuntimeConstantPoolOOM{publicstaticvoidmain(String[]args){Listlist=newArrayList();for(inti=0;i<100000;i++){System.out.println(i);//将i转换为字符串,//并调用intern(),将字符串放入运行时常量池list.add(String.valueOf(i).intern());}}}运行前设置JVM参数为-XX:PermSize=2M-XX:MaxPermSize=2M。在JDK6中运行老年代抛出OutOfMemoryError异常,结果如下:......3581335814Exceptioninthread"main"java.lang.OutOfMemoryError:PermGenspaceatjava.lang.String.intern(NativeMethod)在OneMoreStudy.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:12)在JDK7中运行。循环全部完成后,不会抛出异常。结果如下:......999969999799999899999同样一段代码,在不同版本的JDK中运行结果为什么不一样呢?这是因为:在JDK6中,字符串常量池还在永久代中,而在JDK7中,原本在永久代中的字符串常量池已经被移出。让我们再写一段代码让它抛出这个异常:/**VMArgs:-XX:PermSize=2M-XX:MaxPermSize=2M*/publicclassMethodAreaOOM{staticclassOOMObject{}publicstaticvoidmain(String[]args){for(inti=0;i<300;i++){System.out.println(i);创建新类();}}privatestaticvoidcreateNewClass(){//这里使用了CGLIB,动态创建类并加载方法区Enhancerenhancer=newEnhancer();enhancer.setSuperclass(OOMObject.class);enhancer.setUseCache(false);enhancer.setCallback(newMethodInterceptor(){@OverridepublicObjectintercept(Objectobj,Methodmethod,Object[]args,MethodProxyproxy)throwsThrowable{returnproxy.invokeSuper(obj,args);}});增强器.create();}}运行前设置JVM参数为-XX:PermSize=2M-XX:MaxPermSize=2M。JDK6运行在老年代抛出OutOfMemoryError异常,结果如下:...Causedby:java.lang.OutOfMemoryError:PermGenspaceatjava.lang.ClassLoader.defineClass1(NativeMethod)atjava.lang.ClassLoader。defineClassCond(UnknownSource)atjava.lang.ClassLoader.defineClass(UnknownSource)...12more在JDK7中运行也抛出OutOfMemoryError异常,结果如下:Exceptioninthread"main"Exception:java.lang.OutOfMemoryError在JDK8中运行的线程“main”中的UncaughtExceptionHandler抛出。循环全部完成后,不会抛出异常。结果如下:......298299JavaHotSpot(TM)64-BitServerVMwarning:ignoringoptionPermSize=2M;在8.0JavaHotSpot(TM)64位服务器VM警告中删除了支持:忽略选项MaxPermSize=2M;8.0中去掉了对同一段代码的支持,为什么在不同版本的JDK中运行结果不一样??这是因为:在JDK6和JDK7中,永久代依然存在,而在JDK8中,永久代已经不存在了,而是新增了一个内存空间:metaspace,同时JVM参数PermSize和MaxPermSize也被去掉了。.总结在JVM管理的内存中,大致分为:程序计数器、虚拟机栈、本地方法栈、堆和方法区。程序计数器是当前线程正在执行的字节码行号的指示器。虚拟机栈是Java方法执行的内存模型,用于存放局部变量表、操作数栈、动态链接、方法出口等信息。nativemethodstack是nativemethod执行的内存模型,和虚拟机栈很像。不同之处在于本地方法栈服务于JVM使用的本地方法。堆用于存放对象实例,是垃圾收集器管理的主要区域。方法区用于存放JVM加载的类信息、常量、静态变量、即时编译器编译的代码等数据。