垃圾收集主要是针对堆和方法区进行。程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后就会消失,因此不需要对这三个区域进行垃圾回收。
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
在两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正是因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
public class Test { public Object instance = null; public static void main(String[] args) { Test a = new Test(); Test b = new Test(); a.instance = b; b.instance = a; a = null; b = null; doSomething(); } } 复制代码
在上述代码中,a 与 b 引用的对象实例互相持有了对象的引用,因此当我们把对 a 对象与 b 对象的引用去除之后,由于两个对象还存在互相之间的引用,导致两个 Test 对象无法被回收。
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
Java 虚拟机使用该算法来判断对象是否可被回收,GC Roots 一般包含以下内容:
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java 提供了四种强度不同的引用类型。
1. 强引用:被强引用关联的对象不会被回收。使用 new 一个新对象的方式来创建强引用。
Object obj = new Object(); 复制代码
2. 软引用:被软引用关联的对象只有在内存不够的情况下才会被回收。使用 SoftReference 类来创建软引用。
Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj); obj = null; // 使对象只被软引用关联 复制代码
3. 弱引用: 被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。使用 WeakReference 类来创建弱引用。
Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj); obj = null; 复制代码
4. 虚引用: 又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。 为一个对象设置虚引用唯一目的是能在这个对象被回收时收到一个系统通知。 使用 PhantomReference 来创建虚引用。
Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj, null); obj = null; 复制代码
标记-清除算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
标记-清除算法是最基础的收集算法,其他的收集算法都是基于这种思路并对其不足进行改进而得到的。
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
类从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期包括: 加载、验证、准备、解析、初始化、使用、卸载 七个阶段。
1. 加载一个区别:数组类本身不通过类加载器创建,而是由虚拟机直接创建,但是数组的元素还是需要类加载器创建的;
为了确保Class文件符合当前虚拟机要求,需要对其字节流数据进行验证, 主要包括格式验证、元数据验证、字节码验证和符号引用验证
格式验证:验证字节流是否符合class文件格式的规范,并且能被当前虚拟机处理,如是否以魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围内、常量池是否有不支持的常量类型等。只有经过格式验证的字节流,才会存储到方法区的数据结构,剩余3个验证都基于方法区的数据进行。
元数据验证:对字节码描述的数据进行语义分析,以保证符合Java语言规范,如是否继承了final修饰的类、是否实现了父类的抽象方法、是否覆盖了父类的final方法或final字段等。
字节码验证:对类的方法体进行分析,确保在方法运行时不会有危害虚拟机的事件发生,如保证操作数栈的数据类型和指令代码序列的匹配、保证跳转指令的正确性、保证类型转换的有效性等。
符号引用验证:为了确保后续的解析动作能够正常执行,对符号引用进行验证,如通过字符串描述的全限定名是都能找到对应的类、在指定类中是否存在符合方法的字段描述符等。
3. 准备在准备阶段,为类变量(static修饰)在方法区中分配内存并设置初始值。
private static int var = 100; 复制代码
准备阶段完成后,var 值为0,而不是100。在初始化阶段,才会把100赋值给val,但是有个特殊情况:
private static final int var = 100; 复制代码
在编译阶段会为VAL生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将VAL赋值为100。
解析阶段是将常量池中的符号引用替换为直接引用的过程,符号引用和直接引用有什么不同?
1、符号引用使用一组符号来描述所引用的目标,可以是任何形式的字面常量,定义在Class文件格式中。
2、直接引用可以是直接指向目标的指针、相对偏移量或则能间接定位到目标的句柄。
初始化阶段是执行类构造器方法的过程,方法由类变量的赋值动作和静态语句块按照在源文件出现的顺序合并而成,该合并操作由编译器完成。
private static int value = 100; static int a = 100; static int b = 100; static int c; static { c = a + b; System.out.println("it only run once"); } 复制代码
方法对于类或接口不是必须的,如果一个类中没有静态代码块,也没有静态变量的赋值操作,那么编译器不会生成;
方法与实例构造器不同,不需要显式的调用父类的方法,虚拟机会保证父类的优先执行;
为了防止多次执行,虚拟机会确保方法在多线程环境下被正确的加锁同步执行,如果有多个线程同时初始化一个类,那么只有一个线程能够执行方法,其它线程进行阻塞等待,直到执行完成。
注意:执行接口的方法不需要先执行父接口的,只有使用父接口中定义的变量时,才会执行。
public class SuperClass{ public static int value=123; static{ System.out.printLn("SuperClass init!"); } } public class SubClass extends SuperClass{ static{ System.out.println("SubClass init!"); } } public class Test{ public static void main(String[] args){ System.out.println(SubClass.value); } } 复制代码
最后只会打印:SuperClass init! 对应静态变量,只有直接定义这个字段的类才会被初始化,因此通过子类类引用父类中定义的静态变量只会触发父类初始化而不会触发子类初始化。
public class Test{ public static void main(String[] args){ SuperClass[] sca=new SuperClass[10]; } } 复制代码
public class ConstClass{ public static final String HELLO_WORLD="hello world"; static { System.out.println("ConstClass init!"); } } public class Test{ public static void main(String[] args){ System.out.print(ConstClass.HELLO_WORLD); } } 复制代码
上面代码不会出现ConstClass init!