程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭,这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收问题,因为方法结束或线程结束时,内存自然就跟着回收了。而Java堆和方法区则不一样:一个接口的多个实现类需要的内存可能不一样,一个方法中多个分支需要的内存也可能不一样,只有在程序处于运行时才会知道要创建哪些对象,这部分内存的分配和回收都是动态的,垃圾回收器关注的是这部分内存。
给对象添加一个引用计数器,每当有一个地方引用它时,计数器的值就加一;当引用失效时,计数器的值减一;任何时刻计数器的值为0的对象就是不可能再被使用的。
引用计数法实现简单,判定效率也很高,在大部分情况下是一个不错的算法。但主流的Java虚拟机都没有使用引用计数法来管理内存,其中主要的原因是:它很难解决对象之间互相引用的问题。
通过一系列的称为“GC Roots"的对象作为起点,从这些起点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(图论中的不可达)时,则证明该对象是不可用的。主流实现都是使用可达性分析实现判断对象是否存活的。
在Java语言中,可以作为GC Roots的对象包括以下几种:
上面两种方法都与”引用“有关,Java中引用分为四种:强引用、软引用、弱引用和虚引用。
方法区可以不实现垃圾收集,但也可以进行垃圾收集。永久代(方法区)的垃圾收集主要回收两方面的内容:废弃常量和无用的类。回收废弃常量和回收Java堆中的对象非常类似。
判断一个废弃常量非常容易但判断一个类是”无用的类“则苛刻得多,必须满足下面三个条件:
Java的自动化内存管理最终可以归结为解决两个问题:给对象分配内存和回收分配给对象的内存。
对象的内存分配,往大的方向说就是在堆上分配,对象主要分配在新生代的Eden区上,如果启用了本地线程分配缓冲,将按线程优先在TLAB上分配,少数情况下也直接在老年代中分配。下面是一些最普遍的内存分配原则:
大多数情况下对象在新生代 Eden区分配,如果Eden区没有足够空间进行分配,虚拟机将会发起一次Minor GC。
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对内存分配来说是一个坏消息(更坏的消息是”朝生夕灭“的大对象,编程中应该尽量避免),经常出现的大对象容易导致内存还有不少空间时就要触发急啊急手机以获取足够的空间。
虚拟机给每个对象定义了一个对象年龄计数器。如果对象在经历过第一一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor中,并且对象年龄加一。对象在Survivor中每熬过一次Minor GC就让年龄加一。当年龄增加到一定程度(默认为15),就会被晋升到年老代中。
为了适应不同情况,虚拟机并不永远要求对象年龄达到指定的数值才晋升到年老代。如果在Survivor空间中相同年龄的所有对象综合大于Survivor空间的一半,那么大于等于该年龄的对象都要直接进入老年代。