内存的分配区间
运行时数据区域包括程序计数器、java虚拟机栈、本地方法栈、java堆、方法区、运行时常量池。直接内存不属于运行时数据区域的一部分。
- 程序计数器:线程私有的内存。当前线程所执行的字节码的行号指示器。虚拟机规范中唯一一个没有规定任何OutOfMemoryError情况的区域。
- java虚拟机栈:线程私有。人们常把内存分为堆和栈,这是很粗糙的不正确的方法,这里的栈就指虚拟机栈。虚拟机栈中有一个局部变量表,里面存放了编译器可知的各种基本数据类型(八个)、对象引用和returnAddress类型。
- 本地方法栈:线程私有。类似于java虚拟机栈,区别在于虚拟机栈为虚拟机执行java方法(也就是字节码)服务,本地方法栈则为虚拟机使用到的Native方法服务。
- java堆:线程共享。虚拟机中内存最大的一块,是垃圾收集器管理的主要区域,很多时候被称为GC堆,回收时候采用分代收集算法。许多Java的垃圾收集器都使用了引用的根集,作为分析对象存活与否的依据。引用的根集是正在执行的Java程序随时都可以访问引用的变量的集合——也就是存在堆栈或是静态存储空间上的引用变量。从这些根集变量出发可直接或是间接到达的对象,垃圾收集器会认为这些对象是生命尚存的对象;相对的从这些根集变量出发通过任意途径都无法到达的对象,就是死亡的,它们就会成为下一次垃圾收集的对象。(可达性分析算法)
- 方法区:线程共享。存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 运行时常量池:线程共享。方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
- 直接内存:不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。主要用于jdk1.4之后的NIO类——基于通道Channel和缓冲区Buffer的I/O方式,使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。
内存泄漏和内存溢出。
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,内存泄漏的结果是内存溢出。
内存溢出是指虚拟机中内存不足,无法再开辟新的内存空间,应用所需内存不足,无法正常运行导致卡机,死机。重启后释放部分内存又可以正常运行的情况。
对象是否存活的方法:引用计数算法、可达性分析算法。
- 引用计数算法:给对象中添加一个引用计数器,当有一个地方引用它的时候,计数器增加1,当引用失效的时候,计数器减少1。任何时刻计数器为0的对象就是不可能再被使用的,需要被清除的。但是主流的java虚拟机很少用这种算法来管理内存。因为当两个对象互相引用着对方,但是除此之外再无任何引用,那么实际上这两个对象已经不可能再被访问了,但是他们互相引用,所以他们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。
- 可达性分析算法:通过一系列的GC Roots作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链。当一个对象没有任何引用链(不被任何对象引用的时候),即可判定为可回收对象。
即使是可达性分析算法没有引用的对象,也不会立刻判定对象亡被清除,而是会被标记,列入缓刑状态。即判断是否有执行finalize()方法的必要,finalize方法是Object类的方法,如果对象没有重写finalize方法或者已经被虚拟机调用过,则被视为没有必要执行(清除回收)。如果对象在finalize方法中成功拯救自己——重新与引用链上任何一个对象建立关联,那么在第二次标记时会被从"即将回收"的集合中清除,即不会被回收。finalize方法运行代价高昂,不确定性大,建议不使用,可以忘记。
引用的四种类型:强引用、软引用、弱引用、虚引用。
- 强引用:new出来的对象,只要强引用在,就不会被垃圾收集器回收。
- 软引用:有用但是并非必需的对象。内存够的时候不回收,内存溢出之前会列入回收范围进行第二次回收,如果这次回收还没有足够的内存,就会抛出内存溢出的异常。
- 弱引用:有用但是并非必需的对象,比软引用更弱。垃圾收集之前不管内存是否足够,都清除。
- 虚引用:最弱的引用关系,设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
判断是无用类被回收的条件。
1. 该类所有的实例都已经被回收了。
2. 加载该类的ClassLoader已经被回收了。
3. 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法:标记-清除算法、复制算法、标记-整理算法、分代收集算法。
- 标记-清除算法:最基础的收集算法,后续所有算法都是基于这种思路并对其不足进行改进而得到的。分为标记和清除两个阶段。标记阶段就是判断对象、变量等是否已经死去,死去的就会被标记,存入待清理的集合中,并在清除阶段内存被回收。这种算法主要有两个缺点,一个是效率问题,这两个阶段效率都不高。另外一个是清除之后会产生大量不连续的内存碎片,导致需要分配较大内存的对象时候,无法找到足够大的内存而不得不提前触发另一次垃圾收集动作。
- 复制算法:为了解决效率问题,复制算法产生了。复制算法将内存按照容量分为大小相等的两块,每次只使用其中的一块,当这块的内存用完了,就把存活的对象复制到另外一块内存,并把已经使用过的内存空间全部清理掉,实现简单运行高效,但是这种算法的代价是将内存缩小为了原来的一半,代价比较高。目前商业虚拟机都采用这种算法来回收新生代,内存分为新生代、老年代和永久代(存放常用的库文件和方法),IBM公司专门研究表明,98%的对象都是朝生夕死的。所以不需要按照1:1分配内存, 而是按照8:1:1将内存分为较大的Eden和两个较小的From Survivor、To Survivor空间,每次回收时候Eden中没有被回收的就是幸存下来的,幸存下来的会被放到From Survivor空间或者To Survivor空间(每次只能放到一个区域中,另一个是空的),并且年龄加1,下次回收的时候就会把Eden中幸存的和Survivor中的移出到另外一个空的Survivor中,并且年龄加1。当年龄到达一定数值,比如8的时候,会从新生代移动到老年代。年轻代内存满了会触发minor gc清除年轻代里面不用的垃圾,老年代满了会触发major GC或者full GC。major GC回收老年代。Full GC会把所有进程挂起等待清理垃圾,清理年轻代和老年代。这样内存利用率就是80%+10%=90%,只有10%是浪费的。但是无法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够时候,可以依赖老年代来进行分配担保。
- 标记-整理算法:当对象存活率较高的时候就要进行较多的复制操作,效率就会降低,而且需要额外的空间进行分配担保。于是有了标记-整理算法,该算法是在标记清除的基础上,每次清除之后都将存活的对象向一端移动,然后直接清除掉端边界以外的内存,使可用的内存空间连续。
- 分代收集算法:根据新生代和老年代的不同情况采用不同的垃圾收集算法。例如新生代对象成活率低,采用复制算法,老年代对象成活率高,没有额外担保空间,就必须使用标记-清理或者标记-整理算法来进行回收。