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

Java培训:JVM内存布局

时间:2023-04-02 10:16:20 Java

概述内存是非常重要的系统资源,是硬盘和CPU之间的中间仓库和桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java运行过程中内存的申请、分配和管理策略,保证了JVM的高效稳定运行。上图描述了当前经典的JVM内存布局。(堆区小了2333,逻辑上应该是最大的区)如果按照线程是否共享来分类,如下图所示:PS:线程是否共享,实际理解后每个领域的实际使用,Java培训自然是背下来了。无需死记硬背。让我们仔细看看每个区域。1.Heap(堆区)1.1堆区介绍先说堆。堆是发生OOM失败的主要区域。它是内存区中最大的一块区域,所有线程共享,存放了几乎所有的实例对象和数组。所有的对象实例和数组都必须分配在堆上,但是随着JIT编译器的发展和逃逸分析技术的逐渐成熟,在栈上的分配和标量替换优化技术会带来一些微妙的变化,所有的对象都分配在堆也逐渐变得不那么“绝对”。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)parameterexplanation因为Thenewgeneration由Eden+S0+S1组成,所以按照上面的默认比例,如果eden区的内存大小是40M,那么两个survivor区是5M,整个young区是50M,然后是内存旧区的大小可以按100M来计算。堆的总大小为150M。1.4堆溢出演示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字节]线程“主”java中的异常.lang.OutOfMemoryError:Javaheapspaceatjvm.HeapOOMTest.main(HeapOOMTest.java:18)-XX:+HeapDumpOnOutOfMemoryError可以让JVM遇到OOM异常时,输出堆信息很重要,尤其是OOM异常相隔数月发生。创建新对象的内存分配过程看完了上面对堆的介绍,下面我们就趁热打铁,学习一下JVM创建新对象的内存分配过程。大多数对象都在Eden区域中生成。当Eden区域已满时,将触发YoungGarbageCollection,即YGC。垃圾回收时,在Eden区执行清除策略,直接回收没有被引用的对象。仍然存在的对象将被移动到Survivor区域。Survivor分为两个内存空间,so和s1。每次java培训机构YGC,都是将存活的对象复制到未使用的空间,然后彻底清除当前正在使用的空间,交换两个空间的使用状态。如果YGC要转移的对象大于Survivor区容量上限,则直接转移到老年代。一个对象不可能永远留在年轻代,就像一个人到了18岁就会成年。在JVM中,-XX:MaxTenuringThreshold参数是配置一个对象从中晋升的阈值新一代到老一代。默认值为15,在Survivor区兑换14次后可晋升至老年。2.Metaspace在HotSpotJVM中,永久代(≈方法区)用于存储类和方法以及常量池的元数据,例如Class和Method。每当第一次加载一个类时,它的元数据就被放置在永久代中。永久代的大小是有限的,所以如果加载过多的类,很可能会导致永久代内存溢出,也就是万恶的java.lang.OutOfMemoryError:PermGen,所以我们要调优虚拟机。那么为什么PermGen在Java8中从HotSpotJVM中移出呢?因为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调优参数: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在为方法创建栈帧时,会在栈帧中为该方法创建一个操作数栈,以保证方法中的指令能够完成工作。或者通过实践来理解。publicclassOperandStackTest{publicintsum(inta,intb){returna+b;}}编译生成.class文件后,反汇编查看汇编指令javacOperandStackTest.javajavap-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//局部变量2push2:iadd//将栈顶的两个元素相加,结果入栈3:ireturnLineNumberTable:line10:03.每个栈帧ofdynamiclink在常量池中包含一个对当前方法的引用,目的是支持方法调用过程的动态连接。4、方法返回地址方法执行时有两种退出情况:正常退出,即正常执行返回任何方法的字节码指令,如RETURN、IRETURN、ARETURN等异常退出,无论退出情况如何,它将返回到方法当前调用的位置。方法退出的过程相当于弹出当前栈帧。退出可能有三种方式:将返回值压入上层调用栈帧,将异常信息抛到可处理的栈帧。PC计数器指向方法调用后的下一条指令。4.本地方法栈local方法栈(NativeMethodStack)和虚拟机栈的作用非常相似。它们的区别在于,虚拟机栈是为虚拟机执行Java方法(也就是字节码)服务的,而native方法栈是为虚拟机使用的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代码缓存。来自GlacierTechnologies的诊断选项文章

猜你喜欢