C C++ 对内存管理拥有最高权力,拥有每一个对象的所有权 ,也承担着每一个对象声明从开始到终结的维护任务
Java 虚拟机自动内存管理 不容易出现内存泄漏和溢出的问题 但一旦出现,控制内存的权力交给了Java虚拟机,排查错误修改问题会很难
运行时数据内存 程序计数器程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的 字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器 的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 ---- 来自《深入理解Java虚拟机》
三个特点
如果线程正在执行的是一个Java方法,这个技术器记录的是正执行的虚拟机字节码指令的地址如果执行的是本地方法(Native)方法,这个计数器的值应为空程序计数器是《Java虚拟机规范》中没有规定任何OOM情况的区域 例子来看一段代码
public int demo(){ int a = 1; int b = 2; return a + b;}
javap进行反编译
当执行到当前方法时,当前的线程中会创建对应的程序计数器,存放执行地址 ----> 上图红框中的内容
进一步理解
我们知道线程是CPU进行调度的最小单位,在一个时间内处理机只会分配给一个线程执行指令
所以当线程1正在占有CPU时,线程2进来且优先级比线程1要高,所以我们把线程1挂起,而这时候线程1的程序计数器中存储线程1的执行位置,
当线程2执行完后,唤醒线程1同时查看线程1的程序计数器,恢复指令上下文。
如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)?
因为程序计数器记录的是字节码文件的指令地址,而本地方法大多是C实现未编译成需要指定的字节码指令
Java虚拟机栈Java虚拟机栈是线程私有的,声明周期与线程同步
每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧Stack frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信
息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
-----来自《深入理解Java虚拟机》
重点是局部变量表
局部变量表中存放了编译期可知的各种java虚拟机基本数据类型(整数类型,布尔类型,字符类型)和对象引用类型(不等于于对象本身,可能是一个指向对象起始地址的引用指针,也可以是句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
这些数组类型在局部变量表中的存储空间以局部变量槽(Slot)来表示(64位的long和double占用两个变量槽,其他数据类型只占用一个)。
局部变量表所需的内存空间在编译期间完成分配。
Java虚拟机栈有StackOverflowError异常和OutOfMemoryError异常
本地方法栈Java堆本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
----来自《深入理解Java虚拟机》
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
唯一目的是存放对象实例。
Java世界内几乎所有的对象实例都在堆里分配内存
Java堆是垃圾收集器管理的内存区域,所以也被称为GC堆
从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区
将Java堆细分的目的是为了更好的回收内存,或者更快的分配内存
方法区各个线程共享
用于存储已被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。
JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了 JDK 8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(meta- space)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
运行时常量池方法区的一部分
Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
HotSpot虚拟机对象 对象的创建Java对象的创建我们最熟悉的就是new一个对象了,对象的创建在虚拟机中是怎样一个过程呢?
java虚拟机遇到一条字节码new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有将先执行响应的类加载过程虚拟机为新生对象分配内存 —> 对象所需内存的大小在类加载完成后便可以完全确定 —>把一块确定大小的内存块从Java堆中划分出来内存分配完成后,虚拟机将分配到的内存空间都初始化为零值,如果使用本地线程分配缓冲,这项工作也可以在本地线程分配缓冲中顺便进行Java虚拟机对对象进行必要的设置 —> 这个对象是哪个类的实例,如何才能找到类的原数据信息,对象的哈希码,对象的GC分代年龄等信息。虚拟机角度一个新的对象已经产生了。 从Java程序的视角看,对象创建刚刚开始 —> Class文件中的()方法还没有执行,所有字段默认为零值()方法执行后,一个真正可用的对象才算完全被构造出来指针碰撞
在java堆中内存是绝对规整的,所有被使用的内存都放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,分配内存就是紧紧把那个指针向空闲方向挪动一段与对象大小相等的距离。
空闲链表
如果不是规整的,已被使用的内存和空闲的内存相互交错在一起,那么虚拟机就需要维护一个列表,记录上哪些内存块是可用的,在分配的时候从链表中找到一块足够大的空间划分给对象实例
本地线程分配缓冲(TLAB)
每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,那个线程要分配内存,就在那个线程的本地缓冲区分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
对象的内存布局对象头对象在堆内存中的存储布局 —> 三部分: 对象头,实例数据,对齐填充
包括两类信息
用于存储对象自身的运行时数据,如哈希码,GC分代年龄、锁状态标志,线程持有的锁、偏向时间戳等 官方称这部分为"Mark Word"
对象头里的信息与对象自身定义的数据无关的额外成本,考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构
类型指针 对象指向它的类型元数据的指针
如果对象是数组,对象头还必须有一块用于记录数组长度的数据
实例数据对象真正存储的有效信息(我们在程序代码里面所定义的各种类型的字 段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来)
对齐填充不是必然存在的,也没有特别的含义,仅仅起着占位符的作用。
Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,对象头部分已经被设计成了8字节的倍数,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
对象的访问定位句柄Java程序会通过栈上的reference数据来操作堆上的具体对象。
reference类型在《Java虚拟机》里只规定了他是一个指向对象的引用,并没有定义这个引用应该通过什么方式定位、访问到堆中对象的具体位置
主流的两种访问方式 :句柄 和 直接指针
使用句柄访问,java堆中会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自具体的地址信息
好处:reference中存储的是稳定句柄地地址,对象被移动时只会改变句柄中实例数据指针,reference本身不需要被修改
直接指针访问如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关 信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销
好处:访问速度快,节省了一次指针定位的开销
Hotspot虚拟机主要使用第二种方式进行对象访问
参考资料《深入理解Java虚拟机》周志明著