前言本JVM系列属于我在学习过程中总结的一些知识点。目的是让读者更快的掌握JVM相关知识的重点。想要更详细的学习JVM知识,还是需要阅读专业的书籍和文档。本文主题:JVM内存区概述堆区的空间分配是怎样的?堆溢出演示创建一个新对象内存是如何分配的?从方法区到元空间的栈帧是什么?栈帧中有什么?如何理解?什么是native方法栈程序计数器CodeCache?注意:请区分JVM内存结构(内存布局)和JMM(Java内存模型)这两个不同的概念!概述内存是非常重要的系统资源。它是硬盘和CPU之间的中间仓库和桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java运行过程中内存的申请、分配和管理策略,保证了JVM的高效稳定运行。上图描述了当前经典的Java内存布局。(堆区小了2333,逻辑上应该是最大的区)如果按照线程是否共享来分类,如下图所示:PS:线程是否共享,实际理解后每个区域的实际使用,自然记住就可以了。无需死记硬背。让我们仔细看看每个区域。1.Heap(堆区)1.1堆区介绍先说堆。堆是发生OOM失败的主要区域。它是内存区中最大的一块区域,所有线程共享,存放了几乎所有的实例对象和数组。所有的对象实例和数组都必须在堆上分配,但是随着JIT编译器的发展和逃逸分析技术的逐渐成熟,在栈上的分配和标量替换优化技术会带来一些微妙的变化。所有的对象都分配在堆上,也逐渐变得不那么“绝对”了。扩展知识点:JIT编译优化部分——逃逸分析。推荐阅读:深入理解Java中的逃逸分析Java堆是垃圾收集器管理的主要区域,因此常被称为“GC堆”。从内存回收的角度来看,由于现在收集器基本采用分代收集算法,所以Java堆又可以进一步细分为:新生代和老年代。更详细的包括Edenspace,FromSurvivorspace,ToSurvivorspace等。从内存分配的角度来看,线程共享的Java堆中可能会划分出多个线程私有的分配缓冲区(ThreadLocalAllocationBuffer,TLAB)。但是,无论怎么划分,都与存储的内容无关。无论在哪个区域,仍然存储对象实例。进一步划分的目的是为了更好的回收内存或者更快的分配内存。1.2堆区的调整根据Java虚拟机规范,Java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可,就像我们的磁盘空间一样。实现时,可以实现为固定大小,也可以在运行时动态调整。如何调整?通过设置如下参数,可以设置堆区的初始值和最大值,比如-Xms256M-Xmx1024M,其中字母-X表示是JVM运行时参数,ms是memorystart的缩写,中文意思是内存初始值,mx是memorymax的缩写,意思是最大内存。值得注意的是,一般情况下,服务器在运行时,堆空间不断地扩容和缩容,会造成不必要的系统压力。因此线上生产环境中JVM的Xms和Xmx会被设置成相同的大小,避免GC后调整堆大时的额外压力。1.3堆的默认空间分配另外,我强调一下堆空间的内存分配的一般情况。这里可能有人要问了,你从哪里知道的?如果我要配置这个比例,应该怎么修改呢?先说一下怎么查看虚拟机的默认配置。在命令行执行以下命令,查看当前JDK版本的所有默认JVM参数。java-XX:+PrintFlagsFinal-version输出对应的输出应该有几百行。我们看一下堆内存分配相关的两个参数>java-XX:+PrintFlagsFinal-version[Globalflags]...uintxInitialSurvivorRatio=8uintxNewRatio=2...javaversion"1.8.0_131"Java(TM)SERuntimeEnvironment(build1.8.0_131-b11)JavaHotSpot(TM)64-BitServerVM(build25.131-b11,mixedmode)Parameters解释参数的作用-XX:InitialSurvivorRationew中Eden/Survivor空间的初始比例generation-XX:ThememoryratioofNewRatioOldarea/Youngarea因为新生代是由Eden+S0+S1组成的,按照上面默认的比例,如果eden区的内存大小是40M,那么两个survivor区都是5M,整个young区是50M,然后可以算出old区的内存大小是100M,heap区的总大小是150M。1.4堆溢出演示/***VMArgs:-Xms10m-Xmx10m-XX:+HeapDumpOnOutOfMemoryError*@authorRichard_Yi*/publicclassHeapOOMTest{publicstaticfinalint_1MB=1024*1024;publicstaticvoidmain(String[]args){ListbyteList=newArrayList<>(10);for(inti=0;i<10;i++){byte[]bytes=newbyte[2*_1MB];byteList.add(bytes);}}}输出java.lang.OutOfMemoryError:JavaheapspaceDumpingheaptojava_pid32372.hprof...创建堆转储文件[0.009秒内7774077字节]线程“main”中的异常java.lang.OutOfMemoryError:Javaheapspaceatjvm。HeapOOMTest.main(HeapOOMTest.java:18)-XX:+HeapDumpOnOutOfMemoryError允许JVM在遇到OOM异常时输出堆信息,特别是几个月后发生的OOM异常。创建新对象的内存分配过程看完了上面对堆的介绍,下面我们就趁热打铁,学习一下JVM创建新对象的内存分配过程。大多数对象都在Eden区域中生成。当Eden区域已满时,将触发YoungGarbageCollection,即YGC。垃圾回收时,在Eden区执行清除策略,直接回收没有被引用的对象。仍然存在的对象将被移动到Survivor区域。Survivor分为两个内存空间,so和s1。每次YGC时,他们将幸存的对象复制到未使用的空间,然后彻底清除当前正在使用的空间,并交换两个空间的使用状态。如果YGC要转移的对象大于Survivor区容量上限,则直接转移到老年代。一个对象不可能永远留在年轻代,就像一个人到了18岁就会成年。在JVM中,-XX:MaxTenuringThreshold参数是配置一个对象从中晋升的阈值新一代到老一代。默认值为15,在Survivor区兑换14次后可晋升至老年。以上是垃圾回收的一些术语。不熟悉的读者可以查阅资料或阅读本系列垃圾回收章节。2.Metaspace在HotSpotJVM中,永久代(≈方法区)用于存储类和方法以及常量池的元数据,例如Class和Method。每当第一次加载一个类时,它的元数据就被放置在永久代中。永久代的大小是有限的,所以如果加载过多的类,很可能会导致永久代内存溢出,也就是万恶的java.lang.OutOfMemoryError:PermGen,所以我们要调优虚拟机。那么为什么PermGen在Java8中从HotSpotJVM中移出呢?(参见:JEP122:RemovethePermanentGeneration):因为PermGen内存经常溢出,导致恼人的java.lang.OutOfMemoryError:PermGen,JVM开发者希望这块内存可以更灵活的管理,而不是经常这样的OOM去除PermGen可以促进HotSpotJVM和JRockitVM的融合,因为JRockit没有永久代。根据以上原因,PermGen最终被移除,方法区移至Metaspace,字符串常量池移至堆区。准确的说是Perm区的字符串常量池在Java7之后被移到了堆内存中。在Java8中,PermGen被Metaspace取代,类元信息、字段、静态属性、方法、常量等其他内容移至Metaspace区域。例如java/lang/Object类元信息,静态属性System.out,整型常量100000等。元空间的本质类似于永久代,是JVM规范中方法区的实现.但是,元空间与永久代最大的区别在于,元空间不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存的限制。(就像后面提到的直接内存一样,使用本地内存。)在JDK8中,类元数据现在存储在本机堆中,这个空间称为元空间。对应的JVM调优参数:参数函数-XX:MetaspaceSize分配给Metaspace的初始大小(以字节为单位)-XX:MaxMetaspaceSize分配给Metaspace的最大值。如果超过这个值,就会触发FullGC。这个值默认没有限制,但是应该取决于系统内存的大小。JVM将动态更改此值。-XX:MinMetaspaceFreeRatioGC后,Metaspace剩余空间容量的最小百分比,减少为分配空间引起的垃圾收集-XX:MaxMetaspaceFreeRatioGC后,Metaspace剩余空间容量的最大百分比,减少为空闲空间进一步阅读垃圾收集:关于Metaspace的两篇好文章。Java中的Metaspace8lovestblog.cn/blog/2016/1…3.Java虚拟机栈对于每一个线程,JVM会在创建线程时创建一个单独的栈。也就是说,虚拟机栈的生命周期与线程一致,是线程私有的。Java方法除了Native方法外,都是通过Java虚拟机栈(需要程序控制器、堆、元空间数据的配合)实现调用和执行过程。所以Java虚拟机栈是虚拟机执行引擎的核心之一。Java虚拟机栈中弹出栈的元素称为“栈帧”。栈帧(StackFrame)是一种数据结构,用于支持虚拟机进行方法调用和方法执行。栈帧中存放了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息。每个方法从调用到执行完成的过程对应于一个栈帧在虚拟机栈中从入栈到出栈的过程。栈对应线程,栈帧对应的方法在活动线程中。只有栈顶的帧是有效的,称为当前栈帧。正在执行的方法称为当前方法。当执行引擎运行时,所有指令只能对当前栈帧进行操作。而StackOverflowError表示请求的栈溢出,导致内存耗尽,通常在递归方法中。虚拟机栈通过pop和push对各个方法对应的活跃栈帧进行操作。方法正常执行后,肯定会跳转到另一个栈帧。执行过程中,如果出现异常,会进行异常回溯,通过异常处理表确定返回地址。可见栈帧在整个JVM系统中的地位很高。下面也具体介绍栈帧中的存储信息。1、局部变量表局部变量表是存放方法参数和方法内部定义的局部变量的区域。局部变量表所需的内存空间是在编译时分配的。当进入一个方法时,该方法需要在帧中分配多少局部变量空间是完全确定的,在方法运行过程中不会改变局部变量表的大小。这里直接上代码,方便理解。publicinttest(inta,intb){Objectobj=newObject();returna+b;}如果局部变量是Java的8种基本数据类型,就会存在局部变量表中,如果是引用类型。比如对于new出来的String,引用存放在局部变量表中,实例在堆中。2.操作数栈(OperandStack)看名字就知道是栈结构。Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。JVM在为方法创建栈帧时,会在栈帧中为该方法创建一个操作数栈,以保证方法中的指令能够完成工作。或者通过实践来理解。/***@authorRichard_yyf*/publicclassOperandStackTest{publicintsum(inta,intb){returna+b;}}编译生成.class文件后,反汇编查看汇编指令>javacOperandStackTest.java>javap-vOperandStackTest.class>1.txtpublicintsum(int,int);descriptor:(II)Iflags:ACC_PUBLICCode:stack=2,locals=3,args_size=3//最大栈深度为2个局部变量个数为30:iload_1//局部变量1入栈1:iload_2//局部变量2入栈2:iadd//将栈顶的两个元素相加,计算结果入栈3:ireturnLineNumberTable:line10:03.动态连接Each栈帧包含对常量池中当前方法的引用,目的是支持方法调用过程的动态连接。4、方法返回地址方法执行时有两种退出情况:正常退出,即正常执行返回任何方法的字节码指令,如RETURN、IRETURN、ARETURN等异常退出,无论退出情况如何,它将返回到方法当前调用的位置。方法退出的过程相当于弹出当前栈帧。退出可能有三种方式:将返回值压入上层调用栈帧,将异常信息抛到可处理的栈帧。PC计数器指向方法调用后的下一条指令。延伸阅读:JVM机器指令集图4、原生方法栈原生方法栈(NativeMethodStack)和虚拟机栈起到的作用非常相似,它们的区别在于虚拟机栈执行的是Java方法(即,字节码)为虚拟机)服务,本地方法栈为虚拟机使用的Native方法服务。在虚拟机规范中,对于本地方法栈中的方法使用的语言、用法、数据结构等并没有强制规定,具体的虚拟机可以自由实现。甚至有些虚拟机(如SunHotSpot虚拟机)直接将本地方法栈和虚拟机栈合二为一。和虚拟机栈一样,native方法栈区也会抛出StackOverflowError和OutOfMemoryError异常。五、程序计数器程序计数器(ProgramCounterRegister)是一个很小的内存空间。是线程私有的。它可以看作是当前线程执行的字节码的行号指示符。这意味着什么?白话版:因为代码是在线程中运行的,所以线程可能会被挂起。即CPU执行了一段时间线程A,线程A还没执行完就被挂起,然后执行线程B,最后再次执行线程A。CPU需要知道执行线程A的指令的哪一部分,线程计数器会告诉CPU。由于Java虚拟机的多线程是通过轮流切换线程,分配处理器执行时间来实现的,只有数据加载到寄存器中,CPU才能运行。寄存器存储指令相关的场景信息。由于CPU时间片的限制,在多个线程并发执行的过程中,在某一时刻,一个处理器或多核处理器中的一个核只会执行其中一个线程。操作说明。因此,为了在线程切换后回到正确的执行位置,每个线程都需要有一个独立的程序计数器,每个线程之间的计数器互不影响,独立存储。每个线程创建后,都会生成自己的程序计数器和栈帧。程序计数器用于存储已执行指令的偏移量和行号指示符。线程执行或恢复取决于程序计数器。该区域也不会发生内存不足异常。6、直接内存直接内存(DirectMemory)不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也是经常被使用的,也有可能会导致OutOfMemoryError异常,所以我们把它放在一起说明一下。JDK1.4中新增了NIO(NewInput/Output)类,引入了一种基于通道(Channel)和缓冲区(Buffer)的I/O方法,可以使用Native函数库直接分配堆外内存,然后通过存储在Java堆中的一个DirectByteBuffer对象作为对这块内存的引用进行操作。这在某些场景下可以显着提高性能,因为它避免了在Java堆和Native堆之间来回复制数据。很明显,机器直接分配内存不会受到Java堆大小的限制,但既然是内存,肯定会受到机器总内存(包括RAM和SWAP区或分页)大小的限制文件)和处理器的寻址空间。如果内存区域的总和大于物理内存的限制,也会发生OOM。代码缓存简而言之,JVM代码缓存是JVM将其字节码存储为本机机器代码的区域。我们将每个可执行本机代码块称为一个nmmethod。nmethod可以是完整的或内联的Java方法。即时(JIT)编译器是代码缓存区的最大消费者。这就是为什么一些开发人员将此内存称为JIT代码缓存的原因。这部分代码占用的内存空间成为CodeCache区域。一般情况下,我们不会关心这块区域,大多数开发者也不熟悉这块区域。如果这个区域OOM,你会在日志中看到java.lang.OutOfMemoryError代码缓存。延伸阅读JVM代码缓存简介1、《深入理解Java虚拟机》-周志明2、《码出高效》3、Java84中的Metaspace、JVM机器指令集图5、JVM代码缓存简介