深入理解JVM(一)-理解内存分配

来源:互联网 发布:VM怎样全屏在Linux下 编辑:程序博客网 时间:2024/05/19 04:51

JVM 内存模型

程序计数器:

程序计数器是一块较小内存空间,它可以看做是当前线程所执行的字节码的行号指示器。属于线程私有的

如果线程正在执行的是一个JAVA方法,计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是Native方法,计数器为空,该区域是唯一 不会抛OutOfMemoryError的区域。

说明:Native方法,native方法是指不是用java语言写的方法,简单这么理解,比如JAVA要访问底层,不能直接通过JAVA API访问,需要用C或者C++访问,具体的也不是太清楚,一般很少用到,暂且忽略

所以一般可以认为程序计数器就是正在执行的虚拟机字节码指令的地址

JAVA虚拟机栈:

属于线程私有内存区,生存周期与线程的生命周期一致。一般说的堆栈区域就是指这个,用于存放局部变量表,操作数栈,动态链接,方法出口等信息。

粗浅理解就是reference类(对象引用)

局部变量表存放的JAVA8大基础数据类型(long、double、byte、short、int、char、float、boolean)+对象引用(reference类)+returnAddress类型8大数据类型自然不用说,对象引用也用的多

 下面是贴的:

 returnAddress类型会被Java虚拟机的jsr、ret和jsr_w指令所使用,returnAddress类型的值指向一条虚拟机指令的操作码。与前面介绍的那些数值类的原生类型不同, returnAddress类型在Java语言之中并不存在相应的类型,也无法在程序运行期间更改returnAddress类型的值。

此区域会抛出两类异常:

StackOverflowError:如果线程请求的栈的深度大于虚拟机所允许的深度。一般出现可能是递归调用太深。

OutOfMemoryError:扩展的时候无法申请到足够的内存。---一般出现是的可能是申请的ArrayList 或者Map放的东西太多,扩展导致内存不足。

Native方法区:

类比JAVA虚拟机栈,不同的是Navtive方法区服务对象是Native方法,JAVA堆栈服务对象是JAVA方法。


JAVA堆区:

JVM管理内存区中最大的一块,所有线程共享的内存区。虚拟机启动的时候创建,可以理解成进程启动的时候这一块区域就创建了。这块地方存放对象实例,几乎所有对象实例都是在此处分配内存。

JAVA堆是垃圾收集器管理的主要区域。也被称为"GC堆 Garbage Collected Heap " 中国式翻译---垃圾堆,yeah

垃圾收集基本采用  分代收集算法, 分为老年代,新生代。再细分可分为:Eden空间、From Survivor 空间、To Survivor 空间。(暂时不懂,后续看了再补充)

从内存分配角度看,堆中可能分配出多个线程线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)-----重要

异常:

OutOfMemoryError:如果堆内没有完成内存实例分配,且堆内无法进行扩展时,可能出现在查询数据库返回大量数据时,或者其他N种情况。


方法区(Method Area):

属于所有线程共享区域,用于存储加载的类信息,常量,静态变量、即时编译器编译后的代码等。小名叫非堆(Non-Heap),用于与堆区分。

开发者常把该区域成为“永久代” Permanent Generation 本质上不等价。


以上是对JVM内存模型的简单理解


下面是对象的创建过程,以及对象的内容信息等。

对象再JAVA虚拟机中的创建过程:

创建对象可以有NEW 克隆 反序列化等等。以NEW 为例,当NEW  一个对象时,JAVA虚拟机

首先检查这个 指令( NEW OBJECT) 的参数(类名吧大概) 是否能在常量池中找到该类的符号引用,简单说大概是找找看有没有这个类。

找到类之后接下来是分配内存,等同于将一块固定大小的区域从堆中划分出来:

假设堆内存是绝对完成,并且连续的一整块物理内存,所有用过的内存放在一边,没用过的放另外一边,中间放着一个指针作为分界点的指示器。那所分配的内存就仅仅是把指针往空闲的一边挪动与对象大小等大的一段距离这种分配方法称为:指针碰撞

假设堆内存不是规整的,已使用的内存与空闲内存相互交错,虚拟机就必须维护一个列表,记录那些内存是可用的,并且找到一段足够大的区域分配给对象并更新列表。这种分配方法称为:空闲列表

使用什么方式分配内存,取决于堆内存是否规整,堆内存是否规整取决于又与采用的垃圾收集器是否有压缩整理功能决定。

如果对象创建频繁,并发情况下频繁创建对象,分配内存方式就不是线程安全的,对此有2种解决方式:

第一对分配内存空间的动作做同步处理----实际上虚拟机使用CAS 加上失败重试保证原子性---虚拟机已经实现,我们只是了解,并不需要我们做什么。

第二种把内存分配的动作按线程划分到不同空间进行,即每个线程在JAVA堆中预先分配一小块内存,成为本地线程分配缓冲区(Thread Local Allocation Buffer),不同线程在自己的TLAB上分配,当TLAB分配完时,才需要进行锁定,讲道理,剩下的应该是按照第一种方式进行分配。虚拟机是否使用TLAB 使用参数   -XX:+/-UseTLAB 来设定


对象的内存布局:

对象在内存中的布局分为三块区域:对象头(Header)、实例数据(Instance Data) 和对齐填充(Padding)

其中对象头包括2部分:第一部分称为Mark Word ,32和64位虚拟机分别为32bit 和64bit,用于存储对象自身的运行时数据,例如哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID、偏向时间戳等,除了Hashcode跟分代年龄,其他基本一无所知。

第二部分为类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来去掉这个对象是哪个类的实例。


实例数据存储的是有效信息,即程序定义的各种变量类型的字段内容。这部分数据的存储顺序受虚拟机分配策略参数(FieldAllocationStyle)和字段在源码中的定义顺序的影响,虚拟机的默认分配策略为longs/doubles ints shorts/chars bytes/booleans oops(Ordinary Object Pointers)  可以看出 字段宽带相同的分配在一起 满足这个前提下,父类的会在子类前

对齐填充并不是一定存在的,只是起到占位符的作用,HotSpot VM自动内存管理系统要求对象的起始地址必须是8字节的整数倍,就是说对象的大小必须是8字节的整数倍,对象头是8字节的整数倍,但是实例数据是动态的,如果不是8的整数倍,则有这部分补齐。



 对象的访问定位:

第一种:

句柄访问方式:java堆中将划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息

图片借用:


第二种:

指针访问方式:reference变量中直接存储的就是对象的地址,而java堆对象一部分存储了对象实例数据,另外一部分存储了对象类型数据

这两种访问对象的方式各有优势,使用句柄访问方式最大好处就是reference中存储的是稳定的句柄地址,在对象移动时只需要改变句柄中的实例数据指针,而reference不需要改变。使用指针访问方式最大好处就是速度快,它节省了一次指针定位的时间开销,就虚拟机而言,它使用的是第二种方式(直接指针访问)








0 0
原创粉丝点击