美文网首页面试题jvmjava
JVM内存结构、运行时内存以及类加载过程

JVM内存结构、运行时内存以及类加载过程

作者: 雪飘千里 | 来源:发表于2019-12-06 16:13 被阅读0次

    以下内容都是基于jdk1.8

    1、JVM 内存管理

    image.png

    2、JVM内存区域

    image.png

    JVM内存区域主要分为线程私有Thread Local区域(程序计数器,虚拟机栈,本地方法区)、线程共享Thread Shared区域(java heap堆、方法区)、直接内存Direct Memory。

    • 线程私有Thread Local数据区域:生命周期与线程相同,依赖用户线程的启动/结束 而 创建/销毁。

    • 线程共享Thread shared区域:随虚拟机的启动/关闭 而 创建/销毁。

    • 直接内存Direct Memory:并不是JVM运行时数据区的一部分。
      但也会被频繁的使用:在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用DirectByteBuffer 对象作为这块内存的引用进行操作, 这样就避免了在 Java堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。

    image.png image.png

    1、程序计数器(线程私有Thread Local)

    一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每个线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
    正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
    这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。

    2、虚拟机栈(线程私有Thread Local)

    描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
    栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。

    栈用来存储线程的局部变量表、操作数栈、动态链接、方法出口等信息。如果请求栈的深度不足时抛出的错误会包含类似下面的信息:
    java.lang.StackOverflowError

    另外,由于每个线程占的内存大概为1M,因此线程的创建也需要内存空间。操作系统可用内存-Xmx-MaxPermSize即是栈可用的内存,如果申请创建的线程比较多超过剩余内存的时候,也会抛出如下类似错误:
    java.lang.OutofMemoryError: unable to create new native thread

    相关的JVM参数有:
    -Xss: 每个线程的堆栈大小,JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.
    在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

    3、本地方法栈(线程私有Thread Local)

    本地方法区和 Java Stack 作用类似,区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务,
    如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。

    4、堆heap(线程共享Thread shared)——运行时数据区

    Heap堆是被线程共享的一块内存区域,创建的对象和数组都保存在Java堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代VM采用分代收集算法,因此Java堆从GC的角度还可以细分为:新生代(Eden取、From Survivor区和To Survivor区)和老年代。

    Java堆heap内存主要用来存放运行过程中生成的对象,该区域OOM异常一般会有如下错误信息:
    java.lang.OutofMemoryError:Java heap space;
    通过设置-XX:-HeapDumpOnOutOfMemoryError,可以使其在OOM时,输出一个dump.core文件,记录当时的堆内存快照。
    然后我们就可以通过分析这个dump的内存快照来找到问题原因,比如说,是由于程序原因导致的内存泄露,还是由于没有估计好JVM内存的大小而导致的内存溢出。

    Java堆常用的JVM常数:

    • -Xms:初始堆大小,默认值为物理内存的1/64(<1GB),默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.
    • -Xmx:最大堆大小,默认值为物理内存的1/4(<1GB),默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制
    • -Xmn:年轻代大小(1.4or lator),此处的大小是(eden + 2 survivor space),与jmap -heap中显示的New gen是不同的。

    5、方法区(线程共享Thread shared)

    即我们常说的永久代(Permanent Generation),用于存储被 JVM 加载的类信息、常量(final)、静态变量(static)、即时编译器编译后的代码等数据

    HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的老年代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存,而不必为方法区开发专门的内存管理器(永久代的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。

    运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
    Java 虚拟机对 Class 文件的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。

    方法区主要存储被虚拟机加载的类信息,如类名、访问修饰符、常量池、字段描述、方法描述等。理论上在JVM启动后该区域大小应该比较稳定,但是目前很多框架,比如Spring和Hibernate等在运行过程中都会动态生成类,因此也存在OOM的风险。
    如果该区域OOM,错误结果会包含类似下面的信息:
    java.lang.OutofMemoryError: PermGen space

    相关的JVM参数可以参考运行时常量。

    • -XX:PermSize:设置永久代(perm gen)初始值,默认值为物理内存的1/64
    • -XX:MaxPermSize:设置永久代最大值,默认为物理内存的1/4

    上面说的是jdk1.8之前的内存模型,其中方法区和堆是是线程共享的,但是在jdk1.8之后元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存,同时类的元数据放入 native memory,运行时常量池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制,而由系统的实际可用空间来控制。

    image.png

    注:数组和对象是保存在堆中,类信息(类中字段、变量)、常量(final)、静态变量(static)是保存在方法区(永久代);1.8以后,类信息保存在元空间,常量(final)、静态变量(static)保存在堆中

    3、JVM运行时内存——堆heap

    Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。

    image.png

    新生代

    是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。

    • Eden 区
      Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老
      年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行
      一次垃圾回收。

    • ServivorFrom
      上一次 GC 的幸存者,作为这一次 GC 的被扫描者

    • ServivorTo
      保留了一次 MinorGC 过程中的幸存者。

    MinorGC(也可以称之为young gc) 的过程(复制->清空->互换)——MinorGC 采用复制算法。

    • 1:eden、servicorFrom 复制到 ServicorTo,年龄+1
      首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则复制到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 无法足够存储某个对象,则将这个对象存储到老年代),默认情况下年龄到达15的对象会被移到老生代中;

    • 2:清空 eden、servicorFrom
      然后,清空 Eden 和 ServicorFrom 中的对象;

    • 3:ServicorTo 和 ServicorFrom 互换
      最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom区。

    优点:实现简单,内存效率高,不易产生碎片,每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集

    缺点:可用内存被压缩了,且存活对象增多的话,Copying 算法的效率会大大降低

    image.png

    老年代

    主要存放应用程序中生命周期长的内存对象。
    老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发
    当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

    MajorGC

    标记清除算法(Mark-Sweep):首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。
    image.png

    缺点:该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

    标记整理(Mark-Compact)算法:标记阶段和 Mark-Sweep 算法相同,但是标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象
    image.png

    永久代

    即上面说的线程共享Thread shared的方法区
    指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同(数组和对象是保存在堆中),GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

    在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory,字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用空间来控制。

    4、Minor GC VS Major GC vs Full GC

    Minor GC:清理年轻代的垃圾;
    Major GC:清理老年代的垃圾;
    Full GC:清理整个堆空间—包括年轻代和老年代;

    5、垃圾回收器

    CMS收集器(多线程标记清除算法)

    Concurrent mark sweep(CMS)收集器是一种老年代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法
    最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。

    CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:

      1. 初始标记
        只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。
      1. 并发标记
        进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。
      1. 重新标记
        为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
      1. 并发清除
        清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行。
    image.png

    CMS 出现FullGC的原因:
    1、年轻代晋升到老年代没有足够的连续空间,很有可能是内存碎片导致的,因此会触发FULL GC

    2、在并发过程中JVM觉得在并发过程结束之前堆就会满,需要提前触发FullGC

    G1收集器

    Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,是一款面向服务端应用的垃圾收集器,目标是替换掉CMS收集器

    相比与 CMS 收集器,G1 收集器两个最突出的改进是:

    1. 基于标记-整理算法,不产生内存碎片。
    2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

    与其它收集器相比,G1变化较大的是它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留了新生代和来年代的概念,但新生代和老年代不再是物理隔离的了它们都是一部分Region(不需要连续)的集合。
    G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率

    G1收集器的运作大致可划分为以下几个步骤:

    • 1、初始标记(Initial Making)
    • 2、并发标记(Concurrent Marking)
      3、最终标记(Final Marking)
      4、筛选回收(Live Data Counting and Evacuation)

    看上去跟CMS收集器的运作过程有几分相似,不过确实也这样。初始阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以用的Region中创建新对象,这个阶段需要停顿线程,但耗时很短。并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这一阶段耗时较长但能与用户线程并发运行。而最终标记阶段需要吧Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但可并行执行。最后筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这一过程同样是需要停顿线程的,但Sun公司透露这个阶段其实也可以做到并发,但考虑到停顿线程将大幅度提高收集效率,所以选择停顿。

    image.png

    6、类加载过程

    在代码编译后,就会生成JVM(Java虚拟机)能够识别的二进制字节流文件(*.class)。而JVM把Class文件中的类描述数据从文件加载到内存,并对数据进行校验、准备、解析、初始化,使这些数据最终成为可以被JVM直接使用的Java类型,这个说来简单但实际复杂的过程叫做JVM的类加载机制。

    类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括7个阶段,加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading),其中验证其中验证、准备、解析三个阶段统称为连接。

    image.png

    注:加载、验证、准备、初始化、卸载这五个阶段顺序是一定的,而解析阶段在某些情况下可以在初始化之后再开始。

    6.1 加载阶段:

    在这个阶段,JVM主要完成三件事:

    • 1、通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。而获取的方式,可以通过jar包、war包、网络中获取、JSP文件生成等方式。

    • 2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。这里只是转化了数据结构,并未合并数据。(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域)

    • 3、在内存中生成一个代表这个类的java.lang.Class对象,作用来访问方法区这些数据。这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中。

    对于数组而言,加载情况有所不同,数组类本身不通过类加载器创建,是由 JVM 直接创建的。但是数组中的元素还是要靠类加载器去创建,如果数组去掉一个维度后是引用类型,就采用类加载器去加载,否则就交给启动类加载器去加载。

    另外加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

    6.2 连接阶段:

    类的加载过程后生成了类的java.lang.Class对象,接着会进入连接阶段,连接阶段负责将类的二进制数据合并入JRE(Java运行时环境)中。类的连接大致分三个阶段。

    • 第一步,验证:验证被加载后的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机安全。

    • 第二步,准备:是为类的静态变量(常量除外)在方法区分配内存并设置默认值(如static int a=123 此时a值为0,在初始化阶段才会变成123),这些内存都将在方法区中进行分配。
      这一阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
      另外,静态常量(static final filed)会在准备阶段赋程序设定的初值,如static final int a = 666; 静态常量a就会在准备阶段被直接赋值为666,对于静态变量,这个操作是在初始化阶段进行的。

    • 第三步,将类的二进制数据中的符号引用换为直接引用。
    6.3 初始化阶段:

    类初始化是类加载的最后一步,除了加载阶段,用户可以通过自定义的类加载器参与,其他阶段都完全由虚拟机主导和控制。到了初始化阶段才真正执行Java代码。

    初始化的过程包括执行类构造器方法,static变量赋值语句,static{}代码块,如果是一个子类进行初始化会先对其父类进行初始化,保证其父类在子类之前进行初始化;所以其实在java中初始化一个类,那么必然是先初始化java.lang.Object,因为所有的java类都继承自java.lang.Object。

    如static int a = 100;在准备阶段,a被赋默认值0,在初始化阶段就会被赋值为100。

    首先什么情况下类会初始化?

    Java虚拟机规范中严格规定了有且只有五种情况必须对类进行初始化:

    • 1、使用new字节码指令创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(放入常量池中的常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。

    • 2、通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。

    • 3、当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化。

    • 4、当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。

    • 5、使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
      接口的加载过程与类的加载过程稍有不同,接口中不能使用static{}快。当一个接口在初始化时,并不要求其父接口全部都完成初始化,只有在真正用到父接口时(如引用接口中定义的变量)才会初始化。

    注意,虚拟机规范使用了“有且只有”这个词描述,这五种情况被称为“主动引用”,除了这五种情况,所有其他的类引用方式都不会触发类初始化,被称为“被动引用”。

    6.4 总结

    前面讲了这么多,那还是有一个疑问,一个类是啥时候开始加载的呢?

    其实,Java虚拟机规范中并没有进行强制约束,这点虚拟机根据自身实现来把握。但对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行初始化(加载,验证,准备肯定要在此之前进行了),这5种情况我们上面有过介绍。

    7、类加载器

    类加载器实现的功能是即为加载阶段获取二进制字节流的时候。

    JVM提供了以下3种系统的类加载器:

    • 启动类加载器(Bootstrap ClassLoader):最顶层的类加载器,负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。

    • 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。

    • 应用程序类加载器(Application ClassLoader):也叫做系统类加载器,可以通过getSystemClassLoader()获取,负责加载用户路径(classpath)上的类库。如果没有自定义类加载器,一般这个就是默认的类加载器。

    类加载器之间的层次关系如下:

    image.png

    类加载器之间的这种层次关系叫做双亲委派模型。
    双亲委派模型要求除了顶层的启动类加载器(Bootstrap ClassLoader)外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不是以继承关系实现的,而是用组合实现的。

    双亲委派模型的工作过程:
    如果一个类接受到类加载请求,他自己不会去加载这个请求,而是将这个类加载请求委派给父类加载器,这样一层一层传送,直到到达启动类加载器(Bootstrap ClassLoader)。
    只有当父类加载器无法加载这个请求时,子加载器才会尝试自己去加载。

    双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题。即越基础的类由越上层的加载器进行加载。

    破坏双亲委派模型:
    若加载的基础类中需要回调用户代码,而这时顶层的类加载器无法识别这些用户代码,怎么办呢?这时就需要破坏双亲委派模型了。

    Spring破坏双亲委派模型
    Spring要对用户程序进行组织和管理,而用户程序一般放在WEB-INF目录下,由WebAppClassLoader类加载器加载,而Spring由Common类加载器或Shared类加载器加载。
    那么Spring是如何访问WEB-INF下的用户程序呢?
    使用线程上下文类加载器。 Spring加载类所用的classLoader都是通过Thread.currentThread().getContextClassLoader()获取的。当线程创建时会默认创建一个AppClassLoader类加载器(对应Tomcat中的WebAppclassLoader类加载器): setContextClassLoader(AppClassLoader)。
    利用这个来加载用户程序。即任何一个线程都可通过getContextClassLoader()获取到WebAppclassLoader。

    8、tomcat类加载架构

    image.png

    Tomcat目录下有4组目录:

    /common目录下:类库可以被Tomcat和Web应用程序共同使用;由 Common ClassLoader类加载器加载目录下的类库;
    /server目录:类库只能被Tomcat可见;由 Catalina ClassLoader类加载器加载目录下的类库;
    /shared目录:类库对所有Web应用程序可见,但对Tomcat不可见;由 Shared ClassLoader类加载器加载目录下的类库;
    /WebApp/WEB-INF目录:仅仅对当前web应用程序可见。由 WebApp ClassLoader类加载器加载目录下的类库;
    每一个JSP文件对应一个JSP类加载器。

    9、总结

    我们前面写了JVM内存管理,OOM,垃圾回收,类加载,下面我们通过一个对象的生命周期来把这些知识点串联起来。

    对象的生命周期可以从类加载开始算起,但是JVM并没有严格规定类加载开始的时机,我们这里以new 一个对象开始。

    Java在new一个对象的时候,会先查看对象所属的类有没有被加载到内存,如果没有的话,就会先通过类的全限定名来加载。

    加载并初始化类完成后,再进行对象的创建工作。

    我们先假设是第一次使用该类,这样的话new一个对象就可以分为两个过程:加载并初始化类和创建对象。

    https://mp.weixin.qq.com/s/QXDINKJ_5PfUgvDRREctwQ

    9.1 类加载
      1. 加载阶段
        由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例。
      1. 连接阶段:将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时内存中。
      • 验证:验证是否符合class文件规范
      • 准备:为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)
        被final修饰的static变量(常量),会直接赋值。
      • 解析:将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。
    • 3.初始化阶段

      • 为静态变量赋值
      • 执行static代码块

    最终,方法区会存储当前类类信息,包括类的静态变量、类初始化代码(定义静态变量时的赋值语句 和 静态初始化代码块)、实例变量定义、实例初始化代码(定义实例变量时的赋值语句实例代码块和构造方法)和实例方法,还有父类的类信息引用。

    7.2 创建对象

    1、在堆区分配对象需要的内存
    分配的内存包括本类和父类的所有实例变量,但不包括任何静态变量。
    2、对所有实例变量赋默认值
    将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值。
    3、执行实例初始化代码
    初始化顺序是先初始化父类再初始化子类,初始化时先执行实例代码块然后是构造方法。
    4、如果有类似于Child c = new Child()形式的c引用的话,在栈区定义Child类型引用变量c,然后将堆区对象的地址赋值给它

    需要注意的是,每个子类对象持有父类对象的引用,可在内部通过super关键字来调用父类对象,但在外部不可访问。

    7.3 垃圾回收

    触发GC运行的条件要分新生代和老年代的情况来进行讨论,有以下几点会触发GC:

    • 当Eden区和From Survivor区满时;
    • 老年代空间不足
    • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
    • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
    7.4 OOM

    内存溢出:当申请的内存超出了JVM能提供的内存大小,此时称之为溢出。
    从上面内存模型中我们可以总结出最常见的OOM情况:

    • java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。

    • java.lang.OutOfMemoryError: PermGen space ------>java永久代溢出,即方法区溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。

    • java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。

    4.2 如何排查

    JVM Heap dump和Thread dump

    相关文章

      网友评论

        本文标题:JVM内存结构、运行时内存以及类加载过程

        本文链接:https://www.haomeiwen.com/subject/cuosgctx.html