https://blog.csdn.net/hxpjava1/article/details/55189077
概述
java 的内存管理采用自动内存管理机制, 这样就不需要程序员去写释放内存的代码, 而且不容易出现内存泄漏问题正是由于内存的申请和释放都交给了 Java 虚拟机, 一旦出现内存泄漏和溢出问题时, 在不了解 Java 虚拟机内存结构和自动管理机制的情况下, 很难排查问题的所在所以一个成熟的程序员和架构师, 必须很好的掌握 Java 虚拟机的自动内存管理机制
运行时数据区
上图的虚拟机运行时数据区是 Java 虚拟机规范所规定的区域, 不同的虚拟机有不同的实现
Java 虚拟机栈 / 本地方法栈
Java 栈也称作虚拟机栈(Java Vitual Machine Stack), 也就是我们常常所说的栈, 跟 C 语言的数据段中的栈类似事实上, Java 栈是 Java 方法执行的内存模型为什么这么说呢? 下面就来解释一下其中的原因
Java 栈中存放的是一个个的栈帧, 每个栈帧对应一个被调用的方法, 在栈帧中包括局部变量表 (Local Variables) 操作数栈 (Operand Stack) 指向当前方法所属的类的运行时常量池 (运行时常量池的概念在方法区部分会谈到) 的引用 (Reference to runtime constant pool) 方法返回地址 (Return Address) 和一些额外的附加信息当线程执行一个方法时, 就会随之创建一个对应的栈帧, 并将建立的栈帧压栈当方法执行完毕之后, 便会将栈帧出栈因此可知, 线程当前执行的方法所对应的栈帧必定位于 Java 栈的顶部讲到这里, 大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了以及为什么栈区的空间不用程序员去管理了(当然在 Java 中, 程序员基本不用关系到内存分配和释放的事情, 因为 Java 有自己的垃圾回收机制), 这部分空间的分配和释放都是由系统自动实施的对于所有的程序设计语言来说, 栈这部分空间对程序员来说是不透明的下图表示了一个 Java 栈的模型:
局部变量表, 顾名思义, 想必不用解释大家应该明白它的作用了吧就是用来存储方法中的局部变量 (包括在方法中声明的非静态变量以及函数形参) 对于基本数据类型的变量, 则直接存储它的值, 对于引用类型的变量, 则存的是指向对象的引用局部变量表的大小在编译器就可以确定其大小了, 因此在程序执行期间局部变量表的大小是不会改变的
操作数栈, 想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生, 栈最典型的一个应用就是用来对表达式求值想想一个线程执行方法的过程中, 实际上就是不断执行语句的过程, 而归根到底就是进行计算的过程因此可以这么说, 程序中的所有计算过程都是在借助于操作数栈来完成的
指向运行时常量池的引用, 因为在方法执行的过程中有可能需要用到类中的常量, 所以必须要有一个引用指向运行时常量
方法返回地址, 当一个方法执行完毕之后, 要返回之前调用它的地方, 因此在栈帧中必须保存一个方法返回地址
由于每个线程正在执行的方法可能不同, 因此每个线程都会有一个自己的 Java 栈, 互不干扰
本地方法栈和虚拟机栈非常相似, 不同的是虚拟机栈服务的是 Java 方法, 而本地方法栈服务的是 Native 方法 HotSpot 虚拟机直接把本地方法栈和虚拟机栈合二为一会抛出 StackOverflowError 和 OOM 异常
本地方法栈与 Java 栈的作用和原理非常相似区别只不过是 Java 栈是为执行 Java 方法服务的, 而本地方法栈则是为执行本地方法 (Native Method) 服务的在 JVM 规范中, 并没有对本地方发展的具体实现方法以及数据结构作强制规定, 虚拟机可以自由实现它在 HotSopt 虚拟机中直接就把本地方法栈和 Java 栈合二为一
程序计数器
程序计数器(Program Counter Register), 也有称作为 PC 寄存器想必学过汇编语言的朋友对程序计数器这个概念并不陌生, 在汇编语言中, 程序计数器是指 CPU 中的寄存器, 它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址), 当 CPU 需要执行指令时, 需要从程序计数器中得到当前需要执行的指令所在存储单元的地址, 然后根据得到的地址获取到指令, 在得到指令之后, 程序计数器便自动加 1 或者根据转移指针得到下一条指令的地址, 如此循环, 直至执行完所有的指令
虽然 JVM 中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的 CPU 寄存器, 但是 JVM 中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的, 也就是说是用来指示 执行哪条指令的
由于在 JVM 中, 多线程是通过线程轮流切换来获得 CPU 执行时间的, 因此, 在任一具体时刻, 一个 CPU 的内核只会执行一条线程中的指令, 因此, 为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置, 每个线程都需要有自己独立的程序计数器, 并且不能互相被干扰, 否则就会影响到程序的正常执行次序因此, 可以这么说, 程序计数器是每个线程所私有的
在 JVM 规范中规定, 如果线程执行的是非 native 方法, 则程序计数器中保存的是当前需要执行的指令的地址; 如果线程执行的是 native 方法, 则程序计数器中的值是 undefined
由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变, 因此, 对于程序计数器是不会发生内存溢出现象 (OutOfMemory) 的
堆
Java 堆用于存放对象实例: The heap is the runtime data area from which momory which memory for all class instances and arrays is allocated 是垃圾收集器管理的主要区域可细分为: 新生代和老年代; 新生代又可分为 Eden,from Survivor,to Survivor 会抛出 StackOverflowError 异常
方法区
方法区存储虚拟机加载的类信息, 常量, 静态变量, 即时编译器编译后的代码等数据 HotSpot 中也称为永久代 (Permanent Generation),(存储的是除了 Java 应用程序创建的对象之外, HotSpot 虚拟机创建和使用的对象) 为什么称为永久代呢?? 各个地方说的都不清楚, 查看官方文档, 解释为: 永久代中的对象并不是永久的, 只是历史上被叫做永久代罢了 In fact, the objects in it are not permanent, but thats what it has been called historically.
方法区在不同虚拟机中有不同的实现, HotSpot 在 1.7 版本以前和 1.7 版本, 1.7 后都有变化
jdk7 版本以前的实现
jdk7 版本的改动是把字符串常量池移到了堆中
jdk8 MetaSpace jdk1.8 中则把永久代给完全删除了, 取而代之的是 MetaSpace
运行时常量池和静态变量都存储到了堆中, MetaSpace 存储类的元数据, MetaSpace 直接申请在本地内存中(Native memory), 这样类的元数据分配只受本地内存大小的限制, OOM 问题就不存在了除此之外, 还有其他很多好处:
- Take advantage of Java Language Specification property : Classes and associated metadata lifetimes match class loaders
- Linear allocation only
- No individual reclamation (except for RedefineClasses and class loading failure)
- No GC scan or compaction
- No relocation for metaspace objects
运行时常量池
运行时常量池 (Runtime Constant Pool) 是方法区的一部分 Class 文件中存储有常量池(Constant Pool Table), 用于存放编译期生成的各种字面量和符号引用, 这部分内容将在类加载后进入方法区的运行时常量池中存放一般来说, 处理保存 Class 文件中描述的符号引用外, 还会把翻译出来的直接引用也存储在运行时常量池中
运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性 Java 语言并不要求常量一定只有编译期才能产生
对象的内存布局
Mark Word 用于存储对象自身的运行时数据, 如哈希码, GC 分代年龄, 锁状态标志, 线程持有的锁, 偏向线程 ID, 偏向时间戳等
类型指针即对象指向它的类元数据的指针并不是所有的虚拟机实现都必须在对象数据上保留类型指针(用句柄实现)
实例数据 存储程序代码中定义的各种类型的字段内容, 这部分的存储顺序会受到虚拟机分配策略参数 (FieldsAllocationStyle) 和字段在 Java 源码中定义的顺序的影响 HotSpot 虚拟机默认的分配策略为 longs/doubles,ints,shorts/chars,bytes/booleans,oop(Ordinary Object Pointers)
对齐填充 并不是必然存在的, 没用特别的含义 HotSpot 的自动内存管理系统要求对象的起始地址必须是 8 字节的整数倍(对象的大小必须是 8 字节的整数倍)
对象的创建
虚拟机遇到一条 new 指令时, 首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用, 并检查符号引用代表的类是否已被加载, 解析和初始化过如果没有就先执行类的加载过程接下来虚拟机为新对象分配内存(指针碰撞或空闲列表, Serial,ParNew 等带 Compact 过程的收集器时采用指针碰撞, CMS 这种基于 Mark-Sweep 缩放的收集器时通常采用空闲列表)
处理并发是通过 CAS 配上失败重试的方式或者每个线程在堆上预先分配本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
内存分配完成后, 虚拟机将内存空间都初始化为零值 (不包括对象头) 然后对对象头数据进行设置
在完成以上工作后, 从虚拟机的视角来看, 一个新的对象已经产生但从 Java 程序的视角来看, 在执行完 new 指令之后会接着执行 < init > 方法, 把对象按照程序员的意愿进行初始化, 这样一个真正可用的对象才算完全产生出来
对象的访问定位
这两种对象的访问方式各有优势, 使用句柄来访问的最大好吃就是 reference 中存储的是稳定的句柄地址, 在对象被移动时只会改变句柄中的实例数据指针, 而 reference 本身不需要修改
使用直接指针访问的最大好处就是速度快, 它节省了异常指针定位的时间开销, 由于对象的访问在 Java 中非常频繁, 因此这类开销积少成多也是一项可观的执行成本
HotSpot 是通过直接指针访问对象的方式进行对象访问的
关于 为什么新生代内存需要有两个 Survivor 区 https://blog.csdn.net/antony9118/article/details/51425581 的分析解释
辅助学习还可以看这篇 Java 内存模型 http://www.importnew.com/19612.html
来源: http://www.bubuko.com/infodetail-2546986.html