Java 内存分配策略
Posted 爱coding的卖油翁
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 内存分配策略相关的知识,希望对你有一定的参考价值。
参考来源于深入理解android虚拟机一书。
1. Java 虚拟机栈 VM Stack
栈中的数据是以栈帧(Stack Frame)的格式存在的,虚拟机在执行每一个方法的调用时都会创建一个栈帧的数据结构,栈帧包括了方法的局部变量表(输入参数、输出参数、方法内的变量)、栈操作(记录出栈、入栈的操作)、动态链接、方法、类文件等一些额外的附加信息。
局部变量表中存放了编译期的基本数据类型(boolean、byte、int、char、short、float、long、double)和对象的引用(reference类型,并不是对象本身)。每一个方法的调用,其实就是对应着一个栈帧在虚拟机里入栈出栈的过程。对于活动线程中栈顶的栈帧,称为当前栈,这个栈帧所关联的方法称为当前方法。
虚拟机栈是线程私有的,是在线程创建时创建的,它的生命周期跟随线程的生命周期,线程结束时释放内存,是不需要垃圾回收的。当方法A被调用就会产生一个栈帧F1,并压入到栈中,A方法又调用了B方法,产生栈帧F2也被压入栈,方法执行完毕,先弹出F2,再弹出F1,遵循”先进后出”原则,线程结束,栈释放。
Java栈会抛出的异常:
- 如果线程请求的栈深度大于虚拟机所允许的深度,会抛出StackOverflowError异常。
- 如果无法申请到足够的内存来实现栈的对台扩展,或者没有足够的内存为一个新线程创建Java栈,会抛出OutOfMemoryError异常。
2. Java 堆 Java Heap
Java堆用来存放由关键字new创建的对象和数组。在堆中分配内存,是所有线程共享的内存区域。堆在虚拟机启动时创建。Java堆内存区域的唯一目的就是存放对象的实例,几乎所有的对象实例都在这里分配内存。
实际上,栈中的变量指向堆内存中的变量,这就是Java的指针。
Java堆也是垃圾回收机制管理的主要区域,因此很多时候也会内成为”GC堆”(Garbage Collected Heap)
一个Java虚拟机实例只会存在一个堆内存,堆内存分成三部分:
- 永久存储区 Permanent Space :存放JDK自身携带的Class Interface的元数据,在此区域的数据是不会被GC的,只有关闭虚拟机才会被释放所占用的内存。
- 新生区 Young Generation Space :新生区是类诞生、成长、消亡的区域,一个类在这里产生、应用,最后被GC回收。新生区又分为2部分:伊甸区(Eden Space)和幸存区(Survivor Space)。所有类都在伊甸区被new出来。幸存区又分为:幸存0区(Survivor 0 Space)和幸存1区(Survivor 1 Space)。当伊甸区内存用完时,程序又需要创建对象,Java虚拟机的垃圾回收器就会对伊甸区进行GC,将此区不再被其他对象所引用的对象进行销毁,剩余的对象移动到幸存0区。如果幸存0区也满了,在对该区进行GC,然后移到幸存1区。如果幸存1区也满了,就移动到养老区。
- 养老区 Tenure Generation Space :养老区用于保存从新生区帅选出来的Java对象。
如果堆中没有可用内存完成类实例或数组的分配,在对象数量达到最大的堆容量限制后就会抛出OutOfMemoryError异常。
通过-Xmx和-Xms限制堆内存大小。
3. 本地方法栈 Native Method Stack
在Java中,本地方法栈中执行的不是Java语言所编写的代码,如C、C++。
4. 方法区 Method Area
在Java系统中,方法区在虚拟机启动时创建,是所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。有个别名叫做Non-Heap(非堆)。
方法区的回收目标主要针对的是常量池的回收和对类的卸载。方法区的大小可以控制。
在方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常,在异常后面跟随的信息提示是”PermGen Space”,说明运行时常量池属于方法区(HotSpot虚拟机永久代)的一部分。
通过-XX:PermSize和-XX:MaxPermSize限制方法区大小。
5. 运行时常量池 Runtime Constant Pool
运行时常量池是方法区的一部分。在Class文件中,除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后,存放到方法区的运行时常量池中。
类的常量池在该类的Java Class文件被加载Java 虚拟机成功地加载时创建。
运行时常量池和Class文件的常量池的区别:
- Class文件的常量池在编译期生成,在运行期被装载。
- 运行时常量池具备动态性,在运行期间也可以将新的常量放入运行时常量池中。Native方法String.intern()就可以向运行时常量池中添加内容,该方法的作用是:如果池中已经包含了等于此String对象的字符串,则返回池中代表这个字符串的String对象;否则,将此String对象的字符串添加到常量池中,并且返回此String对象的引用。
运行时常量池受到方法区内存的限制,当运行时常量池无法在申请到内存时,就会抛出OutOfMemoryError异常。
在装载Class文件时,如果Class文件的常量池的创建需要比方法区中需要更多的内存时,也会抛出OutOfMemoryError异常。
6. 直接内存 Direct Memory
直接内存并不是虚拟机运行时数据区的一部分,从JDK1.4开始,加入NIO(new Input/Output),可以通过Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。
本机直接内存的分配不会受到Java堆大小的限制,但是会受到本机总内存(RAM、SWAP区或者分页文件)的大小及处理器寻址空间的限制。如果超出了物理内存的限制,会抛出OutOfMemoryError异常。
通过-XX:MaxDirectMemorySize限制直接内存大小。
7. 对象访问
Object obj = new Object();
如果这段代码出现在方法中,那”Object obj”就会反映到Java栈中局部变量表中,作为一个reference类型数据出现。而”new Object()”就会反映到Java堆中,形成一块存储了Object类型所有数据值的结构化内存。另外,Java堆中还必须包含能查找到此对象类型数据的地址信息,这些类型数据存储在方法区中。
由于reference类型在Java虚拟机只规定了一个指向对象的引用,并没有规定通过哪种方式去查找Java堆中对象的具体位置。不同的虚拟机实现的方式不同,主流又2种:使用句柄和直接指针。
- 如果使用句柄访问方式,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据的地址信息和对象类型数据的地址信息。
- 如果使用直接指针访问方式,reference中直接存储的就是对象地址。(Sun HotSpot使用的是直接指针的访问方式)。使用直接指针访问的好处是速度快,节省了一次指针定位的时间开销。
8. 内存泄漏 Memory Leak
一般我们常说的内存泄漏是指堆内存的泄露。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。
内存泄漏有四种:
- 常发性内存泄漏:泄露代码呗执行多次,很频繁的发生。
- 偶发性内存泄漏:特定环境下的才会发生的内存泄漏。
- 一次性内存泄漏:内存泄漏的代码只会背执行一次,如Singleton类的泄露。
- 隐式内存泄漏:程序在运行过程中分配的内存,直到程序结束时才释放内存。严格的说,并没有发生内存泄露,但是因为不及时的释放内存,可能最终会导致系统内存耗尽。
以上是关于Java 内存分配策略的主要内容,如果未能解决你的问题,请参考以下文章