JVM 内存结构介绍整理

Jvm内存模型

  • JVM内存共分为虚拟机方法区程序计数器本地方法栈五个部分。
    JVM内存模型

虚拟机栈

  • 每个线程都有一个私有的栈,随着线程创建而创建。每个栈空间都存放着栈帧,每个方法都会创建一个栈帧,栈帧主要存放了局部变量列表(局部变量表主要存放了编译器可知的各种数据类型[boolean、byte、char、short、int、float、long、double]、对象引用[reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置])、操作数栈、方法出口等信息.

  • 一个线程中方法的调用链可能会很长,很多方法都同时处于执行状态。对于JVM执行引擎来说,在在活动线程中,只有位于JVM虚拟机栈栈顶的元素才是有效的,即称为当前栈帧,与这个栈帧相关连的方法称为当前方法,定义这个方法的类叫做当前类。

  • 栈中存放对象的引用
    栈中存放对象的引用

  • 栈帧模型
    栈帧模型

  • 执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了。

  • 调用新的方法时,新的栈帧也会随之创建。并且随着程序控制权转移到新方法,新的栈帧成为了当前栈帧。方法返回之际,原栈帧会返回方法的执行结果给之前的栈帧(返回给方法调用者),随后虚拟机将会丢弃此栈帧。

  • 当一个方法开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入内容和读取数据,即入栈和出栈操作。

  • 栈帧是线程本地的私有数据,不可能在一个栈帧中引用另外一个线程的栈帧。

  • 栈帧数过多及栈深度过大时超过虚拟机定义的所允许深度,就会抛出StackOverflowError异常(无限制申请线程),当虚拟机栈无法申请到足够的内存,就会抛出OutOfMemoryError异常

本地方法栈

  • 本地方法栈与虚拟机栈的使用原理基本一致,只不过虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机执行Native方法服务

Java堆

  • Java堆是被所有线程 共享的一片区域,该内存区域的唯一目的就是存放对象的实例
  • Java堆是垃圾收集器管理的主要区域,主要分为年轻代和年老代 == GC算法==
  • Java实例对象在创建时无法再分配堆空间时,就会抛出OutOfMemoryError异常
  • Java堆的大小分配是可以扩展的,启动参数-Xmx/-Xms可以控制大小
  • 线程共享的堆内存可能会 分出来多个线程私有的分配缓冲区==(TLAB,这是为了并发分配内存时的脏分配问题,需要使用相关参数来开启。虚拟机默认使用CAS加上失败重试机制解决脏分配问题)==

Java堆内存和栈内存 区别

  1. 当创建任何对象时,在堆中创建,堆内存是所有线程共享的,栈内存只是对当前被执行的线程可见
  2. 因为创建的对象是存储在堆中,栈空间存储着对他的引用,还有就是该线程执行方法中的局部变量,局部变量随着栈的销毁而销毁
  3. 两者的内存管理机制不同,栈内存遵循栈入和栈出原则,堆内存分年轻代和年老代管理,栈内存存储时间短暂,栈出及被销毁,对内存可能会常驻,依赖垃圾回收器分析处理
  4. 使用-Xms和-Xmx JVM选项来定义堆内存的启动大小和最大大小,使用-Xss来定义栈内存大小。
  5. 当栈内存已满时,运行时抛出,java.lang.StackOverFlowError,如果堆内存已满,则抛出java.lang.OutOfMemoryError:Java Heap Space错误。

方法区

  • 方法区与堆一样,是线程共享的,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
  • 方法区的内存很少触发垃圾收集,但并不是说数据存储到方法区就常驻存在,这区域的内存回收目标主要是针对常量池的回收和堆类型的卸载
  • 方法区的内存不足的时候回触发OutOfMemoryError异常。

运行常量池

  • Java编译的class文件中 除了有类的版本、字段、方法、接口等描述信息外,还存放着常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
  • 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定要在编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入到常量池中,这种特性被开发人员利用得比较多的是String类的intern()方法。
  • 当常量池无法再申请到内存时会抛出OutOfMeMoryError异常

并发编程下的Java内存模型

  • 线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化
    多线程下的读写模型
    A线程与B线程之间进行通信时,线程A首先把修改过的共享变量更新到堆内存中,线程B再去读取A更新过的共享变量,随之而来的是两个问题:
    1. 共享变量对各个线程的内存可见性
    2. 共享变量的竞争问题

内存可见性原则

当A线程对共享变量做出了修改,但是并没有来得及将修改flush到共享内存中时,B线程读取的共享变量并不是我们希望的预期值,这样就产生了线程竞争,因此我们需要确保A的写操作在任何时候都是对B线程可见的,这就引入了一个Happen-Before 规则

  • Java **volatile**会保证共享变量对内存的可见性

Happen-Before 规则

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作
  • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
  • 传递性:如果Ahappens- before B,且B happens- before C,那么A happens- before C。

线程竞争的避免

  • 线程锁:当前线程在进行数据操作的时候给该操作 添加锁(ReentrantLock),其他线程只能阻塞在该线程操作数据结束后解锁再进行操作
  • 线程同步: 每个java对象都有一个内部锁,使用**synchronized**关键字进行方法声明,这个方法在被一个线程操作时就会自动被上锁,其效果与创建Lock对象对数据操作加锁相似

引申

  1. Java程序的内存划分和垃圾回收算法:堆内存(Heap Space)和永久代(Permanent Generation->Permgen)
  2. 为什么wait,notify和notifyall定义在object中
    https://blog.csdn.net/weixin_41950473/article/details/91592261
  3. synchronized关键字的作用
    https://blog.csdn.net/weixin_41950473/article/details/90049998
  4. volatile是如何保证内存可见性和防止编译器指令性重排的?
  5. Java锁的类型、原理及其用法
  6. Jvm 内存OutOfMemoryError场景与分析
  7. CAS原理入门与扫盲
  8. Java基本数据类型的存放位置
    基本数据类型是放在栈中还是放在堆中,这取决于基本类型声明的位置。
    1. 方法中声明的变量为局部变量,局部变量存储在虚拟机栈空间的栈帧中,随着方法的被调用而创建,随着栈的销毁而销毁,如果基本类型是新声明的,存储在栈中;如果基本类型声明的是引用,该引用的基本类型实际存储在堆中,栈中只是存储对基本类型的引用
    2. 类中声明的变量即成员变量,是全局变量,这个是放在堆中的,无论是新声明的还是声明的是引用,基本数据类型都是存贮在堆中的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章