Java内存区域与内存溢出异常、对象的创建

运行时数据区域

在这里插入图片描述

程序计数器(Program Counter Register)

一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
线程私有”:每条线程都有一个独立的程序计数器。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时间,一个处理器都只会执行一条线程中的指令。
如果正在执行一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果是native方法,则计数器值为空。
程序计数器是JVM中唯一没有规定OutOfMemoryError情况的区域

Java虚拟机栈(Java Virtual Machine Stacks)

线程私有,生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表:存放了编译器可知的各种基本数据类型、对象引用类型(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
每个方法执行的过程对应的就是一个栈帧在虚拟机栈中从入栈到出栈的过程。
当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法执行期间是不会改变局部变量表的大小。
两种异常
1、如果线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常。
2、如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存,会抛出OutOfMemoryError异常。
举例:递归,两个实体类之间相互引用。

本地方法栈(Native Method Stack)

与虚拟机栈作用相似,区别:虚拟机站为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机使用到的Native方法服务。
注:一个Native Method就是一个java调用非java代码的接口。

Java堆(Java Heap)

Java堆是Java虚拟机所管理的内存中最大的一块
Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。唯一目的是存放对象实例,Java虚拟机规范描述:所有的对象实例以及数组都要在堆上分配。JIT编译器发展和逃逸分析的成熟,栈上分配、标量替换优化技术发送变化,所有对象在堆上分配不那么绝对。
Java堆是垃圾收集器管理堆主要区域,也称为“GC堆”。从内存回收角度,Java堆还细分为:新生代和老年代;再细致分为Eden空间、From Survivor空间和To Survivor空间。从内存分配角度,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。
如果堆中没有内存完成实例分配,堆也无法扩展时,会抛出OutOfMemoryError异常。

方法区(Method Area)

线程共享区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。(被称为永久代,本质并不等价)

运行时常量池(Runtime Constant Pool)

存放编译器生成的各种字面量和符号引用,翻译出来的直接引用,在类加载后进入运行时常量池中存放。
具备动态性,运行期间也可以放入新的常量,例如String类的intern()方法。

直接内存(Direct Memory)

不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,也可能导致OutOfMemoryError异常。

对象的创建、布局与访问

对象的创建

创建对象仅仅通过一个new关键字,虚拟机的过程如下:
1、首先会检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查该类是否已加载、解析和初始化过。
2、如果没有加载,还要先执行相应的类加载过程。
3、虚拟机为新生对象分配内存。
4、内存分配完成后还要将分配的内存空间都初始化为零值,从而保证对象的实例字段在Java代码中可以不赋初始值就直接使用。
5、会接着执行方法,把对象按照程序员的意愿进行初始化。

=========================================================
虚拟机为新生对象分配内存的两种方法:
选择分配方式:Java堆是否规整,又是由所采用的垃圾收集器是否带有压缩整理功能决定。
(1)指针碰撞
Java堆中内存规整,分配内存时,指针向空闲的内存那边挪动与对象大小相等的距离;
(2)空闲列表
Java堆中内存不规整,列表上记录哪些内存是可用的,在分配时候从列表中找到一块足够大的空间划分给对象实例,并更新列表的记录。

=========================================================
线程安全性问题:并发情况下,可能出现对象A分配内存,指针还没来得及修改,对象B同时使用了原来的指针。解决方法如下:
(1)同步处理:加锁,保证更新操作的原子性。
(2)本地线程分配缓存(TLAB):把内存分配的动作按照线程划分在不同的空间之中进行,每个线程在Java堆中预先分配内存。

对象的内存布局

对象的结构:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
(1)对象头
a)存储对象自身的运行时数据(Mark Word):哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。
b)类型指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
(2)实例数据
a)存储顺序收到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。
b)相同宽度的字段总是分配到一起。
c)父类中定义的变量会出现在子类之前。
(3)对齐填充
a)占位符的作用。
原因:HotSpot VM要求对象起始地址必须是8bit的整数倍,对象大小必须是8bit的整数倍。

对象的访问定位

(1)使用句柄
优点是reference中存储的是稳定的句柄地址,对象移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
在这里插入图片描述
(2)直接指针
优点:速度快,节省了一次指针定位的时间开销。
在这里插入图片描述

发布了18 篇原创文章 · 获赞 0 · 访问量 425

猜你喜欢

转载自blog.csdn.net/qq_33805483/article/details/104105100