jvm之内存结构讲解

Posted java技术阅读

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jvm之内存结构讲解相关的知识,希望对你有一定的参考价值。

I 前言

前文我们介绍了类加载到JVM内存中的过程。那你是不是好奇,jvm是如何运行这段程序的?为什么加载到jvm的这段代码就能完成我们需要的功能?为什么程序运行过程中会抛出OutOfMemoryError异常。今天我们就带着这些疑问来了解下jvm的内存结构。

其中黄色部分方法区和堆是线程共享区,蓝色部分jvm栈,本地方法栈及程序计数器是线程私有区。


I 程序计数器

jvm之内存结构讲解


程序计数器在CPU内部,是最快的存储区(寄存器)。在字节码解释器工作时,就是通过改变程序计数器的值来选取下一条要执行的指令,分支、循环、跳转等基础功能都是依赖此技术去完成的。


在java多线程方面,多线程就是通过线程轮流切换而达到的,同一时刻,一个内核只能执行一个指令,所以,对于每一个程序来说,必须有一个计数器来记录程序的执行进度,这样,当线程恢复执行的时候,才能从正确的地方开始,所以,每个线程都必须有一个独立的程序计数器,这类计数器为线程私有的内存。


此块区域是在jvm规范中没有规定任何OutOfMemoryError情况的区域,程序计数器由jvm内部维护,不需要开发者进行操作。


I jvm栈

上面的代码中,先执行Foo()方法,所以将Foo()及其参数 (i,x,m)方法载入栈中,top指向Foo()方法,执行过程中,发现Foo()方法调用了Bar()方法,为Bar()方法分配一个栈块,将Bar方法及其参数 i 载入到栈块中,位于Foo()方法的上面,top指向Bar()方法,当执行完Bar()方法之后,自动收回其所在的栈块,进行出栈操作,top指向Foo()方法,然后继续执行Foo()方法,当其执行完以后,自动收回存放了该方法及其参数的存储空间,top指向栈底。



每个线程有一个私有的栈,随着线程的创建而创建,生命周期与线程相同。


如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈动态扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。


操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。


动态链接:Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。


方法返回:无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。


配置参数:

  • -Xss 设置每个线程的堆栈大小


I 本地方法栈

与jvm栈类似,两者的区别就是jvm栈是为jvm执行java方法服务,本地方法栈为jvm执行native方法服务。


虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。比如hotSpot虚拟机不区分jvm栈和本地方法栈,两者是一块的。


与jvm栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。


I 方法区

方法区用于存储jvm加载的类信息(类的版本、字段、方法和接口),常量,静态变量,即时编译器编译后的代码数据等。


方法区是jvm规范去中定义的一种概念上的区域,但并没有规定这个区域到底应该位于何处,因此对于实现者来说,如何来实际方法区是有着很大自由度的。


为了弄清楚方法区,那么需先了解下两个名词:永久代(PermGen)和元空间。


永久代


我想大部分的java程序员都应该见过“java.lang.OutOfMemoryError: PremGen space”异常,这里的"PermGen space"指的就是方法区。不过方法区和“PermGen space”又有着本质的区别。前者是JVM的规范,而后者则是JVM规范的一种实现,并且只有HotSpot才有“PermGen space”,而对于其他类型的虚拟机,如JRockit(Oracle)、J9(IBM)并没有“PermGen space”。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。


jdk1.7之前,方法区也称“永久代”,是所有线程共享的资源。当永久代区域内存消耗解决上限,就会触发FullGC。


配置参数:

  • -XX:PermSize设置永久代最小空间大小

  • -XX:MaxPermSize设置永久代最大空间大小


元空间


JDK1.8对JVM架构的改造将类元数据放到本地内存中,另外,将常量池和静态变量放到Java堆里。HotSpot VM将会为类的元数据明确分配和释放本地内存。在这种架构下,类元信息就突破了原来-XX:MaxPermSize的限制,现在可以使用更多的本地内存。这样就从一定程度上解决了原来在运行时生成大量类造成经常Full GC问题,如运行时使用反射、代理等。所以升级以后Java堆空间可能会增加。


元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间的最大区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。


配置参数:

  • -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对改值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。

  • -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

  • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集。

  • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集。


对于方法区,java8之后的变化:

  • 移除了永久代(PermGen),替换为元空间(Metaspace);

  • 永久代中的 class metadata 转移到了 native memory(本地内存,而不是虚拟机);

  • 永久代中的字面量( interned Strings) 和 类的静态变量(class static variables) 转移到了 Java heap;

  • 永久代参数 (PermSize MaxPermSize) -> 元空间参数(MetaspaceSize MaxMetaspaceSize)


I 

java堆存储在RAM中,用于存放对象实例(包含对象实例的变量,即所属对象的成员变量)和数组。堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。在使用堆存储时,能够动态的分配内存空间,而不需要知道存储的数据的生命周期。这种动态分配内存大小的代价是用堆进行存储分配和清理时比用栈更花费时间。


堆是java垃圾回收的重点对象,由Java虚拟机的自动垃圾回收器来回收不再使用的数据。因为GC的存在,现代收集器基本都采用分代收集算法。


内存回收的角度上看,堆可分为新生代(Eden空间、From Survior空间、To Survior空间)和老年代。


新生代:新创建的对象放入Eden空间,GC之后,存活的对象由Eden区和S0区进入S1区,再次GC,存活的对象由Eden区和S1区进入S0区。所以s0和s1两块Survivor 区同时至少有一个为空闲的。


老年代:当每次对象从Eden 复制到Survivor Space 或者从Survivor Space 之间复制,计数器会自动增加其值。默认情况下如果复制发生超过16次,JVM 就会停止复制并把他们移到老年代中去。


如果新创建对象比较大(比如长字符串或大数组),新生代空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。


老年代的空间一般比新生代大,能存放更多的对象,在老年代上发生的GC次数也比年轻代少。


配置参数:

  • -Xms:初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制

  • -Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制

  • -Xmn:新生代的内存空间大小,即Eden+ 2个survivor space。在保证堆大小不变的情况下,增大新生代后,将会减小老生代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

  • -XX:SurvivorRatio:新生代中Eden区域与Survivor区域的容量比值,默认值为8。两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。


内存分配的角度上看,当给对象分配内存时,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。针对这种问题,有两种解决方案:

  1. 对分配内存空间的动作进行同步处理,保证更新操作的原子性(采用CAS + 失败重试机制保障原原子性),但效率较低。

  2. 使用本地线程分配缓冲(TLAB),哪个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并需要分配新的TLAB时,才需要同步锁定(可通过-XX:+/-UseTLAB参数来设定虚拟机启用TLAB)。


Java虚拟机规范规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。也就是说堆的内存是一块块拼凑起来的。要增加堆空间时,往上“拼凑”(可扩展性)即可,但当堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。


经过上面对jvm内存结构的讲解,我想大家对jvm内存结构也有所了解了。


以上是关于jvm之内存结构讲解的主要内容,如果未能解决你的问题,请参考以下文章

你真的懂JVM内存结构吗?—深入理解JVM之内存结构

Java面试题超详细讲解系列之四Jvm篇

JVM之内存结构

深入理解 Java 虚拟机之学习笔记

jvm专题 - 内存结构

JAVA之自动内存管理机制