首页 > 技术文章 > 四种视角看JVM内存模型

xxdmn519 2019-09-27 20:55 原文

最近刚好在看《深入理解JAVA虚拟机》做下学习笔记,和以前自己看到的总结整合记录下。

1.JVM运行视角

  • 程序计数器
  • Java虚拟机栈
  • 本地方法栈
  • Java堆
  • 方法区

HotSpot JVM架构 

        1 .程序计数器

        程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的行号指示器。这个计数器记录的是正在执行的虚拟机字节码指令的地址。此内存区域是唯一一个在JAVA虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

        2.Java虚拟机栈

        与程序计数器一样,Java虚拟机栈也是线程私有的。Java虚拟机栈是描述Java方法运行过程的内存模型。Java虚拟机栈会为每个方法在执行的同时都会创建一个栈帧用于存储局部变量(存放基本数据类型变量,引用类型的变量,返回类型的变量),操作数栈,动态链接,方法出口信息。

        3.本地方法栈

        本地方法栈与虚拟机所发挥的作用是非常相似的(HotSpot虚拟机中,直接就把本地方法栈和虚拟机栈合二为一),它们的区别不过是虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。本地方法被执行的时候,在本地方法栈也会创建一个帧栈,用于存放该本地方法的局部变量表、操作数栈,动作链接,出口信息。方法执行完毕后相应的栈帧也会出栈并释放内存空间。

       4.Java堆

       堆是用来存放内存对象的,是Java虚拟机所管理的内存中最大的一块。所有的对象实例以及数组都要在堆上进行分配。

      5.方法区

       方法区育Java堆一样,是一个线程共享的内存区域。它用于存储已被虚拟机加载的类信息,常量,静态变量、即时编译器后的代码等数据。虽然Java虚拟机规范把方法区描述为难的一个逻辑部分,但是它却有个别名叫做Non-Heap(非堆),目的是为了和Java堆区分开来。

      6.直接内存

      直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用。在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道和缓冲的IO方式。它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复复制数据。直接内存的大小不受Java虚拟机控制,但是当本机物理内存不足时就会抛出OutOfMemoryErrot错误。

线程共享与独有的数据区

 

图二 线程共享与独有的数据区

思考问题:为什么局部变量是线程安全的?答案见文末最后

2.JVM内存功能视角

从JVM内存可以分为三部分

  • Heap区(堆内存)
  • 非Heap(非堆内存)
  • 其他区

图三JVM内存功能分区

Heap区:Eden Space(伊甸园),Survivor Space(幸存者区),Tenured Gen(老年代-养老区)

非Heap区:Code Cache(代码缓冲区),Perm Gen(永久代),Java虚拟机栈,本地方法栈。

其他区:直接内存

3.线程运行视角

图片四线程,工作内存和主内存之间的关系

Java内存模型规定了所有的变量都要存储在主内存中,但是每个线程都有自己的工作内存,线程的工作内存保存了该线程使用的变量。这些变量实际上是主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不是直接读取主内存的变量。】

思考问题:volatile变量?答案见最后

4.垃圾回收视角

JVM的垃圾回收主要针对的是堆内存。在垃圾回收过程,堆内存有以下特点:

  • 堆内存划分为新生代和老年代两部分,新生代主要用于存放新创建的对象与存活时长小的对象,老年代则用来存放存活时间长的对象
  • 新生代又进一步划分为E,S1,S2三个区,其中,E代表Eden区;S1,S2则代表两个类似的Survior。minorGC时候,Eden区不能被会回收的对象被放入到空的survior,Eden则肯定会被清空。另一个surivor里不能被GC回收的对象也会被放入这个surivor,始终保证一个surivor是空的
  • 一个对象被minorGC回收了N次没有被回收掉,则会被移除到老年区里(该次数通过设置-XX:InitialTenuringThresHold)
  • 当老年代的空间被耗尽了,则触发FullGC

 问题解答:

1.为什么局部变量是线程安全的?

JVM在执行Java程序时,会根据其数据用途把内存划分为若干数据区域,包括方法区,堆,栈(JVM,本地方法栈),程序计数器,其中前两者是所有线程共有的,后两者是每个线程独有的,因此,栈是线程私有的,一个线程一个栈,并且栈由栈帧组成,栈帧保存一个方法的局部变量表(包括参数和局部变量),操作数栈,常量池指针等,每一次方法的调用实际上是创建一个帧栈,并且压栈。

所以方法调用实际是帧栈在入栈和出栈的操作,因为栈是线程私有的,所以每个栈之间是独立的,所以帧栈对于多个线程栈来说不存在共享问题,也就不会存在线程安全的问题了

2.Volatile变量

根据JVM规范的规定,volatile变量依然有工作内存的拷贝,但是由于它特殊的操作顺序规定,所有看起来如同直接在主内存中读写。

如果对申明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在工作所在内存的数据写回到主内存,但是,就算写回到主内存,如果其他线程工作内存的值还是旧的,再执行计算操作就会有问题,所以,在多处理器下为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,,每个处理器通过嗅探在总栈上传播的数据来检查自己缓存的值是不是过期了。当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。

 

 

 

       

    

 

        

 

推荐阅读