Java运行时内存模型

Android原生开发以Java为主,而Java程序都是运行在Java虚拟机(JVM)之上的且内存全权交给虚拟机去管理,那虚拟机的运行时内存模型是如何构成的?堆和栈,相信很多人都能脱口而出,但这只是对内存粗略的一种划分,其中”堆”对应内存模型的Java堆;”栈”则是指虚拟机栈,但是实际上Java运行时内存模型比这复杂多了,在SUN制定的Java虚拟机规范中,运行时内存模型,分为线程私有和共享数据区两大类,其中线程私有的数据区包含程序计数器、虚拟机栈、本地方法区,所有线程共享的数据区包含Java堆、方法区(又称为静态存储区,方法区内还有一个常量池),如下图所示 


其中私有内存区伴随着线程的产生而产生,一旦线程中止,私有内存区也会自动消除,因此我们在本文中讨论的内存回收主要是针对共享内存区。

1、线程私有数据区
1.1、程序计数器PC
程序计数器PC(Program Counter Register)是一块较小的内存空间,可以看作所执行字节码的行号指示器,字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,比如循环、跳转、异常处理等这些基础功能都需要依赖这个计数器来完成。Java的多线程是抢占式的调用,即任何一个确定的时刻,CPU都只会执行一条线程,而具体哪一条线程也是不确定的。所以为了线程切换后能够回到正确的执行位置,每个线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,因此这块区域是”线程私有”的内存。当线程正在执行一个Java方法时,PC计数器记录的是正在执行的虚拟机字节码的地址;当线程正在执行的一个Native方法时,PC计数器则为空(Undefined)。值得注意的是这一块的内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError的区域。

1.2、虚拟机栈
和程序计数器一样,虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型,每个方法(不包含native方法)执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息,当方法被执行时,方法体内的局部变量的基本数据类型和引用都存储于栈中都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限,Java虚拟机规范规定该区域有两种异常:

StackOverFlowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出 (递归函数)
OutOfMemoryError:当Java虚拟机动态扩展到无法申请足够内存时抛出 (OOM)
1.2 本地方法栈
本地方法栈(Native Method Stacks)和虚拟机栈差不多,前者是为虚拟机使用到的Native方法提供内存空间。有些虚拟机的实现直接把本地方法栈和虚拟机栈合二为一,比如主流的HotSpot虚拟机。 
异常(Exception):Java虚拟机规范规定该区域可抛出StackOverFlowError和OutOfMemoryError。

2、所有线程共享数据区
2.1、Java堆
Java 堆 又称 动态内存分配是应用程序在运行期通过new请求操作系统分配给自己的向高地址扩展的数据结构,是不连续的Java虚拟机管理的最大的一块内存区域,所以引用的对象实体、成员变量(包括基本数据类型,引用和引用的对象实体)、数组数据全部存储于堆中。( 因为它们属于类,类对象终究是要被new出来使用的,它们属于方法中的变量,生命周期随方法而结束)堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。用一句话总结堆的作用:程序运行时动态申请某个大小的内存空间。 Java 堆内存在不使用时将会由 Java 垃圾回收器来负责回收,所以也是垃圾回收GC(Garbage Collection)的主战场(GC堆,垃圾堆),在Java虚拟机中该区域可抛出OutOfMemoryError。


新生代—— HotSpot JVM把按照8:1:1的比例把新生代分为:1个Eden区和2个Survivor区(分别叫From和To)。通常新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC(新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在新生代区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC)后,如果仍然存活,将会被移到Survivor区,对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。 
因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,即新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

老年代——用于存放新生代中经过N次垃圾回收仍然存活的对象,如果某个对象经历了几次垃圾回收之后还存活,就会被存放到老年代中,而且老年代的空间一般比新生代大, 
老年代的垃圾回收称为Major GC。整堆包括新生代与老年代的垃圾回收称之为Full GC。通常当我们创建一个对象后,它会按照以下流程放到对应的内存区域: 


2.2、方法区
Hotspot JVM用永久带来实现方法区而已,但是永久代不等于方法区,方法区主要存放的是已被虚拟机加载的类信息、常量、静态数据、编译器编译后的代码等数据,这块方法区内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。Java虚拟机规范对这一块区域的限制非常宽松,不同的虚拟机实现也不同,相对而言垃圾回收在这个区域比较少的出现。根据Java虚拟机规范,当方法区无法满足内存分配需求时,会抛出oom异常。

2.3、运行时常量池
运行时常量池隶属方法区的一部分,用于存放编译器生成的各种字面量(与Java语言的常量概念相近,包含文本字符串、声明为final的常量值等)和符号引用(编译语言层面包括类和接口的全限定名、字段的名称和描述符和方法的名称和描述符)。运行时常量池除了编译期产生的Class文件的常量池,还可以在运行期间,将新的常量加入常量池,比如String类的intern()方法。

三、Java内存的管理概述
Java的内存管理就是对象的分配和释放问题。在 Java 中我们通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间,由GC根据具体回收机制执行对象的释放任务。把内存的管理分为了两条支线:程序完成内存的申请分配和 GC 完成内存的释放(Java程序员不需要通过调用函数来释放内存,但GC只能回收无用并且不再被其它对象引用的那些对象所占用的空间),这确实简化了Java程序员的工作。但同时也加重了JVM的工作。( Java 程序运行速度较慢的原因之一)因为GC 为了能够正确释放对象,GC为了更加准确地、及时地释放对象就需要监控每一个对象的运行状态(包括对象的申请、引用、被引用、赋值等,而释放对象的根本原则就是该对象不再被引用) 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值