JVM基础读书笔记
1.基本概念
JVM 内存区域主要分为:
线程私有区域–>【程序计数器、虚拟机栈、本地方法区】
线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束而创建/销毁
(在HotspotVM内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的生/死对应)。
线程共享区域–>【JAVA堆、方法区(元空间)、直接内存】
下图(网上找的)[Java虚拟机运行时数据区]
1.1 程序计数器
程序计数器(Program Counter Register) 是一块较小的内存空间.
每条线程都需要一个独立的程序计数器, 各线程之间计数器互不影响,独立存储, 所以这类内存区域称为线程私有.
如果线程正在执行一个Java方法, 这个计数器记录的就是正在执行的虚拟机字节码指令地址
如果线程正在执行一个本地(Native)方法, 这个计数器的值为空(Undefined)
注意:此区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。
1.2 Java虚拟机栈
Java虚拟机栈也是线程私有, 它的生命周期与线程相同.
每个方法被被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)
用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
这个区域可能有两种异常:
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
如果虚拟机栈可以动态扩,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
1.2.1 局部变量表
存放编译期可知的各种Java虚拟机的基本数据类型(boolean、 byte、char…),对象引用(reference)类型(它不是对象本身,可能是指向对象初始地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
这些基本数据类型在局部变量表中存储空间以局部变量槽(Slot)表示,其中64为的long double存储2个变量槽, 其余只占1个
1.3 本地方法栈
与Java虚拟机栈类型,区别在于本地方法栈则是为虚拟机使用到的本地(Native)方法服务.
有的虚拟机比如常用的Hot-Spot则把两个合2为1
1.4 Java堆
JVM管理内存最大的一块, 是被所以线程共享的。目的:存放对象实例,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域
1.5 方法区
即我们常说的永久代(Permanent Generation),
用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据.
HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区,
这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存,而不必为方法区开发专门的内
存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、
方法、接口等描述等信息外,还有一项是信息是字符串常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
**注:
在JDK1.6及之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代(位于堆内存中)
在JDK1.7 字符串常量池被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代
在JDK1.8 HotSpot 移除了永久代用元空间取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(堆外内存)**
两者最大的区别在于:元空间并不在虚拟机中,而是使用直接内存。(如下图–网上找的)
2 对象探索
2.1 new对象创建
遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载。
类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认),即在堆的空闲内存中划分一块区域
分配内存方式:
- 若Java堆中内存是绝对规整,所有被使用的内存放在一边,空闲的放在另外一边,中间放着一个指针作为分界点的指示器‘那所分配内存仅是把指针向空闲方向挪一段对象大小的距离这种分配方式叫指针碰撞(Bump The Pointer)
- 若Java堆中内存是并不规整,已被使用内存与空闲内存交错在一起,虚拟机就必须维护一个列表,记录哪些块是可用的,在分配的时候从列表中找一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式叫空闲列表(Free List)
- 并发情况下的分配内存分两种方案:
1. JVM采用CAS(compare and swap)加上失败重试的方式保证更新操作的原子性;
2. 将内存分配的动作按照线程划分在不同的空间中,每个线程在java堆中预先分配一小块内存,即(Thread Local Allocation Buffer ,TLAB),那个线程需要分配内存,就在那个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定;
内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。
执行 new 指令后执行 < init >() 方法,按照程序员意愿对对象进行初始化,这才算一份真正可用的对象创建完成。
2.2 对象的内存布局
在 HotSpot 虚拟机中,对象在内存中的布局分为三块区域:
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
2.1.1对象头
Hotspot虚拟机的对象头主要包括三部分数据:
- Mark Word(标记字段) 存对象的hashCode或锁信息
- Klass Pointer(类型指针)存储 到对象类型数据的指针
- Array length 数组长度(只有当前对象是数组才有)
Mark Word用于存储对象自身的运行时数据:哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,,它是实现锁的关键,后续学习Synchronized笔记详说。
Klass Pointer(类型指针)即对象指向它的类型元数据的指针,Java虚拟机通过这个指针来确定该对象是哪个类的实例。
Array length 如果对象是一个数组,那还需存储数组长度的数据(只有当前对象是数组才有).。
2.1.2 实例数据
对象真正存储的有效信息,即我们在代码中所定义的各种类型的字段内容,包含父类。
2.1.3 对齐填充
对齐不是必须存在的,它只起到了占位符(%d, %c 等)的作用。这就是 JVM 的要求了,
因为 HotSpot JVM 要求对象的起始地址必须是 8 字节的整数倍,也就是说对象的字节大小是 8 的整
数倍,不够的需要使用 Padding 补全。
2.3 对象的访问定位
访问到堆中对象的具体位置有下方两种访问:句柄、直接指针
2.3.1 句柄访问
如果使用句柄访问的话,Java堆中可能会划分出一块内存来作为句柄池,
reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址
好处:reference中存储的稳定的句柄地址,在对象被移动时(收集垃圾时经常被移动),只改变句柄中的实例数据指针,而reference本身不需要修改
2.3.1 直接指针
reference中存储的直接就是对象地址。
如果只是访问对象本身的话,就不需要多一次间接访问的开销。
就HotSpot VM而言,主要使用这一种方式