运行时数据区域
运行时数据区域
*****************************************************
* 方法区* 虚拟机栈 本地方法栈 *
* * *
* [共享]* 【私有】 *
* 堆 * 程序计数器 *
* ***** *********************************************
* *
* 执行引擎 本地方法库接口 本地方法库 *
* *
* ***************************************************
程序计数器
描述 当前程序执行字节码的行号指示器,唯一一个不会发生OutOfMemoryError的内存区域
1.虚拟机字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖于程序计数器;
2.由于Java虚拟机的多线程是通过线程轮流切换,分配CPU执行时间的方式实现的 ,
在任何一个时刻,一个处理器都只会处理一条线程中的指令,
因此,为了恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,
线程直接互不影响,独立存储,所以程序技术器是线程私有内存区域
Java虚拟机栈
描述 Java虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行时都会
创建一个栈帧,每个方法的执行完成的过程,就意味着一个栈帧在虚拟机栈中从入栈到出栈的过程
1 虚拟机栈是线程私有的
2 栈帧一般存储局部变量表,操作数栈,动态链接,方法出口信息
3 局部变量表一般存放Java基本数据类型【boolean,byte,char,short,int,long,float,double】,对象引用和返回值地址引用
4 如果线程请求的栈深度大于虚拟机允许的最大深度,则会抛出StackOverFlowError异如果申请不到足够的内存
则会出现OutOfMemoryError异常
本地方法栈
描述 跟虚拟机栈基本相似,只不过本地方法栈是为Native方法服务的,sun的hotspot将两个栈合并在了一起
1 跟虚拟机栈一样,也会出现StackOverFlowError异常,如果申请不到足够的内存
则会出现OutOfMemoryError异常
2 同样也是线程私有的
Java堆
描述 Java堆是Java虚拟机所管理的内存中最大的一块内存区域,Java堆被所有线程共享
1 Java堆是线程共享的
2 Java堆是垃圾收集器管理的主要内存区域
3 几乎所有的对象实例以及数组都在堆上分配,但是随着JIT编译器的发展与逃逸分析技术的逐渐成熟,标量替换,栈上分配,使得所有对象在堆上分配不是那么绝对
4 现代垃圾收集器,根据对象生命周期的不同,采用的基本都是分代收集算法,分为新生代和老年代,
新生代又包括Eden空间,from Survivor空间,to survivor空间
5 为了提高分配效率,线程共享的Java堆空间,可能会划分出多个线程私有的分配缓存区【TLAB Thred Local Allocation Buffer】
6 java可以处于物理上不连续内存空间中,只要逻辑上连续即可
7 Java堆的实现可以按照固定大小和可扩展来实现,目前主流的虚拟机都是按照可扩展来实现的
通过 -Xms -Xmx 控制堆的最大值和最小值,如果无法扩展就会抛出OutOfMemoryError
方法区
描述 方法区与Java堆一样也是被线程共享的,主要用于存贮被虚拟机加载的类信息,常量,静态变量,即时编译后的代码等数据
1 方法区别名 Non-Heap
2 一般我们也称方法区为永久代
3 方法区有一个 -XX:MaxPermSize参数控制方法区内存上限,触碰到内存上限就会抛出OutOfMemoryError
4 1.7已经将原本存放在永久代的字符串常量池移出
5 1.8 已经将方法区改成了MetaSpace,直接用的NativeMemory
运行时常量池
描述 运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译期间生产的各种字面量和符号引用,这部分内容,将在类加载后进入方法区的运行时常量池中存放,翻译出来的直接引用也会存放在运行时常量池中
1 运行时常量池与class文件常量池的一个重要区别就是运行时常量池具备都太性,Java语言不要求常量一定只有在编译器产生,
运行期也可能将新的常量放入常量池,比如String的intern()方法
2 因为运行时常量池是方法区的一部分,所以申请不到内存时也会抛出OutOfMemoryError
直接内存
描述 直接内存不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分区域被频繁使用,也会出现OutOfMemoryError
1 直接内存的分配不会受到Java堆大小的限制,但是会受到本机总内存的限制
HotSpot虚拟机对象
对象的创建
描述 创建对象(克隆 反序列化等)通常仅仅是一个new关键字而已,而具体的创建过程都是透明的,下面就是创建对象的一个简单过程介绍
1 遇到new指令时,首先检查指令参数是否能在常量池中定位到一个类的符号引用
2 存在符号引用,检查符号引用对应的类是否被加载,解析,和初始化过
3 检查通过后,为新生对象分配内存,根据垃圾收集器对Java堆内存区域的管理的方式的不同,一般分配方式分为两种,指针碰撞和空闲列表,指针碰撞要求内存空间连续,所以带有内存空间整理的收集器会使用指针碰撞
分配内存,而像cms这种 Mark-Sweep算法,通常采用的是空闲列表,而Serial ParNew等带Compact通常的收集器,采用的是指针碰撞方式分配内存
4 由于虚拟机创建对象是很频繁的行为,高并发情况下并不是线程安全的,所以需要通过同步保证分配内存的线程安全,
一般有两种方法,一种是对分配内存空间的动作做同步处理,虚拟机采用的是Cas配上失败重试保证更新操作的原子性,
另一种是为线程预分配本地线程分配缓冲区,只需要在缓冲区使用完之后,分配新的缓冲区时使用同步锁定,
是否使用分配缓冲区可以使用参数 -XX:+/- TLAB 来设置
5 内存分配完后,需要为对象初始化零值
6 零值初始化后,就是对象初始化 ,这样对象才真正创建完成
对象的内存布局
描述 对象在内存中存储的布局分为3块区域,对象头,实例数据,对齐填充
对象头包含两部分信息,一部分是对象运行时的自身数据,如哈希吗,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳,另一部分是类型指针,用于确定是哪个类的实例
对象头
第一部分数据长度再32位和64位虚拟机中分别为32bit和64bit,这部分数据也被称为Mark Word ,在32bit的空间中,MarkWord内容如下,根据情况不同 存储的内容也不同
存储内容 | 标志位 | 状态 |
---|---|---|
对象hash码 对象分代年龄 | 01 | 未锁定 |
指向所记录的指针 | 00 | 轻量级锁 |
指向重量级锁的指针 | 10 | 重量级锁 |
空,不需要记录信息 | 11 | gc标记 |
偏向线程ID 偏向时间戳 对象分代年龄 | 01 | 可偏向 |
第二部分是类型指针
指向对象的类元数据指针,可以确定对象属于哪个类的实例,如果是Java数组 还必须有一块用于记录数组长度的数据
实例数据
实例数据部分是对象真正存储的有效信息,Hotspot VM 内存管理系统需要对象的内存起始地址必须是8字节的整数倍,如果不是需要对齐填充
对象的访问定位
描述 创建对象,最终是为了使用对象,Java程序是通过栈上的refenrence数据来操作堆上的具体对象的
主流的访问方式
1.使用句柄[比较稳健,对象呗移动,只需要改变句柄中的实例数据指针]
2.直接指针[减少了一次句柄到实际地址的定位,会更快]
对象访问在Java中非常频繁,为求速度 Java使用的是直接指针,
常见内存溢出
- Java堆溢出
- 虚拟机栈和本地方法栈溢出【递归可能导致】-Xss128k 线程分配的占空间大小
- 方法区和运行时常量池溢出【string 手动插入到运行时常量池可能导致】
- 本机直接内存溢出【nio可能导致】-XX:MaxDirectMemorySize=10M 可指定大小