jvm内存模型

简介

运行时数据区整体结构如下:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • 方法区(永久代/metaspace)




CPU基础知识

CPU缓存

  • 一级缓存:cup内核有独立一级缓存
  • 二级缓存:cup内核有独立二级缓存
  • 三级缓存:cup内核共享三级缓存,会存在一致性问题

缓存一致性问题解决

  • 总线锁:cpu取值时对总线上锁
  • 缓存一致性协议MESI协议:modify/exclusive/shared/invalid,简单来讲cpu会对读写的数据进行四种状态的标记,不同的状态处理的方式不同。(无法被缓存的数据或者夸缓存行的数据MESI无法解决,还是要靠总线锁)

缓存行

  • 目的:读区数据时进行缓存行cache line整行读区(主流一行64字节),提高效率。
  • 伪共享问题:同一缓存行内的不同数据被不同cpu使用锁定,当数据被改动后,整个缓存行会被告知修改,不同cpu重新读区,造成互相影响
  • 缓存行对齐提效:利用缓存行64字节以及整行读写的特点,利用“padding+真实数据 = 64字节”的方式提升真实数据对象的读写操作

乱序问题

  • 并行执行:当两条指令没有依赖关系,cpu会并行执行
  • 合并写:写操作有可能会放在一起合并操作
  • 指令重排序:两条互相不影响的指令有可能被重新排序执行

有序性保证

  • cpu内存屏障
    • sfence:写屏障,屏障前后的写操作不能重排
    • lfence:读屏障,屏障前后的读操作不能重排
    • mfence:读写屏障,屏障前后的读写操作不能重排
  • lock指令

并行与并发

  • 串行:以垃圾回收为例,串行回收器只有一个垃圾回收线程,执行时用户线程无法执行
  • 并行:以垃圾回收为例,并行回收器可以有多个垃圾回收线程,执行时用户线程无法执行
  • 并发:以垃圾回收为例,并发回收器可以有多个垃圾回收线程,执行时用户线程可以同时执行。例如采用时间片轮训的方式




线程共享

  • JVM启动(程序启动)时被创建,是jvm中最重要、最大的一块内存空间
  • 可以物理上不连续但逻辑是连续
  • 所有线程共享,但可以对不同线程划分私有缓存区TLAB
  • 存放对象、数组、class对象信息等
  • -Xms:设置堆初始内存 memory star,-Xms600m,默认是物理内存1/64
  • -Xmx:设置堆最大内存 memory max,-Xmx600m,默认是物理内存1/4
  • 建议初始和最大设置一样,避免堆内存动态变化导致的资源浪费

堆的内存结构

  • 年轻代
    • 组成方式:Eden+survivor0+survivor1
    • 默认比例:8:1:1,实际上却是6:1:1因为jvm对这里有个自适应的设置(可以关闭)
    • 设置比例:-XX:SurvivorRatio=8,即设置8:1:1
    • 特殊设置:-Xmn设置年轻代的大小(用的很少)
  • 年老代
    • -XX:NewRatio=2设置老年代/新生代的比值
  • 永久代/方法区/元数据区:java8及以后已经不属于堆

新生代对象分配与回收过程

  1. 新生代对象在eden区创建(YongGC只有在eden满时才触发,s区不会触发)
  2. 当第一次ygc时eden存活对象放在s0(放不下时直接放入老年代)
  3. 当第二次ygc时eden与s0进行回收,存活对象放入s1(放不下时直接放入老年代)
  4. 当第三次ygc时eden与s1进行回收,存活对象放入s0(放不下时直接放入老年代)
  5. 反复执行上面过程,当对象age年龄计数器达到15(这个数值可以修改)会将对象放入老年代
  6. 如果新生对象过大,将直接放入老年代
  • 老年代满了会触发fullGC
  • 老年代fullGC放不下了会触发oom

GC概念简单说明

分代的目的是为了提高gc的性能,研究表明绝大多数对象都是朝生夕死的,如果不分代回收,那么性能会极大下降。

  1. 部分回收
    1. Minor gc/yang gc:eden区满了针对新生代进行回收。会造成stw,但是速度很快
    2. Major gc/old gc:老年代满了针对老年代进行回收,目前之后cms才会对老年代进行单独回收。会造成stw,速度很长
    3. Mix gc:回收整个新生代及部分老年代,目前只有g1才会有这种行为
  2. 整堆回收
    1. Full gc:整个堆和方法区回收

TLAB(thread local allocation buffer)

线程本地分配缓冲区。由于堆空间是多线程共享的,那么多线程在分配内存的时候会存在同步问题,需要加锁进行解决,但是这样会降低性能,因此出现了TLAB。目的是单独为每一个线程在eden区分配单独的内存空间。

当TLAB空间不够时jvm会进行加锁处理。

  • -XX:UseTLAB可以设置是否开启TLAB,默认开启
  • -XX:TLABWasteTargetPercent可以设置TLAB大小,默认是eden的1%

常见问题

  • 堆是对象存储的唯一选择吗?不是
    • 逃逸分析后有可能将对象直接在栈上分配
    • TaobaoVM会将生命周期长的对象直接分配到堆外
  • 什么是逃逸分析?
    • 如果方法中定义的对象没有被外部使用则说明没有逃逸,否则发生逃逸。
  • 编译器的代码优化方式有哪些?
    • 栈上分配:经过逃逸分析后未发生逃逸的对象直接分配到栈上
    • 同步省略:即锁消除,如果一个对象被发现只能被一个线程访问到,则会省略同步(即使增加了锁)
    • 标量替换:进过逃逸分析后发现对象未发生逃逸,那么对象可能被分解为其组成属性直接存放在栈上,无需创建对象。以属性标量来代替对象的作用。

方法区

  • (以hotspot为例)也称为永久代,JDK8后称为元空间Metaspace
  • 多线程共享、可以是不连续的空间、关闭jvm会被释放
  • 方法区的大小决定可以加载多少类
  • jdk7之前大小设置
    • -XX:PermSize=200m设置初始值,默认20.75m
    • -XX:MaxPermSize=200m设置最大值,默认82m
  • jdk8及之后大小设置
    • -XX:MetaspaceSize=100m设置初始值,又称为高水位线,一旦触及这个水位线就会触发full gc并重新设置水位线,因此在不超过最大值的情况下可以调高这个高水位线
    • -XX:MaxMetaspaceSize=100m设置最大值,如果不设置,默认最大值就是系统内存

演进过程

  1. jdk7之前称为永久代(使用虚拟机设置的内存),jdk8后被元空间替代(属于本地内存)
  2. 只有hotspot才存在方法区,其它虚拟机不一定有
  3. Jdk1.6存在永久代并在永久代存放静态变量
  4. jdk1.7有永久代但逐步弱化,静态变量和字符串常量池存放在堆中
  5. jdk1.8无永久代但逐步弱化,静态变量和字符串常量池存放在堆中
  6. 为什么要用元空间代替永久代(为永久代设置合适的大小困难,对永久代调优困难)
  7. 为什么迁移字符串常量池?永久代只有在full gc时才会回收,效率低,在程序运行时会创建大量的字符串,容易导致永久代内存溢出
  8. 关于静态变量的说明(无论是什么jdk版本静态变量的对象都是放在堆中,只是引用在1.6前放在永久代)

方法区内部结构

  • 类型信息(即类class文件完整信息)
    • 完整类名
    • 父类完整类名
    • 类加载器信息
    • 修饰符
    • 直接接口的有序列表
    • 域信息(成员变量):名称、类型、修饰符
    • 方法信息:名称、返回值类型、参数、修饰符、字节码、操作数栈、局部变量表、异常表
  • 运行时常量池
    • 字节码文件中的常量池表中记录的字面量和符号引用会在类加载后放入运行时常量池
    • 与字节码常量池的区别是具备动态性
    • 字符串常量池(jdk1.7后放入堆中)
  • 静态变量(jdk1.7后放入堆中)
  • JIT代码缓存

方法区垃圾回收

虚拟机规范并未规定一定要回收,可以不回收。方法区回收条件很苛刻,主要涉及如下部分

  • 类型卸载
    • 必须所有实例已被回收
    • 必须类加载器已被回收
    • 必须类对应的class对象不存在引用
  • 常量池中废弃的常量
    • 不存在引用就可以卸载


线程私有

程序计数器

用来存储指向下一条指令的地址,由执行引擎读取下一条指令执行

  • 程序计数器存在的意义?cpu在不停的切换线程执行,因此需要记录下一条可以执行的指令
  • 为什么是线程私有的?如果不是私有会存在一致性问题

虚拟机栈

特点

  • 线程私有
  • 生命周期与线程一致
  • 不存在垃圾回收问题

可能出现的异常

栈大小可以是固定的也可以是动态的

  • Stackoverflowerror:当栈大小是固定的时候,有可能出现此问题
  • Outofmemoryerror:当栈大小是动态的时候,请求增加栈空间有可能报此问题

可以通过-Xss设置栈大小,如-Xss1m

栈的存储结构

栈的基本单位就是栈贞,java方法的执行就是栈贞入栈到出栈的过程,栈贞的具体结构如下

  • 局部变量表
  • 操作数栈
  • 动态链接
  • 方法返回地址
  • 一些附加信息

局部变量表

  • 局部变量表在编译期就确定下来
  • 定义为一个数字数组
  • 存放方法参数、方法中的局部变量,例如基本数据类型、对象饮用、returnaddress类型等
  • 基本存储单位是slot(槽)
  • 32位以内类型占一个slot,如byte\short\char\int\boolen
  • 64位占两个slot,如long\double
1
2
3
4
5
6
7
8
9
#### 变量的分类
- 按数据类型
- 基本数据类型
- 引用数据类型
- 按在类中声明的位置
- 成员变量:在使用前都经历过赋默认值的过程
- 类变量:链接阶段赋默认值
- 实例变量:随着对象的创建会在堆内存分配空间并赋默认值
- 局部变量:在使用前必须显式赋值,否则编译不通过

操作数栈

  • 结构:数组/链表,先入后出
  • 作用:在方法执行的过程中,根据字节码指令,往操作数栈中写入或读区指令。简单来说就是存放操作数、运算中间数据的容器
  • 特点:长度在编译时确定
  • 使用过程:例如1+2的操作,1和2分别压入操作数栈,通过add指令进行求和,结果根据istore_1指令存放入局部变量表的index为1的位置。

贞数据区

  1. 动态链接:栈贞中存储的一个内存地址,这个地址指向方法区中运行常量池记录的方法引用,即指向具体的方法。

    1. 每一个栈贞都记录本栈贞对应的运行常量池方法引用(动态链接)
    2. 栈贞中调用了其它方法时也会记录指向被调用方法的运行常量池方法引用(动态链接)
  2. 方法返回地址

    1. 保存调用该方法的pc寄存器的值

    2. 目的:为了指向方法执行结束后的下一条指令,即返回到该方法被调用的位置

    3. 方法的退出方式

      1. 正常退出:返回该方法被调用的位置
      2. 异常退出:通过异常表来确定返回地址
    4. 返回命令

      1
      2
      3
      4
      5
      6
      ireturn:返回值是boolen、byte、char、short、int
      Lreturn:long类型
      freturn:float类型
      dreturn:double类型
      areturn:引用类型
      return:void
  1. 一些附加信息:例如对程序调试提供支持的信息等,具体要看虚拟机的实现,不一定都有。

常见问题

  • 方法中定义的局部变量是不是线程安全的?具体问题具体分析
    • 如果涉及到将局部变量返回则有可能不安全
    • 如果是参数传递进来的有可能不安全

本地方法栈

  • 本地方法:一个java方法,但是实现不是由java实现而是其它语言,如c/c++
  • 为什么使用本地方法?与java环境外交互、与操作系统交互
  • 本地方法栈:管理本地方法的调用,其他与虚拟机栈相同



版权声明:本文为博主原创文章,欢迎转载,转载请注明作者、原文超链接,感谢各位看官!!!

本文出自:monkeyGeek

座右铭:生于忧患,死于安乐

欢迎志同道合的朋友一起交流、探讨!

monkeyGeek
# ,

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×