第二章
2.1概述
在java中将控制内存的权力交给了JVM,一旦出现内存泄漏和溢出的问题,不了解jvm是如何分配内存的是很难排查错误的
所以要了解jvm中的内存分配
2.2运行时数据区域
上图中蓝色区域是线程共享的,灰色区域是线程私有的
2.2.1程序计数器:
程序计数器是一块较小的内存空间
可以看作是当前线程执行字节码的行号指示器
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它时程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
在多线程中,线程是轮流切换、分配处理器执行时间的方式来实现的,因此每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称内存区域为线程私有的内存
总结:当前执行字节码的行号指示器、线程私有
不存在OutOfMemoryError的情况
2.2.2java虚拟机栈
与程序计数器一样,虚拟机栈也是线程私有的,它的生命周期与线程相同
当每一方法被执行的时候,java虚拟机都会创建一个栈帧用于存储局部变量表、操作栈、动态连接、方法出口等信息
每一个方法被调用直至执行完毕的过程,就对应着一个栈帧从入栈到出栈的过程
在虚拟机栈中有一个局部变量表,是用来存储编译时各种基本数据类型
要注意的是:此处存放的并不是具体的对象,可能是对象地址的指针、也可能是代表对象的句柄
总结:虚拟机栈的生命周期和线程相同,也是线程私有的
每个方法执行时都会创建出对应的局部变量表
要注意的是局部变量表并不是具体的对象,可能是对象地址的指针,也可能是代表对象的句柄
栈内存是可以动态扩张的,当栈内存不够的时候是会出现OutOfMemoryError异常
2.2.3本地方法栈
本地方法栈不同于虚拟机中的栈,但是与虚拟机中的栈发挥的作用非常类似
虚拟机栈为虚拟机执行java方法(字节码文件)服务
本地方法栈式为虚拟机使用本地方法服务的
总结:本地方法栈不是线程私有的
虚拟机栈是为虚拟机执行java方法服务的
本地方法栈是为执行虚拟机本地方法服务的
2.2.4 java堆
是虚拟机中管理的最大的内存,是线程共享的
java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配存储
将堆细分的目的是为了更好的回收内存,或者更快的分配内存
堆的大小既可以被实现成固定大小的,也可以是可扩展的,当前主流的虚拟机都是按照可扩展来实现的,如果在java堆中没有完成实例分配,并且堆也无法再扩展时,java虚拟机会抛出OutOfMemortyError异常
总结:是虚拟机中内存最大的一块区域,是线程共享的
几乎所有的实例对象都在这里分配内存
将java堆细分的目的是为了更好的回收内存、或是更快的分配内存
2.2.5方法区
方法区也是线程共享的,用于存储已被虚拟机加载的类的类型信息,常量、静态变量、即时编译器编译后的代码缓存等数据
java虚拟机规范堆方法区的约束时非常宽松的,除了和java堆一样不需要连续的内存和可以选择固定大小和可扩展之外,甚至还可以选择不实现垃圾收集
当方法区无法满足新的内存分配需求时,将抛出OutOfMemoryError异常
总结:方法区是线程共享的
是用来存储被虚拟机编译后的代码缓存等数据
用于存储类的常量、静态变量、信息等
堆的内存可以不连续,可以选择不实现垃圾回收
2.2.6运行时常量池
是方法区的一部分,class文件中除了有类的版本、字段、方法、接口等信息之外
还有一项信息就是常量池表,在类加载后存放到方法区的运行时常量池中
当常量池无法申请到内存时候会抛出OutOfMemoryError异常
2.2.7直接内存
并不是虚拟机运行时数据区的一部分,但是这部分也可能会导致OutOfMemoryError异常
这是一种基于通道与缓冲区的IO方式,它可以使用native函数库直接分配堆外内存,然后可以通过java中的一个对象来操作这块内存,可以避免在java堆和native堆中来回复制数据
内存不够是会导致OutOfMemoryError异常
总结:可以避免在java堆和native堆中来回复制数据
是一种基于通道和缓冲区的io方式
当内寸不够时会导致OutOfMemoryError异常
并不属于运行时数据区的一部分
2.3 HotSpot虚拟机对象探秘
hotSpot虚拟机在java堆中对象分配、布局和访问的过程
java原先是将源代码编译为字节码在虚拟机中执行,这样执行速度比较慢
而HotSpot是将常用的部分代码编译为本地代码,这样会提高新能
2.3.1对象的创建
在java中是如何创建对象的?
当java虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程
类加载检查通过之后,就是由虚拟机给对象实例分配内存,如何分配内存空间有两种方式:一种时指针碰撞,一种是空闲列表
指针碰撞:当java堆的内存是绝对工整的时候,所有被使用过的内存放一边,没有被使用过的内存放一边,在中间放着指针,创建对象时指针就会向未被使用过的内存移动和内存大小相匹配的距离
空闲列表:当java堆中不是绝对工整的时候,虚拟机就必须维护一个列表,在列表中记录上那些内存可以使用,在分配的时候就会在列表中找出一块足够大的空间分配给实例对象,并更新列表的记录
选择那种分配方式是由java堆是否工整决定的,而是否工整又由所采用的垃圾收集器是否带有空间压缩整理的能力决定
除了划分空间之外,创建对象在虚拟机中是一个非常频繁的操作,即时只是修改一个指针的位置,在并发的情况下也不一定是线程安全的
可能给A对象分配内存,指针还没来得及修改,对象B又同时使用原来的指针来分配内存
解决这个问题有两种方案:
1、对分配内存空间的动作进行同步处理,实际上虚拟机采用的是CAS配上失败重试的方式保证更新操作的原子性
2、另一种是爸内存分配的动作按照线程划分在不同的空间内进行,保证每个线程都有自己一下块内存,称为本地线程分配缓冲,将内存分配放到线程的本地缓冲区中分配
内存分配完成之后,虚拟机必须将分配到的空间都初始化为零值
还需要对对象进行必要的设置
例如这个对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用hashCode方法时计算)、对象的GC分代年龄等信息
将这些信息放到对象的对象头中
在上面工作都完成之后,虚拟机的角度来讲,一个新的对象创建成功,但是从java程序的角度来讲,对象创建才刚刚开始,此时对象还只是默认零值,需要执行class文件中的<init>()方法,执行<init>()方法后,对象就会按照我们的需求创建出来,这样才算时创建对象成功
总结:
遇到new指令,先判断类是否被加载
↓
然后给对象分配空间
此处会根据是否工整使用指针碰撞或是列表空闲
又会根据垃圾回收器来判断是否工整
↓
为了防止更新列表的时候会出现线程安全的问题
有两种解决机制:第一种是采用CAS和失败重试
第二种是在分配内存的线程创建一块自己的内存
将分配内存放到线程缓冲区中
↓
内存分配完毕需要将分配到的内存初始化
↓
接下来需要对对象进行必要的设置
例如那个对象是属于那个实例的
如何找到类的元数据信息
对象的哈希码
对象的GC分代年龄信息
将这些信息放到对象头中
↓
创建对象此时对虚拟机来讲是完成了
但对java来讲才刚刚开始
此时创建的对象的值为默认的零值
要想得到满足我们需求的值
需要执行class文件中的<init>()方法
↓
创建对象完毕
2.3.2对象的内存布局
在hotSpot中,对象在堆内存中的存储布局可以划分为三个部分:对象头、实例数据、和对齐填充
对象头:对象头包含两部分信息
第一类是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等,官方称为Mark Word
这部分数据在32位虚拟机和64位虚拟机中占用的内存大小是不同的
第二类是类型指针,即对象指向它的类型元数据的指针,java虚拟机通过这个指针来确定该对象是那个类的实例
实例数据:是对象真正存储的有效信息,即我们代码中所定义的各种类型的字段内容
对齐填充:当实例对象中的数据没有对齐会使用对齐填充补全,非必然存在
2.3.3对象的访问定位
创建的对象是为了使用,java程序会通过栈上的reference数据来操作栈上的具体对象,但是refence只是规定是一个指向对象的作用,并没有定义这个引用的方式 以及 访问堆中对象的具体位置
对象的访问定位方式有两种:句柄访问和之间指针访问
句柄访问:java堆中划分处一块内存来作为句柄池,引用变量中存储的就是对象的句柄地址,而句柄中包含对象实例数据和对象类型数据各自的具体地址信息。即句柄中包含两个之地,意对象实例数据,一个是对象类型数据(这个在方法区中,因为类字节码文件就放在方法区内)
2.4内存溢出实战
java堆:不断创建对象,并保证避免垃圾回收机制清理这些对象,随着对象数量的增加,会导致堆内存溢出
报错信息:OutOfMemoryError
虚拟机栈和本地方法栈溢出
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
如果虚拟机的栈内存允许动态扩展,当扩展栈容量我也发申请到足够的内存是,将会抛出OutOfMemoryError异常
方法区和运行时常量池溢出
本机之间内存溢出
第二章总结:
虚拟机的内存如何划分,那部分区域,什么样的代码和操作可能导致内存溢出异常