Jvm学习笔记(一)内存模型

JVM内存模型

java不需要用户手动去管理内存的释放,这大大解放了程序员的心智负担,jvm运行的核心绕不开他的内存模型,本章着重于jvm的内存模型分析。

程序计数器

程序计数器是一块较小的内存区域,主要作用是确定下一条需要执行的字节指令(java执行的是字节指令),它是程序控制流的指示器。

java的线程是基于操作系统的时分多路,所以对于一个处理器,在确定的时刻,只有一个线程处于执行状态,为了切换线程以后能够继续执行之前的字节指令,jvm为每一个线程分配了一块程序计数器的内存,即线程隔离。

程序计数器是唯一一个不会出现oom的区域。

Java虚拟机栈

Java虚拟机栈也是线程私有的,它描述的是Java方法执行的线程内存模型:每个方法被执行的时候,都会同步创建一个栈帧(Stack Frame),每个栈帧中存放:

  • 局部变量表

  • 操作数栈

  • 动态连接

  • 方法出口信息

    每一个方法被调用到执行完成,对应着一个栈帧在虚拟机中入栈到出栈的过程。每个方法所用到的变量都存放在局部变量表中,由于栈的空间是连续的,所以在编译期间必须知道栈空间分配的大小,所以,在局部变量表中存放的变量,必须在编译期间就知道空间的占用,故在局部变量表中,存放的是Java虚拟机的基础数据类型、对象的引用(指针)、returnAddress类型(函数的返回其实就是把返回结果的地址传递给外部)。

    变量在局部变量表中的内存占用是用Slot表示,double和long是64位,其余32位。

    如多线程请求的栈的深度超过jvm允许的最大深度,就会出现StackOverFlow异常(HotSpot无法动态扩容,所以只会出现这个异常)

Native本地方法栈

本地方法栈的作用和虚拟机栈的作用非常相似,只不过虚拟机栈执行的是java方法,本地方法栈执行的是原生方法。

Hot Spot直接把本地方法栈和虚拟机栈合二为一了。本地方法栈一样会出现StackOverFlow异常。

Java堆

在栈中的内存必须在编译期间就可以确定,那么那些在运行时才能确定内存空间大小、那些在栈帧回收以后还能继续存在的数据应该存放在哪呢?答案就是堆区。类似CPP的堆,java的堆事jvm中由jvm控制的最大的一块内存,这块内存的唯一作用就是用来存放对象或数组。(在cpp中,堆中的内存由用户手动申请并得到指针,同时由用户在合适的时机手动释放这部分内存,在java中,万物皆对象,对象的创建由jvm分配内存和传递指针,对象的回收由GC完成)。

Java堆是GC主要工作堆区域,gc将堆区内存分为了老年代、青年代等,这部分在后面GC章会具体讲到。

java堆是线程共享的,线程之间通过共享堆内存,来实现线程间通讯,堆区也是发生内存泄漏的内存区。当内存不足时,会出现oom异常。

方法区

和堆区一样,方法区也是线程共享的,主要用来存放被虚拟机加载的类中的信息,比如静态变量、常量等类信息。在jdk7以后,HorSpot就把方法区中的数据移动到了本地内存中一块叫元数据的内存区域。

当方法区内存不足时,会出现oom异常。

运行时常量

运行时常量时方法区中的一块内存,用来存放那些在运行时生成的常量,既非 class文件中常量池中所定义的常量,比如Stirng.intern()方法产生的常量。

直接内存

直接内存不是虚拟机运行时数据区的一部分,而是一块原生内存区域,jvm通过DirectByteBuffer对象作为这块内存的引用进行操作,避免了java和native之间频繁的复制数据导致的开销增大(也就是说jni中把数据存在在这块区域,在java中可以直接操作而无需使用clone的方式)。

HotSpot对象的创建流程和内存模型

创建流程

在java中,万物皆对象,接下来就分析一下HotSpot虚拟机中对象新建时的创建流程和存储的内存结构。

第一步:根据new的对象加载对应的class(假设目前还未加载),具体加载细节在后面章节分析。这里主要分析的是对象的内存模型。

第二步:为对象分配内存,在class加载完成以后,一个对象所需的内存其实已经确定下来了,只需要把一块空闲的内存从Java堆中划分出来即可。内存分配的方式有两种:在内存规整的时候(既可用内存和不可用内存被完全区分开时),可以采用移动指针的方式来快速划分出一块可用内存,这种方式被称为指针碰撞,在内存不规整的时候,虚拟机就必须维护一个列表,记录着可用的空间,当对象需要空间时候,在列表中找到对应的空闲区域划分出去并更新列表,这种方法被称为空闲列表。采用哪一方法和虚拟机的垃圾回收器是否带有空间整理功能有关。Serial、ParNew采用指针碰撞,CMS采用空闲列表。

在分配内存时,还有一个线程同步的问题,当一个线程在分配内存未完成时,新的线程请求分配内存,就会导致线程冲突,虚拟机采用cas来保证更新操作的一致性,此外,虚拟机也会为各个线程分配一块砖们的缓存区域,称为本地线程分配缓冲,每个线程优先在该区域更新,本地缓冲用完以后才会去公共区域申请内存,是否使用TLAB可以通过 -XX:+/-UseTLAB的参数设定。

第三步:内存区域分配完以后,会将内存区域内数据设置为零值,开启TLAB以后会在初始化TLAB的是就将内存区域初始化。

第四步:虚拟机对对象的进行必要的设置,比如这个对象属于哪个类,对象的哈希值,对象GC的分代年龄信息,这些信息会被存储在对象头中。

第五步:内存区域已经分配完成,接下来执行class文件中定义的<init>()方法,该方法会初始化对象默认值并执行构造方法,到这一步一个对象被真正的创造出来了。

内存模型

对象的内存结构分为对象头、实例数据、对齐填充。

对象头内存存储在对象自身的运行时数据,如哈希码、gc分代年龄,锁状态等,这部分数据长度根据虚拟机不同分为32或者64位,并且数据会复用空间。此外,对象头中还存放着类型指针,指向这个对象所对应的数据类型的元数据,如果是数组对象,还会存放这个数组的长度。

实例数据就是对象中存放自定义的数据的区域。

对齐填充:HotSpot虚拟机的内存管理要求地址必须是8的倍数,所以这部分区域用来填充不足8倍数的空间,仅仅起到占位的作用。

对象的访问定位

在栈上,对象是一个指向对象的引用而不是真实数据,这个引用具体怎么指针真实数据,有两种方法。

1、使用句柄访问,引用指向堆区的一个句柄,由句柄指向真实的内存区域。优点是内存真实地址移动或者变化时只需要改变句柄的数据而不用变化引用本身。

2、直接引用,直接指向堆中真实的内存区域。好处就是减少一次内存定位。

HotSpot主要采用第二种方案。

内存溢出

根据上面的内存模型可知,除了程序计数器之外,都会发生OOM(内存溢出)。下来分析各个区域的内存溢出问题。

JAVA堆

java堆内存溢出是最常见的内存溢出情况,出现堆溢出时,会报OutOfMemory错误,会进一步提示heap space。

堆中出现内存溢出的原因是对象不停的创建的同时,始终无法回收垃圾,也就是某个对象本应被回收却还是被认定为使用中,这种现象称为内存泄漏

根据GC Roots和可达性,内存泄漏时因为有gc root节点引用了一个本该被回收的内存,可以使用堆转储工具,堆Dump出来的快照进行分析,看具体哪一个root节点仍然引用了堆中的对象。以下是一些本人建议排查的重点(我是写android的,主要以android的层面):

1、对象间生命周期的差异,从更高的抽象层角度看,内存泄漏其实就是长生命周期的对象引用了短生命周期对象,而使得本应被回收的短生命周期对象迟迟不能被回收。一般来说这些引用都会藏的很深,不会让你直接发觉,这需要对各个组件之间生命周期有一定了解,拿Android举例,Activity属于频繁新建和回收的短生命周期对象,如果采用MVVM或者MVP的价格,P和VM是普通对象,普通对象的生命周期远远长于Activity(至少保持到GC),如果直接持有Activity而不做Detach或者weakref处理,就会出现页面回收以后普通对象始终持有activity,出现内存泄露。

2、匿名内部类/非静态内部类会隐式持有外部类的this引用,如果内部类中存在耗时操作便会出现内存泄漏。解决办法的改为静态内部类,如需使用外部引用,采用弱引用的方式传入。

3、各种组件的regist和unRegist。改进方式是在组件的destory时机进行反注册。

4、单例,单例的生命周期是整个应用,如果持有了某些引用,会导致这些引用始终无法释放。

虚拟机栈和本地方法栈

虚拟机对栈的创建本身是有数量限制的,一旦超过限制,就会出现StackOverflow的异常,(本身虚拟机规范允许虚拟机动态扩容,但是HotSpot并没有扩容功能)栈容量由 -Xoss参数设置。

我觉得这个问题比较容易出现在递归的问题上面,开发中尽量少用递归,要用也是尾递归,尽量用迭代代替递归。

方法区和运行时常量

运行时常量区会在运行过程中产生新的常量,如果空间用完,也会出现oom的问题,会在问题后面出现PermGen space的提示。

直接内存

DirectByteBuffer类通过反射获取unSafe实例进行内存分配。在调用UnSafe::allocateMemory()会出现手动抛出异常的问题。

排查这个问题,当dump的快照文件很小,程序中又直接或间接使用了DirectByteBuffer,就可以考虑是这个问题导致。

在Android中,图片的加载的缓存往往会在Ntaive层复制一份,我曾经就遇到过Flutter下图片在native缓存过多导致程序奔溃并且没有任何相关提示(整个android系统崩溃重启了)。

结语

本章主要是概括了一下java中内存相关的问题,主要参考了书本,也有一些是自己在开发中遇到的问题和感悟,谢谢观看,给个点赞

猜你喜欢

转载自blog.csdn.net/weixin_60227714/article/details/130005971
今日推荐