JVM内存模型

作者: chuan_bai | 来源:发表于2020-03-11 22:42 被阅读0次

    一、jvm内存模型

    JVM内存模型.jpg

    下面说一下大概的一个流程:

    1. class文件会被类装载子系统装载。
    2. 装载到内存中也就是jvm运行时的数据区。
    3. 当我们运行一个方法的时候会创建一个对应的栈桢,
      栈桢包含了操作数栈、局部变量表、动态链接、方法出口等部分,每执行一个操作,都会在程序计数器中,记录程序下一步要执行的指针地址。
    4. 当方法1执行完后会从方法出口,进入到方法二的栈桢中。
    5. 当声明一个方法的时候,会存到到堆区域。
    6. 方法区是线程共享的里面存的是类的所有字段和方法的字节码如Math类
    7. 执行引擎读取运行时数据区的字节码逐个执行

    二、jvm内存结构

    • 本地方法栈(线程私有):本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码

    • 程序计数器(线程私有):就是一个指针,指向方法区中的方法字节码,可以认作为当前线程的行号指示器(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

    • 方法区(线程共享):类的所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中,虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来

    • 虚拟机栈(线程私有):栈的算法是先进后出的, Java线程执行方法的内存模型,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表,操作数栈,动态链接,方法出口等信息)。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程,不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致

    三、jvm堆结构

    首先介绍一下堆,堆是Java 虚拟机所管理的内存中最大的一块,并且是线程共享的,在虚拟机启动时创建,用于存放对象实例,几乎所有的对象(包括常量池)都在堆上分配内存,当对象无法在该空间申请到内存时就会抛出内存溢出异常,堆分为新生代(1/3堆空间)、老年代(2/3 堆空间)、元空间,元空间在就是jdk1.8以前的永久代,是方法区的实现,直接存在内存中,下面分别对堆的几个区域进行介绍

    • 新生代:
      分为两部分伊甸区和幸存者区,所有的类都是在伊甸区被new出来的,幸存区有两个0区和1区当伊甸区空间满了垃圾回收器会对伊甸区进行Minor GC,将伊甸区中不再被其他对象所引用的对象进行销毁,然后将伊甸园区的剩余对象移到幸存0区,若幸存0区也满了,会再对该去进行Minor GC,然后移到1区,新生代中的eden->from->to 每熬过一次Minor GC,年龄会加1,当它的年龄增加到一定程度(默认为15岁),就会晋升为老年代。

    • 老年代:
      新生代进行多次Minor GC仍然存活的对象会移动到老年区,若老年区也满了,会产生FullGC,进行老年区的内存清理。若老年区执行了Full GC之后依然无法进行对象保存,就会抛出内存溢出异常。
      -Xmx和-Xms参数表示最大堆和最小堆

    • 元数据区:
      元数据区就是jdk1.8以前的永久代,是对jvm规范中方法区的实现,使用的是本地物理内存,而1.8以前的永久代在虚拟机中,永久代在逻辑结构上属于堆,但是物理上不属于堆,堆大小=新生代+老年代,元数据区也可能发生内存溢出的异常。


      jvm堆空间.jpg

    四、类加载机制

    • 全盘负责委托机制
      当一个ClassLoader加载一个类时,除非显示的使用另一个ClassLoader,该类所依赖和引用的类也由这个ClassLoader载入
    • 双亲委派机制
      指先委托父类加载器寻找目标类,在找不到的情况下在自己的路径中查找并载入目标类
      优势:
      • 沙箱安全机制:自己写的String.class类不会被加载,这样可以防止核心API库被随意篡改
      • 避免类的被重复加载:当父亲已经加载了该类时,子加载器就没必要再加载一次

    五、GC相关

    5.1 垃圾收集算法

    1. 引用计数法
      给对象中添加一个引用计数器,没当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就认为可以被回收。(这个方法实现简单,效率高,但是目前主流虚拟机没有选择这个算法管理内存,主要原因是很难解决对象之间相互循环引用的问题。)
    2. 可达性分析算法
      以“GC Roots”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象时不可能用的
    3. finalize()方法最终判定对象是否存活
      即使在可达性分析算法中不可达的对象,需要经历再次标记过程才真正宣告一个对象死亡

    5.2 垃圾清除算法

    1. 标记清除算法
      算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,效率也很高,但是会带来两个明显的问题:

      • 效率问题(会遍历内存)
      • 空间问题(标记清除后会产生大量不连续的碎片)


        图片.png
    2. 复制算法
      为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。


      图片.png
    3. 标记整理算法
      为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。

    图片.png
    1. 分代收集算法
      当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

    比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

    • 新生代适合复制算法
    • 老年代适合标记清除算法和标记整理算法

    5.3 垃圾收集器

    • Serial收集器
      最早的收集器,根据名字就可以看出这是一款单线程收集器

    • ParNew收集器
      是Serial收集器的多线程版本

    • Parallel Scavenge收集器
      类似于ParNew收集器

    • CMS收集器
      是一款并发收集器

    • G1收集器
      G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征,可以设置吞吐量,GC会尽量满足设置的吞吐量

    六、JVM调优相关

    JVM调优指标

    • 停顿时间:垃圾收集器做垃圾回收中断应用执行的时间
    • 吞吐量: 垃圾收集的时间和总时间的占比:1/(1+n),吞吐量为1-1/(1+n)

    调优步骤

    • 打印GC日志
    • 分析日志得到关键性指标
    • 分析GC原因,调优JVM参数

    调优命令

    • jps:查看jvm进程
    • jstat:
      • jstat -gc 28485 垃圾回收统计
      • jstat -class 28485
    • jinfo:查看内存信息
      • jinfo -flags 28485 (jvm运行时的参数)
    • jmap:实例个数即占用内存大小
      • jmap -histo 28485 查看运行时内存相关信息
      • jmap -heap 28485 查看堆的概况
    • jstack:死锁排查
    • jvisualvm JDK自带的可视化工具

    相关文章

      网友评论

        本文标题:JVM内存模型

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