1、内存区域概要
java虚拟机会在执行java程序的时候把它所管理的内存划分为若干的区域
方法区(method area)(Non-Heap) | 存储被虚拟机加载的类信息 常量 静态变量 即时编译器编译后的代码。 jvm对方法区限制很宽松,GC处理在该区域较少见,但却是必要。 否则会导致异常:OutOfMemoryError 运行时常量池是class文件的一项信息,用于存放编译器生成的各种字面量和符号引用和翻译出来的直接引用,类加载后进入方法区,成为方法区的一部分。String的intern方法可以动态进入运行时常量池。 |
堆(heap) | jvm所管理的内存中最大的一块儿,被线程共享,jvm启动时创建。 存放对象实例以及数组 是GC(垃圾收集器)主要管理区域 堆可以细分:新生代和老年代 堆可以更细分:Eden空间、FromSurvivor空间、ToServivor空间 堆可以划分多个线程私有分配缓冲区TLAB,目的是更好的回收内存,更快的分配内存 堆无法扩展时,抛异常:OutOfMemoryError |
虚拟机栈(VM Stack) | 线程私有,生命周期与线程相同,描述的是java方法执行的内存模型。 每个方法在执行时都会创建一个栈帧用来存储局部变量表 操作数栈 动态链接 方法出口等。 每一个方法从调用直至执行完成的过程 对应的是栈帧在虚拟机中入栈到出栈的过程。 栈帧分配空间是确定的,运行期间不会改变局部变量表大小 栈深度大于虚拟机允许深度:抛异常 StackOverflowError 栈扩展时无法申请足够内存:抛异常OutOfMemoryError |
本地方法栈(Native Method Stack) | 为虚拟机使用的native方法服务 栈深度大于虚拟机允许深度:抛异常 StackOverflowError 栈扩展时无法申请足够内存:抛异常OutOfMemoryError |
是一块较小的内存空间,可以看作当前线程的字节码行号指示器。
通过改变计数器的值来选取下一条需要执行的字节码指令。
每条线程都需要有一个独立的程序计数器,且相互独立
额外部分:直接内存(Direct Memory):不属于运行时数据区,在一些场景中提高性能,避免java堆和native堆来回复制数据
2、对象创建
1.虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载 解析和初始化过。后续部分会详细说。
2.加载通过后虚拟机为新生对象分配内存,内存大小在类加载完确定。将一个内存指针向空闲的对内存中挪动一段与对象大小相同的距离(也成为指针碰撞)。如果内存空间不是连续 规整的,则虚拟机必须维护一个列表(空闲列表),将合适的区域进行分配并实时更新。区域是否规整取决于GC的功能。注意:并发情况下指针碰撞不是线程安全的,两种解决方案(虚拟机采用CAS 搭配失败重试方式做同步处理 或 不同线程分配不同的线程分配缓冲区TLAB,即每个线程对应一小块儿内存)。
3.内存分配完后虚拟机需要将分配到的内存空间初始化为0
4.jvm对对象的对象头进行必要的设置,例如对象是哪个类实例,如何找到类的元数据信息,对象哈希码,GC分代年龄信息等。
5.至此jvm new对象的工作就完成了,但是在java程序的角度这仅仅是开始,还要执行程序员希望的init方法(由字节码中是否 包含invokespecial指令决定)。init执行之后才是真正的可用对象。
3、对象的存储
对象存储在内存中可以分成三个区域:1、对象头 2、实例数据 3、对齐补充。
对象头包括两部分信息:1、对象自身运行数据 2、类型指针
对象自身运行数据:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据在32和64位系统分别是32和64位长度
类型指针:虚拟机通过这个指针来确定这个对象是哪个类的实例,如果对象是一个java数组,对象头还必须有记录数组长度的数据
实例数据:对象真正存储的有效信息,也是代码中所定义的各种类型的字段内容,存储顺序受虚拟分配策略(相同宽度的字段总是分配到一起)和java源码中定义顺序影响。
对齐补充:不是必然存在的,仅仅是占位符作用。因为对象的大小必须是8字节的整数倍,因此需要补齐。
4、对象的访问定位
java程序如果希望访问堆中的对象实例,需要通过栈的reference数据来操作堆上的具体对象。reference是一个指向对象的引用。指向对象的方式有两种:句柄和直接指针
句柄:需要在堆中额外开辟句柄池,句柄池是对象实例的数据与类型数据的具体地址信息。优点:地址信息变动不用改变reference中存储的句柄地址。缺点:效率低。
直接指针:reference存储的就是对象地址。优点:速度快。缺点 对象移动时改变地址。