基于《深入理解JAVA虚拟机》第三版的第二章学习总结:
目录
2.1 运行时数据区域
2.1.1 程序计数器
程序计数器可以看做线程所执行的字节码的行号指示器,字节解释器工作就是通过改变计数器的值选取下一条需要执行的字节码指令;
关键点:
1.各线程私有
2.唯一一个不存在内存溢出的区域
解释:
1.多线程时,线程切换后,调用自己私有的程序计数器,恢复到正确的执行位置;才能继续执行正确的字节指令;
2.查阅资料之后,发现有说法显示该区域是存在溢出的可能性的,但是目前的代码行数还远远达不到这样的条件,可以直接忽略;
而《java虚拟机规范》中对该区域没有规定任何OutOfMemoryError情况;
2.1.2 虚拟机栈
关键点:
- 各线程私有
- 生命周期与线程相同
- 每个方法被执行时,虚拟机同步创建一个栈帧,栈帧存储局部变量表(基本数据类型、对象引用returnAddress类型)、操作数栈、动态连接、方法出口等信息;
- 这些数据在局部变量表中的存储空间以局部变量槽表示,64位的long与double类型占用两个变量槽,而其他数据类型只占一个变量槽;
- 在编译之后,方法的局部变量表的空间大小是完全确定的,不会在执行中发生改变;
- 栈溢出抛出StackOverflowError异常;
- Hotspot虚拟机的栈不可以动态扩展,而Classic虚拟机可以;
方法执行时,栈帧压入栈中,所以方法体中的方法执行时也会继续压进栈中,而根据栈的特点,最先进栈的栈帧是最晚出栈的;
2.1.3 本地方法栈
关键点:
虚拟机栈是执行java方法,而本地方法栈是为虚拟机使用本地方法服务;
2.1.4 java堆
关键点:
- 所有线程共享的区域
- 虚拟机中内存最大的一块 ,也是可以通过参数扩展的
- 发生溢出时,报OutOfMemoryError异常
- 对象实例以及数组都是在堆上分配的
- 整体分新生代与老生代比例为1:2,新生代又分三个区域:Eden空间,From Survivor空间,To Survivor空间,比例为:8:1:1
- 一般小的,周期短的对象示例以及数组在新生代的三个区域中的其中之一,而大的、周期长的存放在老生代中;
- 一般而言,一个对象实例之后先是到Eden区域,然后GC回收扫描之后,如果存活就转移到From Survivor空间,GC再次回收扫描之后还存活就再转移到To Survivor空间中,而之后每次GC扫描之后仍然存活下来,对象实例的年龄就加1,之后到达一定值(如16)之后,就转移到老生代区域中去;
有关栈申请空间OOM异常的说明
关于本地方法栈与虚拟机栈,《Java虚拟机规范》中,描述了这样两种异常:
- 如果线程申请的栈深度大于虚拟机允许的最大深度,将抛出StackOverflowError异常;
- 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OOM异常;
- 之前的classic虚拟机支持虚拟机栈动态扩展,在动态扩展的情况下,也是有可能使用因为方法无限递归到最后申请不到足够的空间而抛出OOM的;
- 但是Hotspot的虚拟机栈是不能够动态扩展的,而一般抛出异常时,通常情况下都是StackOverflowError的;
- jvm的线程栈申请的内存空间属于堆外内存,是向操作系统申请的,Hotspot下的,理论上只要系统的内存很小,然后将-Xss设置很大,然后死循环创建调用线程方法,就会出现OOM的情况,因为栈申请不到足够的内存空间了;
但是注意!!!,真的真的不建议轻易尝试,执行的时候CPU占用率达到100%,风扇疯狂转动,而且系统会出现假死的情况,感觉特别伤机器;而且如果你的机器内存很大,就更加不建议了,你一边等就会一边慌;
2.1.5 方法区
- 所有线程共享的区域
- 内存不足时抛出OutOfMemoryError异常
- 储存的信息:1.已加载的类信息(loader),2.常量,3.静态变量,4.编译后代码缓存
- 该区域的内存回收的主要目标是:常量池回收和对类型的卸载(unloader);
- 可以选择不进行垃圾回收
2.1.6 运行时常量池
- 方法区的一部分
- 内存不足时抛出OutOfMemoryError异常
- 运行期间可以将新常量放入池中,常见的有String类的intern()方法
2.1.7 直接内存
- 不属于虚拟机管理范畴,所以不受堆大小限制,直接使用分配的系统内存(堆外内存);
- 系统内存不足时抛出OutOfMemoryError异常
- 常见的有JDK1.4出现的NIO类,直接使用系统内存;
2.2 对象分配、布局、访问全过程
2.2.1 对象创建
只讨论普通java对象,不包括数组和class对象等;
- 首先是new指令
- 在常量池定位并检查引用的类是否被加载、解析和初始化过;没有则进行类加载过程;
- 为对象分配内存
- 除对象头之外,分配内存初始化为零;
- 对象头信息设置(所属类、查找元数据途径、hash码、对象GC分代年龄)
内存分配:
- 指针碰撞:指针将内存区域分成两个部分,一部分是已用区域,另一部分是未使用区域;适合于内存是规整的,如系统的内存分配;
- CMS空闲内存分配列表;
多线程下保证线程安全的2种分配方案:
原因:可能在同一时间,多个线程同时分配内存,造成指针来不及修改,内存分配错乱的情况出现;
方案一:CAS算法 + 失败重试,保证更新操作的原子性;
方案二:每个线程预分配一小块内存,即本地线程分配缓冲(TLAB),在本地缓冲区使用完,才需要同步锁定申请新的;(设置:-XX:+/-UseTLAB)
2.2.2对象内存布局
主要可以分成三个部分:1. 对象头,2.实例数据,3.对齐填充
1.对象头
对象头部分分成两类信息:
-
第一类:用于存储对象自身的运行时数据——官方称之为:“Mark Word”
-
第二类:类型指针,指向它的类型元数据的指针,表明属于哪个类;如果对象是数组,还会有一块记录数组长度的数据;
2.实例数据
储存类以及父类继承下来的各种类型的字段内容,hotspot默认将相同宽度的字段放在一起;
3.对齐填充
仅仅起着占位符的作用,不是必然存在的;
2.2.3对象的定位访问
程序通过栈上的reference数据来操作堆上的具体对象;
主流的访问方式有:1.使用句柄;2.直接指针
1. 使用句柄
reference——>句柄池——>对象实例数据/对象类型数据
2. 直接指针
reference(存储对象地址)——>实例对象