Java运行数据区/堆/栈

JVM Runtime Data Area(运行数据区)

根据《Java虚拟机规范(Java SE 7版)》规定,JVM所管理的内存包括:

  • 线程共享:堆区,方法区和运行常量池(位于方法区);
  • 线程私有:程序计数器,栈区,本地方法栈;

PC Register(程序计数器)

  • 程序计数器与线程生命周期保持一致,存储当前线程执行的方法字节码指令地址(如果是native方法,程序计数器存储值为undefined),解释器负责解释程序计数器中的指令,提交OS执行;
  • 线程私有原因:多线程场景下,CPU上下文切换频繁,为每个线程分配一个程序计数器,可以准确记录各线程正在执行的字节码指令地址,防止各线程互相干扰;

Native Method Stack

与Java栈区不同,本地方法栈为虚拟机使用的native方法服务。当程序通过JNI(Java Native Interface)调用native方法(C/C++代码),根据调用语言类型建立对应栈;

Method Area(方法区)

类加载子系统(Class Loader)将字节码文件加载到方法区,因此方法区存储Java类的元数据/型数据(类加载信息,常量static变量,方法字节码),被各个线程共享;
  • 在Java SE7规范中方法区不属于堆;
  • 在HotSpot虚拟机中,方法区仅仅逻辑上独立,物理上属于Java堆区,在没有显式回收方法区内存的情况下,GC只回收方法区中的废弃常量和无用的类,因此称为在堆中称为永久代(Permanent Generation)
★ 判断一个类是否可被回收的条件
  1. Java堆区中不存在该类的任何实例;
  2. 加载该类的ClassLoader已经被回收;
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该类方法;
★ 运行时常量池(constant pool)
在HotpSpot for JDK1.7之前,运行常量池属于方法区,可被GC回收,JDK1.7版本已计划逐步移出方法区;

C/C++中的栈与堆

  • 栈内存:所有局部变量,形式参数都在栈中分配内存,退出函数时自动销毁栈中内容,性能较高;栈中所分配内存大小是在编译时确定,在程序运行时进行;
  • 堆内存:在程序运行时,由程序向OS动态申请,由操作系统进行内存分配,因此在分配和销毁时与栈相比,性能较低;堆中所分配内存大小和生命周期在程序运行时确定和进行;

Java堆区/线程共享

堆区的生命周期与虚拟机相同,一个虚拟机实例对应一个堆区;

堆区划分

 
根据分代收集算法思想,将堆区划分为新生区,养老区和永久区;
1. 新生区(Young Generation):所有对象在该对象被创建,新生区又分为Eden空间,From Survivor空间和To Survivor空间;
  • HoptSpot默认Eden区和一个Survivor区的大小比例是8:1,用参数-XX:SurvivorRatio设置;
2. 养老区(Old Generation):需要大量连续内存空间的Java对象(典型代表如字符串或者数组)直接在养老区中分配内存;
  • 直接1:对象大小大于Eden + From Survivor区大小,直接在养老区分配内存;
  • 直接2:虚拟机提供-XX:PretenureSizeThreshold参数,令大于这个阈值的对象直接在养老区分配;
    • PretenureSizeThreshold参数只对Serial和ParNew两个收集器有效,对Parallel Scavenge收集器无效;
3. 永久区(Permanent Generation Space):即属于堆的方法区;

堆区存储数据

堆内存:通过new创建的对象和/数组都在堆中分配内存,主要用来存放对象;


堆区创建对象过程


  1. 虚拟机遇到new指令时,首先去检查该指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化过。
  2. 类装载过程完成后,即可确定对象内存大小,接下来JVM将要对其进行内存分配(涉及到线程安全机制和GC机制);
  3. 分配完内存后,JVM会初始化对象头和实例数据,最后将对象引用入栈,更新程序计数器中的字节码指令地址;

对象分配内存

堆区内存规整时,采用指针碰撞算法进行内存分配,适用于Serial, ParNew等带标记-整理过程的收集器;
堆区内存不规整时,采用空闲链表进行内存分配,适用于CMS这种带有标记-清除的收集器;

保证对象内存分配的线程安全机制

  • TLAB:为每个线程在Eden区中划分一片私有内存,称为本地线程分配缓冲区(Thread Local Allocation Buffer, TLAB);
  • 类装载完成后JVM优先选择TLAB给对象分配内存;TLAB空间默认仅占Eden区的1%,当对象在TLAB空间内存分配失败,JVM通过加锁保证分配操作的原子性,然后直接在Eden区中分配内存;

对象内存布局(堆区)

 
在HotSpot虚拟机中,对象在堆中分为三部分存储:对象头,实例数据和对齐填充;
  • 对象头(Mark Word)
    • 存储对象自身的运行数据,大小是8字节的整数倍;
    • 对象头元数据指针,对象指向方法区中类元数据的指针,虚拟机通过该指针确定对象是哪一个类的实例,确定对象大小;
    • 对于Java数组,对象头中必须额外记录数组长度,因为虚拟机无法从数组的类型数据中确定数组大小;
  • 实例数据
  • 对齐填充由于HotSpot VM的内存管理系统要求对象大小必须是8字节的整数倍,当对象实例数据部分没有对齐时,对齐填充负责补全;




堆区的优缺点

  • 优点:可以动态分配内存大小,对象生命周期不必事先告诉编译器,Java GC负责回收堆内存;
  • 缺点:由于在运行时动态分配内存,导致访问速度比栈慢;


Java栈区/线程私有

Java栈区又被称为Java虚拟机栈,Java栈区主要用于存储栈帧
栈的生命周期:栈的生命周期与线程相同,栈内存不存在GC过程,不同线程中的栈帧不存在相互引用;
栈内存:函数中定义基本类型变量对象引用都在栈中分配内存,主要用来执行程序;

栈区的优缺点

  • 优点:访问速度比堆快,仅次于直接位于CPU的寄存器
  • 缺点:存在栈中的数据大小和生命周期必须时确定的;

栈帧(Stack Frame)

栈中数据以栈帧(stack frame)形式存在,每个栈帧是一个实际的内存块,栈帧中主要保存3类数据:
  • 局部变量表(local variables):输入/输出参数,方法内的临时变量,在编译阶段确定大小;
  • 操作数栈(operand stack):记录出栈/入栈操作,在编译阶段确定大小;
  • 动态链接(frame data):指向运行时常量池中该帧所属方法的引用;
  • 方法返回地址;一旦方法在执行过程中遇到字节码返回指令时,将方法返回值返回给它的调用者;
  • 其他额外信息;

在线程中,只有位于栈顶的栈帧有效,称为当前线程(Current Stack Frame);

栈帧与方法

栈帧是线程中方法的执行环境。每一个方法被调用时,JVM会创建一个与之对应的栈帧,负责存储方法执行所需的各项数据信息,每一个方法从调用到执行结束,伴随着一个独立的栈帧从入栈到出栈的过程;

栈帧:局部变量表

局部变量表用于存储方法参数和在方法内部定义的局部变量,容量大小在编译阶段确定(保存在.class文件中Code属性);
局部变量表的存储单元是变量槽(Slot),JVM为每一个Slot分配一个访问索引,通过该索引即可访问指定局部变量;
  • 1个Slot可以存储boolean/byte/char/short/float/reference/returnAddress(指向字节码指令地址,已过时),2个Slot可以存储long和double的64位数值(高位对齐的方式连续分配2个Slot);
  • 单个Slot大小不固定,一般默认为32位;

栈帧:操作数栈

操作数栈本质是一个后入先出栈,JVM通过标准的出栈/入栈数据类型或字节码指令,来解释执行字节码;
操作数栈中的存储单元可以为任意Java数据类型,32位数据类型栈空间为1,64位数据类型栈空间为2,

如果指定在栈中为对象分配内存?

通过逃逸分析技术(用于分析出对象的作用域)筛选出未发生逃逸的对象,然后直接在栈帧中为对象分配内存空间
  1. 对象逃逸:当定义在方法体中的对象被方法体外部引用时,对象发生“逃逸”;
  2. ★ 对象未逃逸:定义在方法体内的对象未被任何外部成员引用,虚拟机在栈帧中为该对象分配内存空间
    • 对象在栈上分配空间后,生命周期与栈帧相同,无需GC进行垃圾回收;
    • 方法被调用时,栈上对象随着栈帧一同被创建;方法执行结束后,随着栈帧出栈被一并销毁;


引用定位对象

引用分类

  • 强引用/StrongReference:引用明确指向对象,GC运行时不回收;
  • 软引用/SoftReference:内存不足时,运行GC时回收,用于实现内存敏感的高速缓存;
  • 弱引用/WeakReference:无论当前内存是否足够,GC运行时会立即回收被弱引用关联的对象;
  • 虚引用/PhantomReference:类似于无引用,无法通过虚引用获取对象实例,
    • 为对象设置虚引用关联的唯一目的是在该对象被回收时,获取一个系统通知;
    • 主要跟踪对象被回收的状态,必须与引用队列/ReferenceQueue联合使用,不允许单独使用;

引用定位对象

引用定位对象的主流方式分为:句柄和直接指针两种

通过句柄

堆中专门分配一块内存作为句柄池,引用中存储的是对象的句柄地址,句柄中包含对象实例数据地址和类型数据地址;
优点:对象移动时,引用本身不用修改,只需修改句柄中的实例数据指针;

通过直接指针

 引用直接存储对象在堆区中的地址,速度更快,HotSpot虚拟机采用该方式定位对象;







OutOfMemoryError/内存溢出错误

Java堆溢出
  • Java堆中用于存储对象实例,在GC清楚对象的前提下,对象数量达到堆的最大容量限制后产生内存溢出异常;
  • -Xmx 设置堆区最大值
Java栈溢出
  • 在单线程下,线程请求的栈深度大于虚拟机所允许的最大深度,当内存无法分配时,抛出StackOverflowError异常;
  • 在多线程下,由于操作系统分配给每个进程的内存容量有限制,不断建立线程会抛出OutOfMemoryError异常;
    • -Xss 设置Java栈大小;
    • -Xoss 设置本地方法栈大小(HotSpot虚拟机并不区分Java栈和本地方法栈,因此-Xoss参数实际无效);
运行时常量池溢出
由于常量池分配在永久代(方法区)中,通过-XX:PermSize和-XX:MaxPermSize限制方法区大小间接限制常量池大小;
方法区溢出
本机直接内存溢出
  • 本地直接内存不是虚拟机运行时数据区的一部分,本质是本机内存;
  • 当虚拟机中各部分区域内存总和超过物理内存限制时,抛出OutOfMemoryError异常;
  • -XX:MaxDirectMemorySize指定直接内存大小,如果不指定,默认与Java堆最大值一样;

原文地址:https://www.cnblogs.com/yzwall/p/6637164.html