垃圾回收机制
将垃圾回收机制,我想先从JVM的运行时数据区开始讲起。
JVM的运行时数据区
程序计数器可以看作是当前程序所执行的字节码的行号指示器。java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的命令。因此,为了线程切换之后能够恢复到正确的执行位置,每个线程需要一个独立的程序计数器。
虚拟机栈: 生命周期与线程相同,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈: 为虚拟机使用到的Native方法服务。
Java堆: 几乎所有的实例对象都在堆上分配内存,java堆是垃圾收集器管理的主要区域。
方法区: 用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码。垃圾收集行为在这个区域中时很少出现的。
方法区不是很好理解,为了加深印象,下面举个例子来说明:
public class AppMain //运行时, jvm 把appmain的信息都放入方法区
{
public static void main(String[] args) //main 方法本身放入方法区。
{
Sample test1 = new Sample( " 测试1 " ); //test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面
Sample test2 = new Sample( " 测试2 " );
test1.printName();
test2.printName();
}
} Sample.java
public class Sample //运行时, jvm 把appmain的信息都放入方法区
{
/** 范例名称 */
private name; //new Sample实例后, name 引用放入栈区里, name 对象放入堆里
/** 构造方法 */
public Sample(String name)
{
this .name = name;
}
/** 输出 */
public void printName() //print方法本身放入 方法区里。
{
System.out.println(name);
}
}
运行时常量池:
其为方法区的一部分,
运行时常量池(Runtime Constant Pool)会在类加载时载入class文件中的常量池信息(constant_pool table)。
对象的内存分布
在虚拟机中,对象在内存中的存储可以分为3块区域:对象头,实例数据,对齐填充。
- 对象头包含2部分信息,第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄,线程持有的锁,偏向线程ID,偏向时间戳。另一部分是类型指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
- 建立对象是为了使用对象,我们的java程序需要通过栈上的reference数据操作堆上的具体对象。reference类型在java虚拟机上只规定了一个指向对象的引用。没有定义这个引用应该通过何种方式去定位,对象访问方法是取决于虚拟机的。有2个主流的访问场景:
句柄访问和直接指针访问。
可达性分析
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连的时候,证明此对象是不可用的。
在Java语言中,可作为GC Roots的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈的JNI(即一般说的Native方法)引用的对象
回收方法区
在堆中,尤其是在新生代中。常规应用进行一次垃圾收集一般可以回收70%——95%的空间。而方法区(或者说是HotSpot虚拟机中的永久代)是没有垃圾收集的。
方法区的垃圾收集主要为2个部分。
第一:废弃的常量 例如一个字符串“abc”已经放入了常量池中,当时当前系统中没有任何以一个String对象叫做“abc”,即没有任何的String对象引用常量池中的“abc”变量。如果发生内存回收,而且必要的话,这个“abc”常量会被系统清理出常量池。常量池中的其他类。方法,字段,字段的符号引用也与此类似。
第二:无用的类
判断一个类是否是无用的类比较复杂。需要同时满足以下三个条件:
- 该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例。
- 加载该类的classLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾回收算法
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代收集算法(新生代采用复制算法,老年代采用标记-清理或标记-整理算法)
垃圾收集器
这7种垃圾收集器各有优劣以及用处。
详细阐述如下:
- Serial垃圾收集器:在回收垃圾时,必须暂停所有的其他工作线程,直到其收集结束。其简单而高效。没有线程交互的开销,可以做到最高的单线程收集效率。并且其停顿时间完全可以控制在几十毫秒。
- ParNew垃圾收集器:使用多线程进行垃圾收集。其与Serial收集器相比没有太多的创新之处,但是除了Serial收集器之外,只有它可以和CMS配合使用。其默认开启的收集线程数量和CPU的数量相同。
- Parallel Scavenge垃圾收集器:Parallel Scavenge目标是达到一个可控的吞吐量。
- Serial Old垃圾收集器:单线程收集器。Serial的老年代版本。
- Parallel Old垃圾收集器:搭配Parallel Scavenge使用,实现“吞吐量优先”。
- CMS(Concurrent Mark Sweep):着重来介绍以下CMS垃圾回收器。它是一种以获取最短回收停顿时间为目标的垃圾回收器。其基于标记-清除算法。整个过程分为4个步骤:
1.初始标记 2. 并发标记 3. 重新标记 4. 并发清除
初始标记和重新标记仍然需要“Stop the World”,初始标记仅仅只是标记以下GC Roots能直接关联到的对象。
并发标记阶段是进行GC Roots Tracing
重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作。 - G1垃圾回收器: 最前沿的一款垃圾回收器。主要有初始标记,并发标记,最终标记,筛选回收这四个步骤。