android 虚拟机内存
JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;
方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。
科普一下JVM栈内存的相关知识:
JVM的栈包括两部分,虚拟机栈和本地方法栈,各JVM的最终实现不一样,例如HotSpot是将两个栈合二为一,不区分。
Java虚拟机栈(Java Virtual Machine Stacks)
线程私有的,生命周期与线程相同。每个方法在执行的同时,会创建一个栈帧(Stack Frame)。方法开始执行时,压入这个栈帧,方法执行完成,这个栈帧就出栈。递归调用方法如果递归的深度过深,就会出现栈溢出(StackOverflowError)异常,值得就是这个地方的栈溢出。如果这个区域允许动态扩展,但是无法申请到足够的内存,就会内存溢出(OutOfMemoryError)。
设置参数:
-Xss 调整栈内存容量
本地方法栈(Native Method Stack)
线程私有。与上面提到的Java虚拟机栈(Java Virtual Machine Stacks)相对应,Java虚拟机栈为Java方法服务,本地方法栈为Native方法服务。Sun HotSpot将这两个栈合二为一。栈溢出和内存溢出规则也一样。
设置参数:
-Xoss 调整栈内存容量(Hot spot虚拟机无效)
虚拟机栈和本地方法栈的-Xss(-Xoss)参数影响了栈的深度,当抛出StackOverflowError异常说明-Xss参数设置过小。不停递归调用会抛出此异常。定义大量本地变量增加方法帧本地变量表的长度也会抛出此异常。
栈里面放啥?
栈中进行压栈出栈的叫栈帧,栈帧由三部分组成:局部变量区、操作数栈和帧数据区(保存一些数据来支持常量池解析、正常方法返回以及异常派发机制)。
局部变量区:
局部变量区是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了方法所需要分配的最大局部变量表的容量。局部变量表的容量以变量槽(Variable Slot)为最小单位(字节数组组成)。
JVM规范规定:
每个Slot都应该能存放一个boolean,byte,char,short,int,float,refrence,returnAddress类型的数据,对于非基本类型的局部变量,在局部变量区只存储引用,具体对应的值还是在堆中,通过reference表示。returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。大部分资料表示Slot的长度为32位,不排除在64位系统中Slot的长度为64位(未考证),对于64位的数据类型,如long、double,虚拟机会以高位在前的方式为其分配两个连续的Slot空间。由于局部变量表建在线程的堆栈上,是线程私有的数据,无论读写两个连续的Slot是否是原子操作,都不会引起数据安全问题。虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的Slot数量。如果32位数据类型的变量,索引N就代表了使用第N个Slot,如果是64位数据类型的变量,则说明要使用第N个和N+1两个Slot。对于非static方法,局部变量的第1个Slot默认为方法所属对象实例的引用,通过this访问。
操作数栈:
操作数栈用于存放方法中计算时的中间结果,也是被组织成以字长为单位的数组。但不是通过数组下标来访问,而是通过压栈出栈来访问,操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int(原因是由于Java虚拟机缺乏对byte、short和char类型的,某些指令只支持int类型)。
帧数据区:
除了局部变量区和操作数栈外,Java栈帧还需要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些数据都保存在Java栈帧的帧数据区中。
常量池解析存的是一个引用,这个引用指向栈帧当前运行方法所在类的常量池,通过这个引用支持动态链接。
除了处理常量池解析外,帧里的数据还要处理Java方法的正常结束和异常终止。如果一个方法有定义 try-catch 或者 try-finally 异常处理器,那么就会创建一个异常表,每个异常表入口包含四个信息:
stack_data_area
JVM 在 try 住的代码区间内如有异常抛出的话,就会在当前栈桢的异常表中,找到匹配类型的异常记录的入口指令号,然后跳到该指令处执行。异常指令块执行完后,再回来继续执行后面的代码。JVM 按照每个入口在表中出现的顺序进行检索,如果没有发现匹配的项,JVM 将当前栈帧从栈中弹出,再次抛出同样的异常。
当 JVM 弹出当前栈帧时,JVM 马上终止当前方法的执行,并且返回到调用本方法的方法中,但是并非继续正常执行该方法,而是在该方法中抛出同样的异常,这就使得 JVM 在该方法中再次执行同样的搜寻异常表的操作。
如果所有的栈帧都被弹出还没有找到匹配的异常处理器,那么这个线程就会终止。如果这个异常在最后一个非守护进程抛出(比如这个线程是主线程),那么也有会导致 JVM 进程终止。
方法出口:如果是通过return正常结束,则当前栈帧从Java栈中弹出,恢复发起调用的方法的栈。如果方法有返回值,JVM会把返回值压入到发起调用方法的操作数栈。
Java虚拟机--方法区(运行时常量池)
一 方法区描述
方法区(Method Area)与Java堆一样,是##各个线程共享的内存区域##,它用于存储已经被虚拟机加载的类信息/常量//静态信息/即时编译器编译后的代码等数据.虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却又一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来.
对于习惯在HotSpot虚拟机上开发,部署程序的开发者来说,很多人都更愿意把方法区成为”永久代”(Permanent Generation),本质上两者并不相等,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内容,能够省去专门为方法区编写内存管理代码的工作.对于其他虚拟机(如BEA JRockit/ IBM J9)来说是不存在永久代的概念的.原则上,如何实现方法区属于虚拟机实现细节,不受虚拟机规范约束,但是使用永久代来实现方法区,现在看来并不是一个好主意,因为这样更容易遇到内存泄漏问题(永久代有-XX:MaxPermSize的上限,J9和JRockit只要没有触碰到进程可用内存的上限,例如:32位操作系统中的4GB,就不会出现问题),而且有极少的方法(例如String.intern())会因为这个原因导致不同虚拟机下有不同的表现.因此,对于HotSpot虚拟机,根据官方发布的路线图信息,现在也有放弃永久代并逐步采用Native Memory来实现方法区的规划了,在目前已经发布的JDK1.7的HotSpot中,已经把原本放在永久代的字符串常量池移出.
Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集.相对而言,垃圾收集行为在这个区域是比较少出现的,但是并非数据进入了方法区就如同进入永久代的名字一样”永久”存在了.这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回收”成绩”比较难以令人满意,尤其是对类型的卸载,条件相当苛刻,但是这部分区域的回收确实是存在必要的.在Sun公司的BUG列表里,曾出现过的若干个严重的BUG就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏.
根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常.
这里写图片描述
1、类型信息
类型的全限定名
超类的全限定名
直接超接口的全限定名
类型标志(该类是类类型还是接口类型)
类的访问描述符(public、private、default、abstract、final、static)
2、类型的常量池
存放该类型所用到的常量的有序集合,包括直接常量(如字符串、整数、浮点数的常量)和对其他类型、字段、方法的符号引用。常量池中每一个保存的常量都有一个索引,就像数组中的字段一样。因为常量池中保存中所有类型使用到的类型、字段、方法的字符引用,所以它也是动态连接的主要对象(在动态链接中起到核心作用)。
3、字段信息(该类声明的所有字段)
字段修饰符(public、protect、private、default)
字段的类型
字段名称
4、方法信息
方法信息中包含类的所有方法,每个方法包含以下信息:
方法修饰符
方法返回类型
方法名
方法参数个数、类型、顺序等
方法字节码
操作数栈和该方法在栈帧中的局部变量区大小
异常表
5、类变量(静态变量)
指该类所有对象共享的变量,即使没有任何实例对象时,也可以访问的类变量。它们与类进行绑定。
6、 指向类加载器的引用
每一个被JVM加载的类型,都保存这个类加载器的引用,类加载器动态链接时会用到。
7、指向Class实例的引用
类加载的过程中,虚拟机会创建该类型的Class实例,方法区中必须保存对该对象的引用。通过Class.forName(String className)来查找获得该实例的引用,然后创建该类的对象。
8、方法表
为了提高访问效率,JVM可能会对每个装载的非抽象类,都创建一个数组,数组的每个元素是实例可能调用的方法的直接引用,包括父类中继承过来的方法。这个表在抽象类或者接口中是没有的,类似C++虚函数表vtbl。
运行时常量池(Runtime Constant Pool)
运行时常量池(Runtime Constant Pool)是方法区的一部分.Class文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放.
Java虚拟机对Class文件每一部分(自然也包括常量池)的格式都有严格规定,每一个字节用于存储哪种数据都必须符合规范上的要求才会被虚拟机任何/装载和执行,但是对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不用的提供上实现的虚拟机可以按照自己的需要来实现这个内存区域.不过,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中.
运行时常量池相对于Class文件常量池的另外一个重要特征就是具备动态性,Java语言并不要求常量一定只有在编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法.其中intern()方法描述如下:
这里写图片描述
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常.
二 方法区特征
1 线程共享
方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的.整个虚拟机只有一个方法区.
2 永久代
方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,我们把方法区称之为老年代.
3 内存回收效率低
方法区中的信息一般需要长期存在,回收一遍内存之后可能只有少量信息无效.对方法区的内存回收的主要目标是:对常量池的回收和对类型的卸载.
4 Java虚拟机堆方法区的要求比较宽松
和堆一样,允许固定大小,也允许可扩展的大小,还允许不识闲垃圾回收.
- 点赞
- 收藏
- 关注作者
评论(0)