美文网首页
Java内存模型

Java内存模型

作者: preDatar | 来源:发表于2020-10-23 13:59 被阅读0次
  • Java内存模型(Java Memory Model ,JMM)

    Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。简要言之,jmm是jvm的一种规范,定义了jvm的内存模型。它屏蔽了各种硬件和操作系统的访问差异,不像c那样直接访问硬件内存,相对安全很多,它的主要目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。


    jvm运行时数据区.png

    上图可以展现并扩展出以下几点:

    1、运行时数据区分为运行时公有数据区和运行时私有数据区两部分。

    1.1、运行时公有数据区

    • 所有jvm线程共享

    • 所有为类实例和数组分配的内存都来自于它

    • 同时它也是GC所管理的主要区域,因此常被称为GC堆

    • 包含字符串常量池

      字符串常量池:字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串。在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身)

      public final class String
          implements java.io.Serializable, Comparable<String>, CharSequence {
          private final char value[];
          //String对象实际存放是值是在value数组中
      
      String s1 = new String("hello");
      //个人理解是首先会把"hello"转成char数组存放在堆中,是s1引用指向的对象包含了这个char数据的引用,同时会在常量池中也会生成hello,这个hello存放的是对这个char数组的引用
      String s2 = s1.intern();
      //返回字符串常量池中hello的引用
      String s3 = "hello";
      //如果"hello"已经在字符串常量池中存在,s3获取的是该引用,否则则创建。
      System.out.println(s1 == s3);//false
      //debug可以看到虽然value指向是用一个char数组,但本身对象不是同一个
      System.out.println(s2 == s3);//true
      

      贴上java8 String.intern()方法解释。

      /**
       * Returns a canonical representation for the string object.
       * <p>
       * A pool of strings, initially empty, is maintained privately by the
       * class {@code String}.
       //字符串池最初是空的,由String类私有地维护
       * <p>
       * When the intern method is invoked, if the pool already contains a
       * string equal to this {@code String} object as determined by
       * the {@link #equals(Object)} method, then the string from the pool is
       * returned. Otherwise, this {@code String} object is added to the
       * pool and a reference to this {@code String} object is returned.
       // 在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串对象相等的字符串,则返回池中的字符串。
            否则,该字符串对象将被添加到池中,并返回对该字符串对象的引用
       * <p>
       * It follows that for any two strings {@code s} and {@code t},
       * {@code s.intern() == t.intern()} is {@code true}
       * if and only if {@code s.equals(t)} is {@code true}.
       // 可以得出,对于任意两个字符串s和t,当且仅当s.equals(t)=true时,(s.intern() == t.intern())=true
       * <p>
       * All literal strings and string-valued constant expressions are
       * interned. String literals are defined in section 3.10.5 of the
       * <cite>The Java&trade; Language Specification</cite>.
       *
       * @return  a string that has the same contents as this string, but is
       *          guaranteed to be from a pool of unique strings.
       */
      public native String intern();
      

      在1.7之前,字符串常量池是在PermGen区域,这个区域的大小是固定的,不能在运行时根据需要扩大,也不能被垃圾收集器回收,因此如果程序中有太多的字符串调用了intern方法的话,就可能造成OOM。

      在1.7以后,字符串常量池移到了堆内存中,并且可以被垃圾收集器回收,这个改动降低了字符串常量池OOM的风险。

    方法区

    • 所有jvm线程共享
    • 存储类的结构。像运行时常量池,字段和方法数据,方法和构造函数的代码,还有特殊的方法用于类和实例的初始化,以及接口的初始化
    • 运行时常量池: 类或接口的字节码文件里的常量池的运行时表示形式。如在编译时就已经知道的数字字面量值,和必须在运行时解析的方法和字段的引用,运行时常量池的功能类似于传统语言的符号表,不过它包含的数据会更加宽泛。运行时常量池分配在jvm的方法区,类或接口的运行时常量池在类或接口被jvm创建时才会构建。

    1.2、运行时私有数据区

    程序计数器

    1. 什么是程序计数器

      程序计数器是当前线程正在执行的字节码的地址。程序计数器是线程隔离的,每一个线程在工作的时候都有一个独立的计数器。

    2. 字节码执行原理
      从字节码的执行原理来看,单线程的情况下程序计数器是可有可无的。因为即使没有程序计数器的情况下,程序会按照指令顺序执行下去,即使遇到了分支跳转这样的流程也会按照跳转到指定的指令处继续顺序执行下去,是完全能够保证执行顺序的。但是现实中程序往往是多线程协作完成任务的。JVM的多线程是通过CPU时间片轮转来实现的,某个线程在执行的过程中可能会因为时间片耗尽而挂起。当它再次获取时间片时,需要从挂起的地方继续执行。在JVM中,通过程序计数器来记录程序的字节码执行位置。程序计数器具有线程隔离性,每个线程拥有自己的程序计数器。

    3. 程序计数器的作用

      从字节码的执行原理来看,单线程的情况下程序计数器是可有可无的。因为即使没有程序计数器的情况下,程序会按照指令顺序执行下去,即使遇到了分支跳转这样的流程也会按照跳转到指定的指令处继续顺序执行下去,是完全能够保证执行顺序的。

      但是现实中程序往往是多线程协作完成任务的。JVM的多线程是通过CPU时间片轮转来实现的,某个线程在执行的过程中可能会因为时间片耗尽而挂起。当它再次获取时间片时,需要从挂起的地方继续执行。在JVM中,通过程序计数器来记录程序的字节码执行位置。

    4. 程序计数器的特点

      (1)程序计数器具有线程隔离性

      (2)程序计数器占用的内存空间非常小,可以忽略不计

      (3)程序计数器是java虚拟机规范中唯一一个没有规定任何OutofMemeryError的区域

      (4)程序执行的时候,程序计数器是有值的,其记录的是程序正在执行的字节码的地址

      (5)执行native本地方法时,程序计数器的值为空。原因是native方法是java通过jni调用本地C/C++库来实现,非java字节码实现,所以无法统计

    虚拟机栈

    栈中存储的是栈帧。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程

    当调用本地方法时,虚拟机会保持 Java 栈不变,不会在 Java 栈祯中压入新的祯,虚拟机只是简单地动态连接并直接调用指定的本地方法。

    栈帧包含了局部变量表、操作数栈、动态链接、方法返回地址、附加信息。

    • 局部变量表。一片连续的内存空间,用来存放方法参数,以及方法内定义的局部变量,存放着编译期间已知的数据类型(八大基本类型和对象引用(reference类型),returnAddress类型。它的最小的局部变量表空间单位为Slot,虚拟机没有指明Slot的大小,但在jvm中,long和double类型数据明确规定为64位,这两个类型占2个Slot,其它基本类型固定占用1个Slot。

      reference类型:与基本类型不同的是它不等同本身,即使是String,内部也是char数组组成,它可能是指向一个对象起始位置指针,也可能指向一个代表对象的句柄或其他与该对象有关的位置。局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。

    • 操作数栈。每个帧包含一个后进先出的栈,用于存储正在执行的jvm指令的操作数。当帧被创建时,操作数栈是空的,jvm提供一些指令用于加载常量值,本地变量值,字段值到操作数栈上,另一些jvm指令采用操作数栈上的操作数进行操作,并把结果放回到操作数栈上。操作数栈也用于准备将要传递给方法调用的参数和接收方法调用返回的结果。long和double类型的值占用两个单位的栈深度,其它类型的值占用一个单位的栈深度。

    • 动态链接。每一个帧都包含了对当前方法所属类型的运行时常量池的引用。目的是为了支持方法代码的动态链接。class文件中描述一个方法引用被调用的方法和被访问的变量的代码,是采用符号引用的形式实现的。

      符号引用的形式可以粗略的认为是字符串的形式,就是用字符串标明需要调用哪个类的哪个方法或访问哪个字段或变量。就像符号引用这个名字一样,这些仅仅是符号,是拿不到具体值的,所以必须要进行转换。

      动态链接就是把这些符号方法引用转换为具体的方法引用,在必要时加载类来解析尚未明确的符号,把符号变量的访问转换为这些变量运行时所在存储结构的适合的偏移量(索引)。这样的方式又称为后期绑定。

    • 方法调用。一个方法调用正常完成(即没有抛异常)时,会根据所返回的值的类型执行一个适合的return指令,当前帧会去恢复调用者的状态,包括它的本地变量和操作数栈,使调用者的程序计数器适合的递增来跳过刚刚的那个方法调用指令。

      返回值会被放到调用者帧的操作数栈上,然后继续执行调用者方法的帧。一个方法在调用时抛出了异常,且这个异常没有在这个方法内被捕获处理,将会导致这个方法调用的突然结束,这种情况下永远不会向方法的调用者返回一个值。

Java虚拟机栈可能出现两种类型的异常:

  1. 线程请求的栈深度大于虚拟机允许的栈深度,将抛出StackOverflowError。
  2. 虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时,抛出OutOfMemory异常。

本地方法栈

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

2、Java7和Java在内存模型的区别在于元空间替换了永久代。

方法区是所有线程共享的。主要用于存储类的信息、常量池、方法数据、方法代码等。
方法区是JVM 的规范,永久代(PermGen space)和 Metaspace(元空间)都是HotSpot对方法区这种规范的实现。

需要多大的持久代空间取决于类的数量,方法的大小,以及常量池的大小。

在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之的是 Metaspace(元空间)。

不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小

-XX:MetaspaceSize 初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize最大空间,默认是没有限制的。

调优:-XX:MaxMetaspaceSize={unlimited}

  • 元空间的大小受限于你机器的内存
  • 限制类的元数据使用的内存大小,以免出现虚拟内存切换以及本地内存分配失败。如果怀疑有类加载器出现泄露,应当使用这个参数;32位机器上,如果地址空间可能会被耗尽,也应当设置这个参数。
  • 元空间的初始大小是21M——这是GC的初始的高水位线,超过这个大小会进行Full GC来进行类的回收。
  • 如果启动后GC过于频繁,请将该值设置得大一些
  • 可以设置成和持久代一样的大小,以便推迟GC的执行时间

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:

-XX:MinMetaspaceFreeRatio在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

为啥要移除永久代

  1. 字符串存在永久代中,容易出现性能问题和内存溢出。
  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。如果动态生成类(将类信息放入永久代)或大量地执行 String.intern (将字段串放入永久代中的常量区),很容易造成 OOM。有 -XX:MaxPermSize 的上限。
  3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
  4. Oracle 可能会将HotSpot 与 JRockit 合二为一。

元空间的特点

  • 充分利用了Java语言规范中的好处:类及相关的元数据的生命周期与类加载器的一致。
  • 每个加载器有专门的存储空间
  • 只进行线性分配
  • 不会单独回收某个类
  • 省掉了GC扫描及压缩的时间
  • 元空间里的对象的位置是固定的
  • 如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉

3、直接内存

NIO的Buffer提供了一个可以不经过JVM内存直接访问系统物理内存的类——DirectBuffer。 DirectBuffer类继承自ByteBuffer,但和普通的ByteBuffer不同,普通的ByteBuffer仍在JVM堆上分配内存,其最大内存受到最大堆内存的限制;而DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制。

直接内存的读写操作比普通Buffer快,但它的创建、销毁比普通Buffer慢(猜测原因是DirectBuffer需向OS申请内存涉及到用户态内核态切换,而后者则直接从堆内存划内存即可)。

因此直接内存使用于需要大内存空间且频繁访问的场合,不适用于频繁申请释放内存的场合。

(Note:DirectBuffer并没有真正向OS申请分配内存,其最终还是通过调用Unsafe的allocateMemory()来进行内存分配。不过JVM对Direct Memory可申请的大小也有限制,可用-XX:MaxDirectMemorySize=1M设置,这部分内存不受JVM垃圾回收管理。)

使用对外内存的原因:

  • 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。
  • 提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。

相关文章

网友评论

      本文标题:Java内存模型

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