• 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堆最大值一样;

  • 相关阅读:
    redhat,centos Linux常用命令LS之常用功能
    人生信用卡
    如何让Redhat Linux启动时进入字符终端模式(不进入XWindow)
    OpenJDK和JDK区别
    Linux rpm 命令参数使用详解[介绍和应用]
    linux 的vim命令详解
    centos6.4安装javajdk1.8
    samba服务器 实现Linux与windows 文件共享
    SELinux 宽容模式(permissive) 强制模式(enforcing) 关闭(disabled) 几种模式之间的转换
    linux学习之 Linux下的Eclipse安装
  • 原文地址:https://www.cnblogs.com/yzwall/p/6637164.html
Copyright © 2020-2023  润新知