基于Java程序的内存空间布局规则研究*
2017-01-05杨静杨观赐
杨静,杨观赐
(贵州大学 现代制造技术教育部重点实验室,贵阳 550000)
基于Java程序的内存空间布局规则研究*
杨静,杨观赐
(贵州大学 现代制造技术教育部重点实验室,贵阳 550000)
针对在一定大小的内存空间中Jave虚拟机在处理大型Jave程序时,Jave对象之间频繁交互导致内存占用高、处理效率低的问题,给出了减少Jave对象占用内存空间的三种布局规则。该规则利用Jave虚拟机运行机制,综合对象属性及影响内存空间大小等指标,得出相同对象不同属性之间按规则存放顺序的最优方法。结果表明,相对传统的相同对象不同属性之间无规则的存放方法,按规则存放顺序的方法能够大幅度节省内存空间,并有效提高Jave虚拟机的运行效率,程序对象越多,该方法对内存空间的节省和计算效率的提高效果就越明显。
Jave程序;内存空间;布局规则;属性;对象
引 言
随着社会经济的迅猛发展与市场对软件性能要求的提高,基于Java的应用程序因其具有“一次编译,到处运行”的特点而得到广泛应用。虽然Java开发人员对JDK虚拟机解释器不断进行优化处理,但是仍然不能完全令用户满意。目前国内Java研究者主要将焦点放在提高现有的开发设备,但是面对大型的Java程序时,效果并不明显[1]。参考文献[2]中提出在Java虚拟机中使用垃圾回收机制。在虚拟监控之下,垃圾收集器将定期进行清除行动,当所有对象是非常稳定的,此时垃圾回收器的效率会非常低,切换到“标记-扫描”模式,当堆空间存在很多碎片,切换到“标记-清理”模式,但如果想撤销内存之外的清理工作,就只能调用Java的某个方法,这样做只是对虚拟机中产生的垃圾进行清理,从而减少内存占用。参考文献[3]介绍了Java的多线程机制的应用,从而减少内存消耗,提高程序运行速度,但是Java的多线程机制容易造成死锁和资源分配不均等问题。参考文献[4]采用三种基于硬件的代码缓存策略,采用动态方式写入和读取Java代码,这种方式过于依赖CPU执行性能。参考文献[5]主要研究了内存池空间调度数据的研究,通过内存池解决了内外存之间频繁的交付问题。参考文献[6]、[7]介绍了一种Java虚拟机将类的全限定名分离为不同的结点,减少整个类的字符串在常量池中所占据的大小,这使得在内存有限的系统中装载.class文件后,能减少对存储空间的占用。
本文主要从Java对象占用空间的角度分析,对Java对象空间做优化处理,首先分析了Java虚拟机以及Java类的加载过程,其次研究了Java对象的性质,提出了Java对象的使用规则,从而达到降低内存消耗,提高运行效率的目的。
1 Java内存空间的工作原理
Java虚拟机(Java virtual machine,JVM)是运行 Java 程序必不可少的机制。JVM实现了Java语言最重要的特征,即平台无关性。其原理为编译后的 Java 程序指令并不直接在硬件系统的 CPU 上执行,而是由 JVM 执行。JVM屏蔽了与具体平台相关的信息,使Java语言编译程序只需要生成在JVM上运行的目标字节码(.class),就可以在多种平台上不加修改地运行。Java 虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行,因此实现Java平台的无关性。它是 Java 程序能在多平台间进行无缝移植的可靠保证,同时也是 Java 程序的安全检验引擎(还进行安全检查)。JVM 是 编译后的Java 程序(.class文件)和硬件系统之间的接口(编译后,Javac 是收录于JDK中的Java 语言编译器,该工具可以将后缀名为.Java的源文件编译为后缀名为.class的可以运行于Java虚拟机的字节码)。Java内存空间工作原理如图1所示。
图1 Java内存空间工作原理
JVM=类加载器classloader+执行引擎execution engine+运行时数据区域runtime data area classloader,把硬盘上的.class文件加载到JVM中运行时数据区域,但是它不负责这个类文件能否执行,这个是执行引擎负责的[8]。classloader作用是装载.class文件,classloader有两种装载.class的方式:第一种:隐式,运行过程中,碰到new方式生成对象,隐式调用classloader到JVM;第二种:显示,通过class.forname()动态加载[9]。
类的加载过程采用双亲委托机制[10],这种机制能更好地保证Java平台的安全。该模型要求除了顶层的Bootstrap Class Loader启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(interface)关系来复用父类加载器的代码。每个类加载器都有自己的命名空间(由该加载器及所有父类加载器所加载的类组成,在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类)。双亲委托机制具体的工作过程为:classloader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载,则直接返回原来已经加载的类。每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回。如果没有找到被加载的类,则委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到Bookstrap Class Loader。当所有父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。使用这种模型来组织类加载器之间的关系,主要是为了安全性,避免用户自己编写的类动态替换Java的一些核心类(比如String),同时也避免了重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的.class文件被不同的classloader加载就是不同的两个类,如果相互转型的话,会抛Java.lang.ClassCaseException。类加载器[11]classloader是具有层次架构的,也就是父子关系。其中,Boolstrap是所有类加载器的父亲。具体关系如图2所示。
图2 Java类的加载过程
2 对象所占空间分配规则及内存使用情况
2.1 对象所占空间分析
Java对象内存布局:对象头(header)、实例数据(Instance Data)和对齐填充(padding)[12]。所在环境为32位的windows系统,对象头在32位系统中占用8字节。原生类型[13](Primitive Type)的内存占用如表1所列。
表1 原生类型的内存占用
Reference在32位系统上每个占用4字节。对齐填充,Hotspot的对齐方式为8字节对齐:(对象头+实例数据+padding)%8=0,且0≤padding<8。首先,任何对象都是8字节对齐,对象占用内存大小的计算公式为:
对象占用字节数=基本的8字节+基本数据类型所占用的内存空间(累加后对齐到8的倍数)+对象引用所占用的空间(累加后对齐到8的倍数)
2.2 对象所占空间的分配规则
经过前面对Java对象相关属性分析以及Java虚拟机的工作原理和Java类的加载过程的分析研究,可以知道Sun公司的Jave虚拟机并没有按照属性声明的顺序来进行内存布局,而是按照下面的顺序规则来进行内存布局:[long,double]、[int,float]、[char,short]、[byte,boolean]、reference(引用类型),这样可以节约Java运行时的内存空间。举例如下:
Public class Test{
Byte a;
Boolean b;
Char c;
Short d;
Float e;
Double f;
Object g;
}
如果这个对象的属性按照无规则的顺序存放的话,要占用的空间为:head(8)+a(1)+b(1)+c(2)+d(2)+padding(2)+e(4)+padding(4)+f(8)+g(4)+padding(4)=40,但是按照这个规则得到:head(8)+f(8)+e(4)+d(2)+c(2)+a(1)+b(1)+e(4)+padding(2)=32,可以看到节省了不少空间。
2.3 Java继承其他子类的内存布局规则
Java虚拟机将遵循以下规则来组织父类中的类成员以及子类和父类中类成员的关系,规则如下:不同类继承关系中的成员不能混合排列。首先按照规则1处理父类中的成员,接着才是子类的成员。举例:
Class A{
Long a;
Int b;
Int c;
}
Class B extends A{
Long d;
}
这样存放的顺序及占用空间如下:head(8)+a(8)+b(4)+c(4)+d(8)=32。这是比较理想的情况,父类成员和子类成员刚好满足对其的填充规则,但在实际编程中,如果父类中的属性不够8个字节,就有了新的一条内存布局规则:父类中最后一个成员与子类的第一个成员的间隔如果不够4个字节,此时需要扩展到4个字节的基本单位,举例:
Class A{
Byte a;
}
Class B extends A{
Byte b;
}
那么此时占用的空间如下:head(8)+a(1)+padding(3)+b(1)+padding(3)=16,显然这种方式比较浪费空间。
当子类的第一个成员是Double或者Long,并且父类并没有用完8个字节,此时JVM会破坏规则,将较小的数据填充到该空间,举例:
Class A{
Byte a;
}
Class B extends A{
Long b;
Short c;
Byte d;
}
此时占用的空间如下:head(8)+a(1)+padding(3)+c(2)+d(1)+padding(1)+b(8)=24。
2.4 内存使用情况
针对Java程序进行内存空间优化处理之前,必须对被优化的目标进行有效分析。开发人员只有通过内存测试过程发现程序中哪部分代码需要进行优化,才能够针对实际情况选择相应的优化策略。有多种方式可以用于发现Java程序中的内存泄漏现象。最简单的方法就是使用一个操作系统进程监视器,它可以提供一个正在运行的进程所使用的内存数t,也可以使用JavaRuntime类中提供的totalMemory()和freeMemory()等方法来得到虚拟机所控制的连续内存空间的内存容量t,以及在特定时刻未使用的内存容量t,通过将两个方法捆绑在一起使用,可以计算出当前运行的Java程序所使用的内存量。大多数商业用的Java集成开发环境并没有提供虚拟机级的控制,因此通常可以通过JDK来完成对内存使用状况的测试。
结 语
在Java虚拟机中优化Java程序设计,就是充分利用软硬件资源,根据Java对象分配规则,采取相应的
Layout Rules of Memory Space Based on Java Program
Yang Jing,Yang Guanci
(Key Laboratory of Ministry of Education for Advanced Manufacturing Technology,Guizhou University,Guiyang 550000,China)
Frequent interaction between Java objects will cause high memory occupancy and low processing efficiency when the Java virtual machine in the treatment of the large Java program in memory space of a certain size.A new method to reduce the memory space occupancy is given.The method uses the object allocation rules,comprehensive the object properties and influence of memory size and other indicators,obtains the optimal method between the different attributes of the same object by the rules of order.The experiment results show that the method can greatly save memory space and effectively improve the operating efficiency of the Java virtual machine.The more program objects,the effect is more obvious.
Java program;memory space;layout rules;property;object
贵州省优秀青年科技人才培养对象专项资金项目(黔科合人字(2015)13号)。
TP311.1
A