Java虚拟机--内存管理区域的深入学习笔记

在实际开发项目的当中,以android为例,我们时常会遇到关于Out Of Memory的的情况,这种情况尤其发生在我们进行图片加载的时候,如果代码中有提示我们如何问题所在,我们很容易可以尝试去解决这个问题,在一般的开源框架中,框架直接帮我们考虑了这方面的情况,不过我们需要知其所以然,这里,就详细从虚拟机的角度上介绍一下为什么会有OOM的产生。


运行时的数据区域

java虚拟机在执行java程序的过程中会把内存划分为若干个不同的区域,如下图所示:
这里写图片描述

程序计数器(PCR)

这是操作系统中的知识,程序计数器是一块较小的内存空间,可以认为是当前线程所执行的字节码的行动指示器,在虚拟机的概念模型中,字节码解释器工作时候是通过改变这个计数器的值选取下一条需要执行的字节码指令,如循环,异常处理,线程恢复等功能都需要依赖这个计数器来完成。

Java中的虚拟机的多线程是通过线程的轮流获得cpu资源来进行并发的操作,在任何一个确定的时刻,一个cpu处理器都只会执行一条线程中的指令。因此为了线程切换后能够恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程的PCR互不影响,独立存储,所以这类内存区域为内线程私有的。

如果一个线程正在执行的是一个java的方法,这个PCR记录的是正在执行虚拟机字节码指令的地址;如果正在执行的是native方法,PCR为空。

注意,这是Java虚拟机中唯一一个么有规定任何OOM情况的区域。


Java虚拟机栈

java虚拟机栈也是私有的,他的生命周期与线程相同。

虚拟机栈描述的是java方法执行的内存模型每个方法执行的时候会创建一个栈帧用来存储局部变量表,操作数栈,动态链接,方法出口等信息,每个方法调用过程都伴随着一个栈帧入栈出栈的过程。

在局部变量表中存放了编译时期的各种基本类型(java数据的基本类型),对象引用(reference),其实质上是一个引用指针,指向对中引用的对象,即对象的内存地址,其结构如下图所示:
这里写图片描述

局部变量表的内存在编译时期完成分配,当一个方法进入到栈帧的时候,其大小完全确定,在方法运行期间不会有变化。

在java中规定了虚拟机栈中的两种异常情况:

  1. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlow异常。
  2. 如果虚拟机栈可以动态拓展,如果拓展时候无法申请到足够的内存则会OOM。

本地方法栈

本地方法栈与虚拟机栈非常类似,主要区别在于其运行的是底层的native方法,而虚拟机运行的是java方法,我们习惯性将虚拟机栈与本地方法栈合称为一个栈,就是我们经常在GC机制的会提到的栈堆中的栈的概念。


Java堆

java堆是所有线程共享的内存区域,此地区唯一的目的就是存放对象实例,几乎所有对象的实例都在这里分配内存。
堆是GC管理的主要区域,从内存回收的角度来看,大部分Java的虚拟机都才有分代收集的算法来进行GC回收,在java堆中又可以对其细分为新生代和老年代,新生代中可以又细分为Eden区,两个Survivor区,如图所示:

这里写图片描述

GC机制运行过程的描述如下:
一共有三个空间,其中包含两个幸存者空间。每个空间的执行顺序如下:

  • 绝大多数刚刚被创建的对象会存放在伊甸园空间。

  • 在伊甸园空间执行了第一次GC之后,存活的对象被移动到其中一个幸存者空间。

  • 此后,在伊甸园空间执行GC之后,存活的对象会被堆积在同一个幸存者空间。

  • 当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。之后会清空已经饱和的那个幸存者空间。

  • 在以上的步骤中重复几次依然存活的对象,就会被移动到老年代。

由于新生代的新陈代谢速度非常快,所以在这里选择copy算法对其进行内存回收,对于老年代而言,则采用压缩算法,其具体步骤如下:

算法的第一步是标记老年代中依然存活对象。(标记)

第二步,从头开始检查堆内存空间,并且只留下依然幸存的对象。(清理)

最后一步,从头开始,顺序地填满堆内存空间,并且将对内存空间分成两部分:一个保存着对象,另一个空着(压缩)。

从内存分配的角度来看,线程共享的堆都可能划分出多个线程私有的分配缓冲区(TLAB),堆中可以出在物理上不连续的内存空间,只要逻辑上连续即可,像我们的磁盘空间一样。


方法区

方法区和堆一样,也是线程共享区域,它用于存储虚拟机加载的类信息,常量,静态量等数据,我们习惯性称之为”永久代”,java虚拟机规范对方法区的限制非常宽松,除了和堆一样不需要连续的内存和可以选择固定大小或者可拓展外,还可以不实现垃圾收集。

运行时常量池

这是方法区的一部分,class文件中除了有类的版本,字段,方法,接口等描述信息外,还有一项信息是常量池,用于存放编译时期生成的各种变量和符号引用,这部分内容将在类加载后进入方法去的运行时常量池中存放。

运行时常量池相对于Class文件常量池的另外一个重要特征是具有动态性,不要求常量一定只有编译器才能产生,可以再运行期间将新的常量放入池中,如String.intern()方法。


由上述的介绍大概了解了一下虚拟机的构造,下面研究一下对象创建的过程。

对象通常由关键字new来进行新建,当虚拟机遇到new指令的时候,首先将会去检查这个指令的参数是否在常量池中定位到一个类的符号的引用,并且检查符号引用代表的类是否已经被加载,解析,初始化过,如果没有,则必须执行相应的类加载过程。

类加载完成后,进行内存分配,对象所需的内存大小在类加载完成后可以完全确定,在java堆中为其划分一部分内存给这个对象,其中分配的方式有两种:

  1. 如果java堆中的内存是绝对规整的,即所有占用的内存放一边,没用的方放另一边,中间放个指针作为分界点的指示器,分配内存时只需要将指针像空闲区域移动即可,这种称为”指针碰撞”。
  2. 如果堆不是规整的,已使用的和空闲内存相互交错,虚拟机必须维护一张表,记录内存的使用情况,分配时找到足够大的内存分配给新建对象,这种称为”空闲列表”。

接下来,虚拟机要对对象进行必要的设置,如这个对象是哪个类的实例,如何能找到类的元数据信息,对象的哈希吗,对象的GC分代年龄等信息,这些信息存放在对象头中。

当上述过程都完成后,一个对象新建过程完成,但是从java程序的角度来看,对象创建才刚刚开始—-init方法还没有执行,所有字段为0,所以一般而言,执行new命令后会接着执行init方法,这样一个对象才真正的创建完毕。

流程图如下:

这里写图片描述

参考资料:
《深入理解java虚拟机》
http://importnew.com/3146.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值