当前位置: 首页 > 科技观察

jvm系列(二):JVM内存结构

时间:2023-03-17 19:05:06 科技观察

所有Java开发者都可能遇到这样的困惑?我应该为堆内存设置多少空间?OutOfMemoryError异常涉及到运行时数据的哪些区域?如何解决?事实上,如果你经常解决服务器性能问题,这些问题就会变得很普遍。了解JVM内存也是为了在服务器出现性能问题时快速了解内存区域的问题,从而快速解决问题。排查生产故障。先看一张图,这张图可以很清楚的说明JVM的内存结构布局。JVM内存结构主要有三大块:堆内存、方法区、栈。堆内存是JVM中最大的部分,由新生代和老年代组成,新生代内存分为三部分,Eden空间,FromSurvivor空间,ToSurvivor空间。默认情况下,新生代是8:1:1的方法区,存放类信息、常量、静态变量等数据。它是线程共享的区域。为了区别于Java堆,方法区还有一个别名Non-Heap(非堆);栈分为java虚拟机栈和native方法栈,主要用于方法执行。用一张图来了解如何通过参数来控制各个区域的内存大小。控制参数-Xms设置堆的最小空间大小。-Xmx设置堆的最大空间大小。-XX:NewSize设置新生代的最小空间大小。-XX:MaxNewSize设置新生代的最大空间大小。-XX:PermSize设置第一代的最小空间大小。-XX:MaxPermSize设置***代***空间的大小。-Xss设置每个线程的堆栈大小。没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小这两个参数来间接控制。老年代空间大小=堆空间大小-新生代大空间大小从更高的维度,再看一下JVM和系统调用的关系。方法区是所有线程共享的内存区域;而java栈和本地方法栈以及Programmer'sCounter运行在线程私有的内存区域。下面我们详细介绍各个区域的作用。Java堆(Heap)对于大多数应用来说,Java堆(JavaHeap)是Java虚拟机管理的最大的一块内存。Java堆是虚拟机启动时创建的所有线程共享的内存区域。这块内存区的唯一用途就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此常被称为“GC堆”。从内存回收的角度来看,由于目前的收集器基本都是采用分代收集算法,所以Java堆也可以细分为:新生代和老年代;更详细的包括Edenspace,FromSurvivorspace,ToSurvivorspace等。根据Java虚拟机规范,Java堆可以在物理上不连续的内存空间,只要逻辑上连续,就像我们的磁盘空间一样.在实现的时候,可以实现为固定大小,也可以是可伸缩的,但是目前主流的虚拟机都是按照可伸缩性来实现的(由-Xmx和-Xms控制)。如果堆中没有内存来完成实例分配,堆不能再扩展,就会抛出OutOfMemoryError异常。方法区(MethodArea)和Java堆一样,方法区是各个线程共享的一块内存区域。用于存放类信息、常量、静态变量、虚拟机已经加载的just-in-time编译器编译的代码。和其他数据。虽然Java虚拟机规范将方法区描述为堆的逻辑部分,但它有一个别名叫做Non-Heap(非堆),应该与Java堆区分开来。对于习惯于在HotSpot虚拟机上开发部署程序的开发者来说,很多人愿意将方法区称为“永久代”(PermanentGeneration)。两者在本质上并不等价,只是因为HotSpot虚拟机设计团队选择了将GC分代收集扩展到方法区,或者使用第一代来实现方法区。Java虚拟机规范对这方面的限制非常宽松。除了不需要像Java堆那样连续的内存,可以选择固定大小或者可扩展,也可以选择不实现垃圾回收。相对来说,这个区域的垃圾回收行为比较少见,但并不是数据进入方法区,就像初代的名字一样,“***”是存在的。该区域内存回收的目标主要是常量池的回收和类型的卸载。总的来说,这方面的恢复“得分”是比较差的,尤其是类型的卸载。条件相当苛刻,但在这部分地区回收利用确实是必要的。根据Java虚拟机规范,当方法区不能满足内存分配要求时,会抛出OutOfMemoryError异常。方法区有时被称为持久代(PermGen)。在实例化后的整个运行周期中,所有对象都存储在堆内存中。堆内存分为不同的部分:Eden、SurvivorSpace和OldGenerationSpace。方法的执行伴随着线程。局部变量和引用的原始类型存储在线程堆栈中。与引用关联的对象(例如String)存在于堆中。为了更好地理解上面这段话,我们可以看一个例子:));publicvoidsayHello(Stringmessage){SimpleDateFormatformatter=newSimpleDateFormat("dd.MM.YYYY");Stringtoday=formatter.format(newDate());LOGGER.info(today+":"+message);}}这个程序数据在内存中的存储情况如下:通过JConsole工具,可以查看正在运行的Java程序(如Eclipse)的一些信息:堆内存的分配情况、线程数、加载的类数;程序计数器(ProgramCounterRegister)程序计数器(ProgramCounterRegister)是一块很小的内存空间,它的作用可以看作是当前线程执行的字节码的行号指示符。在虚拟机的概念模型中(只是一个概念模型,各种虚拟机可能有一些更高效的实现方式),字节码解释器通过改变这个计数器的值来选择下一个要执行的基本功能,如字节码指令、分支、循环、跳转、异常处理、线程恢复都需要依赖这个计数器来完成。由于Java虚拟机的多线程是通过轮流切换线程,分配处理器执行时间来实现的,所以在任何给定时刻,一个处理器(多核处理器的一个核)只会执行一个线程中的指令。因此,为了在线程切换后回到正确的执行位置,每个线程都需要有一个独立的程序计数器。线程间的计数器互不影响,独立存储。我们称这种类型的内存区域为“线程私有”。"内存。如果线程正在执行Java方法,这个计数器记录正在执行的虚拟机字节码指令的地址;如果正在执行Natvie方法,则计数器值为空(未定义)。这块内存区域是唯一的在Java虚拟机规范中没有规定任何OutOfMemoryError条件。JVM栈(JVMStacks)和程序计数器一样,Java虚拟机栈(JavaVirtualMachineStacks)也是线程私有的,它的生命周期和线程一样,虚拟机栈描述了Java方法执行的内存模型:每个方法执行时,同时创建一个栈帧(StackFrame)来存放局部变量表等信息,操作栈、动态链接、方法出口,每个方法被调用到执行完成的过程,对应虚拟机栈中一个栈帧从入栈到出栈的过程。局部变量表存储编译时已知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(引用类型,不等同于对象本身,根据不同的虚机器实现,它可能是指向对象起始地址的引用指针,也可能指向代表对象的句柄或与对象相关的其他位置)和returnAddress类型(指向字节码指令的地址)。其中,64位的long和double类型数据会占用2个局部变量空间(Slots),其余数据类型只会占用1个slot。局部变量表所需的内存空间是在编译时分配的。当进入一个方法时,该方法需要在帧中分配多少局部变量空间是完全确定的,在方法运行过程中不会改变局部变量表的大小。在Java虚拟机规范中,针对该区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(目前大多数Java虚拟机都可以动态扩展,但是Java虚拟机规范也允许定长虚拟机栈),当扩展无法申请到足够的内存时,抛出OutOfMemoryError异常将被抛出。原生方法栈(NativeMethodStacks)原生方法栈(NativeMethodStacks)和虚拟机栈很相似,不同的是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈服务于虚拟机使用的Native方法。本地方法栈中方法的语言、用法和数据结构在虚拟机规范中没有强制要求,具体虚拟机可以自由实现。甚至有些虚拟机(如SunHotSpot虚拟机)直接将本地方法栈和虚拟机栈合二为一。和虚拟机栈一样,native方法栈区也会抛出StackOverflowError和OutOfMemoryError异常。Where'sOutOfMemoryError清楚了解内存结构也可以帮助理解不同的OutOfMemoryErrors:Exceptioninthread"main":java.lang.OutOfMemoryError:JavaheapspaceReason:ObjectscannotbeallocatedtoheapmemoryReason:classormethodcannotbeloadedintoold一代。可能出现在一个程序加载了很多类的时候,比如引用了很多第三方库;Exceptioninthread“主”:java。lang.OutOfMemoryError:requestbytesfor.Outofswapspace?原因:分配本地分配失败。JNI、本机库或Java虚拟机将从本机堆分配内存空间。Exceptioninthread"main":java.lang.OutOfMemoryError:(Nativemethod)原因:native方法的内存分配也失败了,但是被JNI或者native方法或者Java虚拟机发现了【本文是专栏作者《纯洁的微笑》原创稿件,请微信联系作者公众号获得授权】点此查看该作者更多好文