《深入理解java虚拟机》读书笔记(一)---java内存
吐槽
最近上课无聊的时候还是赶紧看下java虚拟机这本书吧,因为最近要看热修复技术这块,但是直接上手发现好的原理性的东西还是不知道唉,还是先看下《深入理解java虚拟机》这本书吧。
java的内存
之前和一个学C++的朋友一起聊天的时候,他给我说,C++的内存什么的有的要程序猿自己去分配,分配完了然后还有去自己手动去回收这块,当时我感觉他们要考虑的东西真的多,java好像从来没有让我考虑这块,原因也很简单。
java虚拟机自动管理内存
虽然虚拟机帮我们管理了内存,但是我们还是要看下这块他怎么去分配内存的,怎么样去处理的
java虚拟机运行时候数据区
其中所有线程共享的区域是:方法区,堆
线程隔离私有区域:程序计数器,虚拟机栈,本地方法栈
程序计数器
- 线程私有,生命周期和线程相同
- 占有的内存很小
- 程序的分支,循环,跳转,异常处理,线程恢复都依赖这个计数器
- 如果程序正在执行一个java方法,计数器记录正在执行的字节码指令
- 如果正在执行是行一个Native方法,计数器值为空。
- 字节码解释器工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令的。
- 为了线程切换后能恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,线程之间互不影响
//感觉这个就是类似操作系统里面的那个中断技术
被native关键字修饰的方法叫做本地方法,本地方法和其它方法不一样,本地方法意味着和平台有关,因此使用了native的程序可移植性都不太高。另外native方法在JVM中运行时数据区也和其它方法不一样,它有专门的本地方法栈。native方法主要用于加载文件和动态链接库,由于Java语言无法访问操作系统底层信息(比如:底层硬件设备等),这时候就需要借助C语言来完成了。被native修饰的方法可以被C语言重写。
java虚拟机栈
- 线程私有,生命周期和线程相同
- 描述java方法执行的内存模型的,每个方法都会创建一个栈帧
- 一个方法调用直到执行完成的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 栈帧是方法运行时的基本数据结构,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 粗糙划分内存为堆和栈时,栈其实指的就是虚拟机栈中变量表部分。变量表里面存放的编译期可知的各种基本的数据类型,对象引用,有可能是句柄
- 局部变量表在编译期间完成内存分配,方法运行的时候不会改变局部变量表的大小
本地方法栈
- 本地方法栈则是为了虚拟机使用Native方法的
- 和虚拟机栈一样类似StackOverflowError异常和OutOfMemoryError异常。
java堆
- 所有的线程共享,虚拟机启动时候创建
- 存放对象实例,几乎所有的对象实例,数组都在这里分配内存
- java堆是垃圾收集器管理的主要区域
- java堆还会细分为,新生代和老生代
- 内存分配角度Java堆可细分出多个线程私有的分配缓冲区(TLAB)
- Java堆物理可不连续,逻辑连续即可,可实现成固定大小,也可实现成可扩展
- 规定OutOfMemoryError异常,当堆中没有足够剩余内存完成实例分配,并且堆无法再扩展时抛出此异常
直接内存
- 并不是JVM运行时数据区的一部分,也不是JVM规范定义的内存区域,但是被频繁使用
- 受本机总内存大小和处理器寻址空间的限制,各个内存区域总和大于物理内存限制时会导致动态扩展时出现OutOfMemoryError异常
- 避免了在Java堆和Native堆间复制数据,在一些场景下会显著提高性能
方法区
- 所有线程共享
- 储存已经被虚拟机加载的类的信息,常量,静态变量,及时编译器后的数据
- 使用HotSpot虚拟机的人可能会将方法区称为永久代。
- 方法区物理内存同样可不连续,可选择固定大小也可选择可扩展大小。甚至可以选择是否实现垃圾回收
- 虽然是java堆的一个逻辑部分,但是还是要区分开
运行时常量池
- 运行时常量池是方法区的一部分。
- 存放编译期生成的各种字面量和符号引用(一般还会有直接引用)。
- 这部分内容在类加载后进入方法区的运行时常量池存放。
- 运行时常量池具有动态性。
- 运行期间也可将新的常量放入池中
HotSpot虚拟对象
HotSpot虚拟机是JDK默认虚拟机。
以常用HotSpot和内存区域Java堆为例,探讨HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。
主要就是三部分
- 对象的创建
- 对象的内存布局
- 对象的访问
对象的创建
这块其他的都好理解,就第三步,分配内存的那步很复杂
当类加载检查完成之后,虚拟机就为新创建的对象分配内存,对象的大小在类加载完成之后就可以确定。
为对象分配空间的任务等于把一块确定的大小的内存从java堆划分出来
根据java堆中的内存是否是规整的,分成两种方法
1 java堆中的内存是规整,采用指针碰撞的方法
- 因为内存是规整的,所以把用了的内存放到一边,没用过的内存放到另一边,中间放个指针当界线
- 内存分配的时候,就把那个指针向空闲区域方向移动一段和对象大小相等的距离
2 java堆中的内存不是规整的,采用空闲列表的方法
- 因为内存不是规则的,所以采用不了指针碰撞的方法
- 虚拟机维护一个表,记录那些内存块可以用,分配的时候就按那个分配,然后跟新列表
选择那个分配的方式是由java堆是否规则决定的,而堆是否规整是看采用的垃圾收集器是否带压缩整理功能决定的//标记整理算法
对象的布局
这块就纯属是记了emmmmmm
对象头的运行时数据包括:
- 锁状态标志
- HashCode
- GC分代年龄
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
对象头的类型指针
- 对象指向它的类元数据的指针。 虚拟机通过这个指针确定该对象是哪个类的实例
- 不是所有虚拟机都要实现保存类型指针,也就是说查找对象的元数据信息并不一定要经过对象本身。
对象头的数组大小记录
- 如果对象是一个Java数组,那还需要一块记录数组长度的数据。
- 普通Java对象可以通过元数据信息确定对象大小,但是从数组元数据中无法确定数组大小。
实例数据
- 真正存储对象有效信息的,各类型的字段内容。
- 存储顺序受到虚拟机分配策略参数和字段在代码中的定义顺序影响
- 父类中定义的变量会出现在子类定义的之前
对齐填充
- 不是必然存在,也没有特别含义。 仅仅是占位符的作用,实现内存对齐。
- HotSpot自动内存管理系统要求对象起始地址必须是8字节的整数倍。
对象大小也就要求是8字节整数倍,对象头正好是8字节整数倍,实例数据如果不够8字节整数倍,就需要这部分来补全。
对象的访问
Java程序需要通过栈上的reference来操作具体对象。
两种方法
第一种:使用句柄
- Java堆中划分出一块内存作为句柄池,每个对象的句柄存储着对象的引用,reference存储对象句柄的地址。
- 句柄存储包括在Java堆中的实例数据的引用和在方法区的类型数据的引用。
- 优点在于renference中存储的是稳定的句柄地址。对象移动时只改变句柄中的值,renference本身不需要改变
第二种:直接指针
- reference直接指向Java堆中的对象实例数据,而类型数据的引用存储在实例数据中。
- 对象布局就需要考虑如何放置类型数据信息了,也就是前面说到的对象头中的类型指针。
- 优点在于省去一次指针定位的时间开销,速度更快。
总结
就是简单把书上学的整理下,了解下java里面的内存的分别和一个对象的创建,内存分布和访问的方式。