JVM内存区域

  • 程序计数器

  1. 简介:

    java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,任何一个确定的时刻,一个处理器(内核)都会只执行一条线程中的指令,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各线程之间计数器互不影响,独立存储,这类内存区域为线程私有内存区域

  2. 异常:

    此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

    • Java虚拟机栈:

      1. 简介:

        Java虚拟机栈也是 线程私有 的,是用来描述Java方法执行的内存模型:每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用到执行完成,对应着一个栈帧从入栈到出栈的过程。

        通常把JVM内存区域分为堆和栈的这个栈值得是虚拟机栈中的局部变量表部分:存放了各种基本数据类型,对象的引用。其中,64位长度的long和double类型会占两个局部变量空间,其余数据类型占一个。局部变量表所需要的内存空间在编译期间完成分配的,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,并且方法运行期间不会改变变量表的大小

      2. 异常:

        如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError错误,原因基本是因为深度递归、死循环或定义大量的本地变量导致,栈大小通过-Xss(每个线程的栈大小,也可以说是虚拟机的最小栈分配单位)来进行设置,一般设置大小为1M足够

        虚拟机栈可动态扩展时,当无法申请到足够的内存时,会抛出OutOfMemoryError异常,JVM栈空间被线程分割的一块一块的,如果有线程没有栈空间可以分配了会抛出OutOfMemoryError异常,很多线程等待,一直不释放,通过-Xss给每个线程分配的空间加起来大于Jvm的内存空间

    • 本地方法栈:

      1. 简介:

        与Java虚拟机栈的区别:虚拟机栈为虚拟机执行Java方法,本地方法栈为虚拟机执行使用到的native方法服务

      2. 异常:

        异常同上

  • Java堆

    1. 简介:

      被所有线程所共享的一块内存区域,用来存放对象实例,是垃圾收集器主要管理的区域,有新生代和老年代

    2. 异常:

      当需要在堆中完成实例的分配,但是堆中无法再扩展时,将会抛出OutOfMemoryError:java heap space的异常,通过-Xmx和-Xms来分配大小。

      当遇到异常时,首先对dump出来的对转储进行分析,重点是确认内存中的对象是否是有必要的,也就是先分清楚到底是内存泄露,还是内存溢出:

      • 内存泄露的话查看泄露对象到GC Roots的引用链,看是哪个对象做了什么操作导致垃圾收集器没有进行回收
      • 如果是内存溢出的话,也就是这些对象都很正常,都应该活着,则看下虚拟机的堆参数-Xmx和-Xms是否可以调大
  • 方法区(jdk1.8之后MetaSpace是方法区的实现(使用本地内存),以前永久代是方法区的实现)

    1. 简介

      各个线程共享的区域,存放加载的类信息,常量,静态变量,及时编译后的代码

      运行时常量池 :是方法区的一部分,用于存仓编译器生成的各种字面量和字符的引用,联想到String那一节(jdk1.8之后放到了堆中)

    2. 异常:

      当方法区无法满足内存分配时,会抛出OutOfMemoryError:PermGen space的错误

  • java内存区域示意图:

  • 对象的创建

Dog dog = new Dog();

虚拟机遇到一个new指令时:

  1. 首先在常量池中查找Dog,看是否能定位到Dog类的符号引用,如果能定位到说明这个类已经被加载到了方法区了,继续执行,没有的话执行类的加载过程,在类加载那里详细说

  2. 类加载完后,虚拟机将为新生的对象分配内存,对象所需的大小在类加载完之后就可以确定下来,就是在堆中划出一部分内存给此对象

  3. 内存分配完后,要对对象进行一些必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象的GC分代年龄信息,这些信息存放在对象头上

  4. 从虚拟机的视角来看,此时一个新的对象已经产生了,但是对于java程序的视角,对象才刚刚开始,然后去执行对象的构造方法,构造方法执行完对象才算彻底创建完成了

  5. 到此,new运算符可以返回堆中这个对象的引用了,然后,根据dog这个对象是局部变量还是成员变量(实例变量),还是静态变量来确定将引用放在哪里:

    局部变量:变量在帧栈的局部变量表,这个对象的引用就放在帧栈

    成员变量:变量在堆中,对象的引用也放在堆中

    静态变量:变量在方法区,对象的引用也放在方法区
    picture 5

    picture 6

    picture 7

  • 对象的内存布局

    对象的内存布局分为三部分:对象头(Header),实例数据(Instance),对齐填充(Padding)

    对象头分为两部分信息:

    • 第一部分:用于存储对象自身的运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等。

    • 第二部分:存储的是类型指针,通过这个指针来确定对象是哪个类的实例,这个访问方式主流的有两种,根据虚拟机的不同而不同,主流的有使用句柄和直接指针两种,示例图如下:

      picture 8

      picture 9

  • 常见问题

    1. java性能调优参数-Xms -Xmx -Xss的含义

      -xss:规定了每个线程虚拟机栈的大小,将会影响并发线程数的大小

      -xms:堆的初始值,对象刚创建出来的时候的大小

      -xmx:堆能达到的最大值

    2. java内存模型中堆和栈的区别-内存分配策略

      • 静态存储:编译时确定每个数据目标在运行时的存储空间需求:要求程序代码中不允许有可变结构的存在,也不允许有嵌套或者递归
      • 栈式存储:数据区需求在编译时未知,运行时模块入口前确定
      • 堆式存储:编译时或运行时模块入口都无法确定,需要动态分配,可变长度串,和实例

      联系:

      • 引用对象、数组时,栈定义变量保存堆中目标的首地址

      区别:

      • 管理方式:栈自动释放,堆需要GC
      • 空间大小:栈比堆小
      • 碎片相关:栈产生的碎片小于堆
      • 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
      • 栈的效率比堆高
    3. MetaSpace相比PermGen的优势

      • 字符串常量池存在永久代中,容易出现性能问题和内存溢出
      • 类和方法的信息大小难以确定,给永久代的大小指定带来困难
      • 永久代会为GC带来不必要的复杂性

      intern方法:

      picture 10

      picture 11

      picture 12

      picture 13

  • 参考:https://www.jianshu.com/p/ebaa1a03c594