美文网首页
jvm-内存模型

jvm-内存模型

作者: 滴流乱转的小胖子 | 来源:发表于2021-03-23 07:00 被阅读0次

    一、学习jvm的目的

    jmm规范(java memory model),定义了多线程环境下,线程间的交互行为,保证正确的、同样的代码,在不同操作系统的jvm中执行结果是一致的

    二、JVM执行字节码的核心内存结构

    栈帧结构和load/store机制

    image image
    • 栈结构

    栈结构是线程栈帧中一部分,类似于CPU中ALU的功能,用于缓存运算需要的参数,一般字节码运行时候基于栈的后入先出的机制读取参数,参数读取完之后进行弹栈,指令运行完后,如果有运行结果,将结果存储于栈顶,后面的指令从栈顶读取前面指令的运行结果,对于栈操作分为load和store两种,load类指令将局部变量表中的数值加载到栈顶,store类指令将栈顶数据弹栈并根据槽位号存储到局部变量表的对应位置

    • 局部变量表

    线程栈帧的组成部分,类似于CPU中通用寄存器的功能,用于保存临时变量,每一个槽位都绑定一个局部变量,本质是内存中的一个数组,指令运行时候数据会在栈和局部变量表之间以load/store方式进行交互

    image

    https://img.haomeiwen.com/i22466533/9d90f8d19ab6509a.gif

    大致运行过程:

    程序计数器指向第0行,将整数1压入栈顶

    程序计数器指向第1行,将栈顶的1出栈并赋值给局部变量表的第0个变量,也就是a

    程序计数器指向第2行,将证书2压入栈顶

    程序计数器指向第3行,将栈顶的2出栈并赋值给局部变量表的第1个变量,也就是b

    程序计数器指向第4行,加载第0个变量并将其压入栈顶,也就是a

    程序计数器指向第5行,加载第1个变量并将其压入栈顶,也就是b

    程序计数器指向第6行,执行加法运算,将1 和 2 出栈,并将结果3压入栈顶。

    程序计数器指向第7行,将整数5压入栈顶

    程序计数器指向第8行,执行乘法运算,5和3出栈,将结果15压入栈顶。

    程序计数器指向第9行,将栈顶的15赋值给第2个变量,也就是c

    程序计数器指向第a行,return

    三、类加载器原理

    类加载器主要功能是通过一些列步骤将字节码文件加载到JVM内存中,供jvm使用

    3.1生命周期

    • 大方向分3阶段: 加载、链接、初始化
    细化分7个阶段:加载、链接(5步骤)、初始化 image

    1加载: 根据ClassPath等信息查找Class文件

    2验证:验证字节码文件的格式和依赖

    3准备:构造静态字段,方法表

    4解析:将符号解析为真正的引用

    5初始化:静态变量赋值,静态代码块执行

    6使用:具体线程中对类进行使用

    7卸载

    3.2类加载的场景和时机

    1. 程序入口执行静态的main方法时候,会触发main函数所在的类的加载
    2. 调用new创建类A对象时候,会触发加载类A
    3. 调用类A的静态方法时候,会触发加载类A
    4. 访问类A的静态数据字段时候,会触发加载类A
    5. 子类被加载时候会先触发其父类的加载
    6. 如果接口中实现了default方法,直接或者间接实现了该接口的类被加载时候会先触发该接口类的加载
    7. 用反射API对类进行操作时候,会触发该类的加载
    8. 初次调用MethodHandle时候,会触发该MethodHandle指向的方法所在的类的加载

    3.3类加载但不是初始化的场景

    1. 子类引用了父类的静态字段,会触发父类的初始化,但是不会触发子类本身的初始化
    2. 创建类A的对象数组(本质是构造引用数组), 类A会被加载,但是不会触发初始化
    3. 对常量的引用不会触发该常量所在的类的初始化,因为常量本质是存放在常量池的,其数值在编译器已经确定,不依赖于其所在类的初始化
    4. 通过类A.class方式引用A的Class对象,不会直接触发类A的初始化,除非用该Class对象实例化该类的对象或者访问了该类的静态字段或者方法
    5. Class.forName加载指定类时候,如果initialize参数传递false,不会触发该类的初始化
    6. 通过ClassLoader默认的loadClass方法加载类,不会触发类的初始化

    3.4类加载器的分类

    启动类加载器(BootstrapClassLoader)加载JVM依赖的最核心的系统类,例如rt.jar包中的类

    拓展类记载器(ExtClassLoader)加载拓展类路径下的类

    应用类加载器(AppClassLoader)加载应用开发者自己编写的类或者jar包

    https://blog.csdn.net/briblue/article/details/54973413

    3.5类加载器的运行原则

    • 类加载器的层级关系
    image
    • 类加载器具体实现类的继承关系
    image

    1 双亲委托 当前级别的类加载器加载类时会先查看其上一级加载器有没有加载过对应的类,如果已经加载了则直接使用类加载器的层级关系

    2 负责依赖 加载一个类时候,其依赖的所有类也必须被加载

    3 缓存加载 对于同一个类加载器而言,其已经加载的类只会被加载一次,第二次会从缓存中读取直接使用

    四、JVM内存模型

    image

    4.1线程与进程

    http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html

    操作系统的设计,因此可以归结为三点:

    一个进程可以包括多个线程。

    (1)以多进程形式,允许多个任务同时运行;

    (2)以多线程形式,允许单个任务分成不同的部分运行;

    (3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

    4.2JVM内存结构

    image
    • 线程中关于内存操作的原则:
    1. 每一个线程持有自己的栈帧,所有原生类型的局部变量都存储在每个线程自己的栈帧中,A线程的局部变量对于B线程不可见
    2. JVM进程的堆空间对所有该JVM中的线程可见,所有线程创建的对象全部都保存在堆空间中,每一个线程都使用引用方式对堆中的对象进行访问,每一个线程对于堆中对象的引用变量也是互相独立的
    3. 对象内部的字段不论是原生字段还是引用类型,均存储在堆中
    4. 类的静态字段保存于堆中
    5. 每个线程通过引用访问同一个对象的字段时候,会在线程自己的栈中先拷贝副本,如果有修改会在修改后回写到堆中,所以两个线程同时对一个对象进行的操作,各自有各自的缓存,互相对于对方的读写行为是不可见的

    4.3JVM内存整体结构

    image
    • JVM进程内存模型各组成部分:
    1. 栈Stack 用于给每个线程分配栈帧的内存空间
    2. 堆Heap 用于存放JVM进程中所有线程创建的对象,其中按照对象的生存周期状态分为新生代,老年代, 新分配的对象一般存活于Eden(伊甸区)区,当Eden区满了,执行YnagGC时候,会将Eden中存活对象放到S区中当前活动的区域,另个S区始终保持为空,同样S区中非存活对象也会被清理掉,清理后,Eden区全部清空,S区会减少一些生命周期结束的对象,多一些内存碎片,也会多一些从Eden生存下来的对象,

    如果发生了S区放不下生存对象的情况下,所有生存下来对象会统一搬移到另外一个原来为空的S区去,消除内存碎片,然后两个S区倒换,交换身份;经过一定次数GC后一直生存的对象会从新生代搬移到老年代中去

    1. 非堆Non-Heap 包含CCS, CodeCache等部分, JDK8之后永久代改为Metaspace,位置移动到了非堆空间中,用于保存常量和方法的代码段等不变的信息

    直接内存

    又称堆外内存,也就是说这不是jvm运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但这部分也会被频繁的使用,而且也可能导致OOM。

    堆外内存有什么优点呢?

    1 减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作

    2 可以提高性能,避免java堆和native堆(直接内存)来回复制数据。

    使用场景

    1.在JDK1.4之后加入了NIO,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native库函数直接分配堆外内存,然后通过DirectByteBuffer对象作为这块内存的引用来进行操作,jvm会自动对这部分的堆外内存进行回收。

    2.使用jdk内部未对外公开的unsafe来直接使用堆外内存,但不会被JVM回收

    4.4 jvm栈内结构

    image

    栈帧是一个逻辑上的概念,具体的大小在 一个方法编写完成后基本上就能确定。 比如返回值 需要有一个空间存放吧,每个 局部变量都需要对应的地址空间,此外还 有给指令使用的 操作数栈,以及 class 指 针(标识这个栈帧对应的是哪个类的方法, 指向非堆里面的 Class 对象)

    • 小结:

    伊甸区满了,通过youngGC写到 s0/s1中,时间久了 s0/s1中出现碎片化,内存空间不连续,就会重新将存活的写到另一个区域中

    新生代逐渐满了,就将存活的对象晋升到老年代。如果一对象特别大,创建时就在老年代创建

    新生代: 新创建的对象,存活周期短的对象

    老年代:特别大的对象,已经存活一段时间的对象

    非堆中元数据区:java8才改名,之前叫持久代,xmx xms 管不了,占内存比较大

    xmx 不超过总内存的 70%(一般60% 足以) ,4G 2.8G 8G6G

    和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。

    堆外内存,又被称为直接内存。这部分内存不是由jvm管理和回收的。需要我们手动的回收。

    五、JVM启动参数

    5.1分类

    按照形式分类

    • -开头标准参数,所有JVM都要实现这些参数,并且向后兼容

    • -D系统属性 --- 系统环境变量

    • -X 开头非标准参数,基本都是传给JVM的,默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容,可以使用java -X 命令查看当前JVM支持的非标准参数

    • -XX 开头的非稳定参数, + 开 -关 ,专门用于控制 JVM 的行为,跟具体的 JVM 实现有关,随时可能会在 下个版本取消。 -XX:+-Flags 形式, +- 是对布尔值进行开关。 -XX:key=value 形式, 指定某个选项的值。

    5.2常见启动参数与内存区域关系

    Xss Xmn Xms Xmx Meta DirectMemory

    java进程内存 = 栈 + 堆 + 非堆(java8之前叫持久代) + JVM自身 + 堆外内存(直接内存)

    xmx 不超过总内存的 70%(一般60% 足以) ,4G 2.8G 8G6G

    image image

    https://blog.csdn.net/yrwan95/article/details/82826519

    相关文章

      网友评论

          本文标题:jvm-内存模型

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