本文整理自网络和书籍。
Java内存分配与管理是Java的核心技术之一,一般来说,Java在内存分配会涉及到以下区域:
区域 | 说明 |
---|---|
寄存器 | 我们在程序中无法控制。 |
栈 | 存放基本类型的数据和对象的引用,但对象本身不存放在栈中,而是存放在堆中。 |
堆 | 存放new产生的数据。 |
静态域 | 存放在对象中用static定义的静态成员。 |
常量池 | 存放常量。 |
非RAM存储 | 硬盘等永久存储空间。 |
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
注意:图片来自《深入理解Java虚拟机-JVM高级特性与最佳实践
》
在函数中定义的一些基本类型的变量数据,还有对象的引用变量都在函数的栈内存中分配。当在一段代码中定义一个变量的时候,Java就在栈中为这个变量分配内存空间,当该变量退出作用域的时候,Java会自动释放掉为该变量所分配的内存空间。
栈也叫栈内存,是Java程序的运行区,是在线程创建的时候创建的。它的生命周期是随着线程的生命期,线程结束栈内存也就释放了,对于栈来说不存在垃圾回收的问题,只要线程一结束,该栈就结束了。
StackOverFlowError
和 OutOfMemoneyError
。当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError
错误;虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时候,抛出OutOfMemoneyError
。在一个栈中有两个栈帧,栈帧2是最先被调用的方法,先入栈,然后方法2又调用了方法1,栈帧1处于栈顶的位置,栈帧2处于栈底,执行完毕后,依次弹出栈帧1和栈帧2,线程结束,栈释放。
栈帧数据 | 说明 |
---|---|
本地变量(Local Variables) | 包括输入参数和输出参数以及方法内的变量。 |
栈操作(Operand Stack) | 记录出栈和入栈的操作。 |
栈帧数据(Frame Data) | 包括类文件、方法等。 |
StackOverFlowError
和 OutOfMemoneyError
。堆内存用来存放由关键字new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。
在堆中产生了一个数组和对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或者对象在堆内存中的首地址,栈中的这个变量就成了数组或者对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或者对象。
引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之后被释放。而数组和对象本身在堆中分配,即使程序运行到使用new产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才会变为垃圾,不能再被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器回收(释放掉)。这也是Java占内存的原因。
实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针。
Garbage Collected Heap
)。更细一点年轻代又分为Eden区最要放新创建对象,其中From survivor
和To survivor
保存gc后幸存下的对象,默认情况下各自占比 8 : 1 : 1 8:1:1 8:1:1。区域 | 说明 |
---|---|
eden + S0 + S1 | 新生代 |
S0 | 放置存活的对象 |
S1 | 放置存活的对象 |
tenuren | 老年代,用于保养从新生区筛选出来的Java对象,一般池对象都在这个区域活跃。 |
premanent | 永生代,或者叫做永久存储区域,用于存放JDK自身所携带的Class Interface的元数据。也就是说,它存储的是运行环境必需的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭Java虚拟机才会释放此区域所占用的内存。 |
permanment generation
)string
的intern()
方法。OutOfMemoryError
异常。NIO
(New Input/Output)类,引入了一种基于通道(Channel
)与缓冲区(Buffer
)的I/O方式,它可以使用Native
函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer
对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能能,因为避免了在Java堆和Native堆中来回复制数据。对象访问会涉及到Java栈、Java堆、方法区这三个最重要的内存区域之间的关联关系。
如下面这句代码:
Object objectRef = new Object();
假设这句代码出现在方法体中,Object objectRef
这部分将会反映到Java栈的本地变量中,作为一个reference
类型数据出现。而new Object()
这部分将会反映到Java堆中,形成一块存储Object类型所有实例数据值的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout
)的不同,这块内存的长度是不固定。另外,在java堆中还必须包括能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些数据类型存储在方法区中。
reference
类型在java虚拟机规范里面只规定了一个指向对象的引用地址,并没有定义这个引用应该通过那种方式去定位,访问到java堆中的对象位置,因此不同的虚拟机实现的访问方式可能不同,主流的方式有两种:使用句柄和直接指针。
指针访问方式:reference
变量中直接存储的就是对象的地址,而java堆对象一部分存储了对象实例数据,另外一部分存储了对象类型数据。
跟踪收集器采用的为集中式的管理方式,全局记录对象之间的引用状态,执行时从一些列GC Roots
的对象做为起点,从这些节点向下开始进行搜索所有的引用链,当一个对象到GC Roots
没有任何引用链时,则证明此对象是不可用的。
下图中,对象Object6
、Object7
、Object8
虽然互相引用,但他们的GC Roots
是不可到达的,所以它们将会被判定为是可回收的对象。
可作为GC Roots
的对象包括:
标记清除算法是最基础的收集算法,其他收集算法都是基于这种思想。标记清除算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,标记完成之后统一清除对象。
它的主要缺点:
①.标记和清除过程效率不高
②.标记清除之后会产生大量不连续的内存碎片。
它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,不会产生碎片等情况,只要移动堆订的指针,按顺序分配内存即可,实现简单,运行高效。
主要缺点:内存缩小为原来的一半。
标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针。
主要缺点:在标记-清除的基础上还需进行对象的移动,成本相对较高,好处则是不会产生内存碎片。
引用计数收集器采用的是分散式管理方式,通过计数器记录对象是否被引用。当计数器为0时说明此对象不在被使用,可以被回收。
主要缺点:循环引用的场景下无法实现回收,例如下面的图中,ObjectC和ObjectB相互引用,那么ObjectA即便释放了对ObjectC、ObjectB的引用,也无法回收。SunJDK在实现GC时未采用这种方式。
深入理解Java虚拟机-JVM高级特性与最佳实践
》深入理解Android虚拟机
》