Java   发布时间:2022-04-12  发布网站:大佬教程  code.js-code.com
大佬教程收集整理的这篇文章主要介绍了JVM内存模型大佬教程大佬觉得挺不错的,现在分享给大家,也给大家做个参考。
今天我们来看JVM的核心部分:运行时数据区

JVM内存模型

 一.PC寄存器(程序计数器)

概念:就像相当于一个行号指示器
@H_944_20@
  • JVM中的PC寄存器是对物理PC寄存器的一种抽象模型,该区域非常小,几乎可以忽略不计
  • 每一个线程都有自己独有的PC寄存器
  • 用来存储下一条指令的地址,执行引擎每次读取指令时都先从PC寄存器得到下一条指令(字节码指令)的地址,然后找到指令进行执行
  • 是唯一的一个在JVM规范中没有任何内存溢出(OutOtMemoryError)情况的区域,同时也没有GC 

  • 作用:

    @H_944_20@
  • 程序执行到哪了?因为CPU需要不停的切换各个线程,这时候切换回来,就得知道接着从哪执行。
  • 下一步执行哪个指令?JVM的字节码解释器需要通过PC寄存器的值来明确下一条应该执行什么样的字节码文件

  • PC寄存器为什么被设置为线程私有?

    @H_944_20@
  • PC寄存器主要是为了在多线程下,CPU不停的做任务切换的时候,保证可以准确的执行所有的指令。
  • 如果在单线程下,所有指令都是按照顺序执行的,就不存在cpu做任务切换时导致指令混乱。而多线程就需要每个线程独立记录自己执行的情况,从而会出现互相干扰
  •  二.栈

    概念:
    @H_944_20@
  • 栈管运行,堆管存储 ,栈解决程序运行问题,即程序该如何执行,如何处理数据。 堆解决数据存储的问题,即数据往哪放,该怎么放。
  • 每一个线程都会创建一个虚拟机栈(独有),内部保存为一个一个栈帧,对应一个一个java方法,它保存方法的局部变量,部分结果,并参与方法的调用和返回。
  • 存在OOM,不存在GC

  • 栈帧的内部结构

    JVM内存模型

    @H_944_20@
  • 局部变量表:是一个数字数组,主要用来存储方法参数局部变量 @H_944_20@
  • slot(变量槽):在局部变量表中,每一个局部变量都放在一个slot中。 32位以内的类型只占一个slot,64位的类型占两个slot(比如double,long占两个slot)
  • JVM内存模型

    @H_944_20@
  • 操作数栈:主要用于保存计算过程的 中间结果/临时变量的存储空间
  • 方法返回地址:存放方法返回的 地址(pc寄存器的值)
    @H_944_20@
  • 方法返回时,会将调用者的pc寄存器的值作为返回地址,让方法执行完之,得到下一条指令的地址
  • 动态链接:指向运行时常量池的方法引用
    @H_944_20@
  • 在字节码文件中,所有的变量方法引用都作为"符号引用"保存在常量池中。
  • 比如:调用一个方法时,就是通过常量池中指向方法的符号引用来表示。
  • 那么,动态链接的作用就是把这些符号引用转换为调用方法的直接引用。
  • JVM内存模型

    @H_944_20@
  • 附加信息:例如:对程序调试提供支持的信息等...
  •  三.本地方法栈
    @H_944_20@
  • 虚拟机栈用于管理java相关的方法。本地方法栈用于管理本地(C/C++)相关的方法
  • 调用本地方法时使用关键字 native
  • 并不是所有的JVM都支持本地方法,因为jvm虚拟机规范并没有明确的要求本地方法栈。 只是当前的hotspot有本地方法栈。
  • JVM内存模型

    四.堆
    概述:
    @H_944_20@
  • 栈管运行,堆管存储。说白了,堆就是用来存放new出来的对象/数组
  • 一个进程就对应一个JVM的实例,一个JVM实例只存在一个堆(堆是共享的,但是也有私有的空间(TLAB))
  • 在JVM规范中规定,堆在物理内存中可以是不连续的,但在逻辑上应该视为连续的

  • 堆的内存结构

    JVM内存模型

    @H_944_20@
  •  java对象可以分为两类: @H_944_20@
  • 生命周期短的对象,这类对象创建和消亡都很快
  • 生命周期长的对象,在某些极端的情况下,能够与JVM的生命周期保持一致
  • 关于新生代中 Eden/Survivor0/Survivor1:然官方说默认为8:1:1,但是实际测试中却不是。需要手动设置
  • 几乎所有的对象都是在Eden区被new出来的
  • 80%对象的销毁都是在新生代中进行。(朝生夕死)

  • 为对象分配内存的过程: 为对象分配内存是一件非常复杂和严谨的任务。不仅要虑内存的分配,还得虑垃圾回收

    一般流程:

    1. 对象进来申请空间,先放在Eden中。(细节:每个线程今天自己的TLAB,放不下才直接进入Eden大空间)
    2. 当Eden满时,触发YGC/Minor GC 进行垃圾回收,刷新Eden区。有用的对象放在Survivor0中
    3. 在Survivor中给每个对象一个年龄值,当Eden满了,触发垃圾回收,会把S0也进行垃圾回收(YGC/Minor GC 回收两个区域)。 此时Eden和S0的所有有用的对象都会重新分配到S1中。SO清空,变为to
    4. 重复步骤3,直到对象的年龄值>15时,将S区中满足条件的对象Promotion到老年代
    5. 在养老区,相对休闲。当养老区满的时候,触发@H_757_1@major GC进行垃圾回收
    6. 如果清理完垃圾后,养老区还是满,就进行Full GC,还不行就报OOM

    注意:

    @H_944_20@
  • YGC是在Eden满的时候被触发,刷新Eden内的所有对象。同时YGC顺带把s0,s1的对象进行刷新
  • Survivor满的时候不会触发YGC,如果S区满了,Eden还没有触发YGC,那么S区会将一些特殊的对象直接放在养老区(特殊的对象下面说)
  • Promotion是晋升的意思,15就是临界值,也可以自己设置
  • S区口诀:复制之后有交换,谁空谁是to
  • 特殊:在特殊时候,就会满足内存分配策略

    @H_944_20@
  • 对象优先分配到Eden
  • 大对象直接分配到老年代
  • 长期存活的对象分配到老年代
  • 对象年龄动态判断: 如果S区有很多对象年龄相同(大于一半),S0,S1来回倒腾的太费事,就认为它们也是可以进入老年代的对象。 就不需要等到年龄>15才晋升
  • 空间分配担保(一种空间分配保障机制)
  • 关于大对象:

    @H_944_20@
  • 是需要连续的内存空间
  • 比如:一些大的数组,长的字符串等
  • 代码编写的过程中应该避免大对象,尤其是那些只用一次的大对象 因为更痛苦的是,不仅是大对象还是朝生夕死的。这样就减少了效率    
  • 关于空间分配担保:

    @H_944_20@
  • 在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间 @H_944_20@
  • 如果大于,此次Minor GC就是安全的
  • 如果小于,就会开启空间分配担保策略 @H_944_20@
  • 继续检查老年代最大的可用连续空间是否大于历次晋升到老年代的对象的平均大小 @H_944_20@
  • 如果大于,就尝试进行Minor GC,但是此次的Minor GC是有风险的
  • 如果小于,就不进行Minor GC,进行Full GC

  • 垃圾回收的原则:频繁收集新生代,很少收集老年代,几乎不动永久代(方法区) (JVM在进行垃圾回收时,不是每次都对"新生代","老年代","方法区"进行回收,绝大部分是对新生区进行回收)

    GC的分类:

    @H_944_20@
  • 部分收集: @H_944_20@
  • 新生代收集(@H_757_1@minor GC/YGC):只收集新生代
  • 老年代收集(@H_757_1@major GC/Old GC):只收集老年代 目前只有CMS GC会有单独收集老年代的行为
  • 混合收集(@H_757_1@mixed GC):收集整个新生代以及部分老年代 G1 GC会有这种行为
  • 整堆收集(Full GC):收集整个JVM堆和方法区的垃圾
  • 注意:

    @H_944_20@
  • 所有的GC都会引发STW,Minor GC时间最短,Major GC比Minor GC多十倍以上的时间
  • 一般出现了Major GC,都至少伴随一次Minor GC 。但也不是绝对, Parallel Scavenge的收集策略就有直接进行Major GC的选择
  • 程序在报OOM之前,会触发一次Full GC 。回收完空间任然不够,才报OOM
  • @H_924_21@major GC 和 Full GC都会在老年代满的时候触发,一定要区分是部分回收还是整堆回收

    Full GC触发机制:

    @H_944_20@
  • 调用System.gc()时,系统建议执行Full GC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • 由Eden区复制时,s0区向s1区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

  • 什么需要把堆进行分代?不分代就不能正常工作了吗?

    @H_944_20@
  • 不同对象的生命周期不同。经过研究,java中绝大部分的对象都是临时对象。只有少部分是长期对象 所以分代管理,会让堆中的数据更加高效。
  • 不分代也是完全可以的,分代的唯一理由就是优化GC