垃圾回收器
新生代回收器 之 Serial
算法: 复制算法
特点: 单线程串行收集
优点: 它是所有垃圾回收器中额外内存消耗最小的, 单核心场景下收集效率很高
STW: True
搭配 : 可搭配 Serial Old
新生代回收器 之 ParNew
算法: 复制算法
特点: 该收集器就是Serial的多线程并行版本
优点: 多核场景下, 回收效率很高
STW: True
搭配 : 只能搭配CMS
新生代回收器 之 Parallel Scavenge
算法: 复制算法
特点: 可并行收集的多线程收集器, 与ParNew类似, 不同点在于Parallel Scavenge关注点是尽可能缩短垃圾收集时用户线程的停顿时间, 即吞吐量优先, 可以开启自适应调整策略提升系统吞吐量。
优点: 吞吐量高
STW: True
搭配: Parallel Old
老生代回收器 之 Serial Old
算法: 标记-整理算法
特点: 单线程串行收集, 会导致STW, 不会产生碎片
优点: 单核心场景下收集效率高
STW: True
搭配 : 可搭配 Serial , 也可作为CMS收集器发送失败时的后备预案
老生代回收器 之 Parallel Old
算法: 标记-整理算法
特点: 并行, Java8的默认垃圾回收器, 也是一款吞吐量优先的回收器, 可以开启自适应调整策略
优点: 吞吐量高
STW: True
搭配: Parallel Scavenge
老生代回收器 之 CMS
算法: 标记-清除
特点: 第一款真正意义上的并发垃圾收集器, 第一次实现了垃圾回收线程和用户线程同时工作, 它的关注点是尽可能缩短垃圾回收停顿时间
优点: 并发收集, 停顿时间短
缺点: 会产生内存碎片, 无法处理”浮动垃圾”
STW: 某些阶段STW
搭配: 可搭配ParNew
- 四个阶段
- 初始标记(STW) : 暂停时间很短, 只标记GC ROOTs能够直接关联到的对象
- 并发标记: 从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程很耗时, 但是不需要停顿用户线程
- 重新标记: 该阶段是为了修正并发标记期间, 因用户程序运作导致标记产生变动的那一部分对象的标记记录, 该阶段停顿时间比初始阶段稍长, 但远比并发标记时间短
- 并发清理: 并发清理判断已死亡的对象, 由于采用标记-清除算法, 不需要移动对象, 因此可与用户线程并发运行.
Garbage First
- JDK 7 正式支持
- JDK 9 废弃了CMS回收器, 默认采用G1回收器
- 兼顾吞吐量和低延迟
- G1把连续的Java堆内存划分为多个大小相等的独立区域, 成为Region, 每一个Region都可以根据需要, 扮演新生代的Eden空间, Survivor空间, 或者老年代空间. 收集器能够对不同角色的Region采用不同的策略去处理.
- Region中有一类特殊的Humongous区域, 专门用来存储大对象, 即大小超过Region容量一半的对象.
- G1的处理思路是, 去跟踪各个Region里面的垃圾堆积的”价值”大小, 价值即回收所获得的的空间大小以及回收所需时间的经验值, 然后在后台维护一个优先级列表, 每次根据用户设定允许的停顿时间来优先处理回收价值收益最大的那些Region, 这也是Garbage First名字的由来
- 对于跨Region引用对象的问题, 使用记忆集(规范)和卡表(实现)的方式解决, 当Region中一个卡页中的对象引用了另一个Region中的对象, 就将该卡页标记为脏卡, 这也每次垃圾回收时, 只需要从脏卡中遍历GC roots即可
- G1的运行过程分为四个步骤:
- 初始标记: 仅仅标记一下GC Roots能直接关联到的对象, 这个阶段需要停顿用户线程, 但耗时很短, 而且这个阶段会在Minor GC的时候完成
- 并发标记: 从GC Roots的直接关联对象开始可达性分析, 这个过程很耗时, 但是不需要停顿用户线程
- 最终标记: 对用户线程做另一个短暂暂停, 用户处理并发标记阶段遗留下来的少量SATB记录
- 筛选回收: 负责计算Region统计数据, 计算各个Region的回收成本和价值, 回收时会采用复制算法将存货对象复制到空的Region中, 再清理掉整个旧Region.这里的操作涉及对象的移动, 因此必须暂停用户线程,由多条线程并行完成.
- 由上面描述可见, 该算法整体上是标记=整理算法, 局部(Region)是标记-复制算法.
类加载
类的生命周期
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。其中验证、准备、解析统称为连接
类加载-过程
加载
加载阶段(可参考java.lang.ClassLoader的loadClass()方法),虚拟机要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中(堆中?!)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成4个阶段的检验动作:
- 文件格式验证:验证字节流是否符合Class文件格式的规范, 如是否以魔术0xCAFEBABE开头
- 元数据验证
- 字节码验证
- 符号引用验证
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
初始化
类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序制定的主观计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器
()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的 ,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如下: ```java
public class Test { static { i=0;//给变量赋值可以正常编译通过 System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用) } static int i=1; } ```
()方法与实例构造器 ()方法不同,它不需要显示地调用父类构造器,虚拟机会保证在子类 ()方法执行之前,父类的 ()方法已经执行完毕,一次虚拟机中第一个被执行的 ()方法的类肯定是java.lang.Object。
类加载器(class loader)
类加载器可以划分如下:
- 启动类加载器(Bootstrap ClassLoader)
这个类加载器负责将放置在
- 扩展类加载器(Extension ClassLoader)
这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载
- 应用程序类加载器(Application ClassLoader)
这个类加载器由sum.misc.Launcher.$AppClassLoader来实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也被称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
- 自定义类加载器(User ClassLoader)
可以自定义类加载器, 重写loadClass()方法来破坏双亲委派模型, 因为这个方法可以指定类通过哪个类加载器来加载.
双亲委派机制
双亲委派机制是指一个类在收到类加载请求后不会尝试自己去加载这个类,而是把该类加载请求向上委托给其父类去完成,父类收到请求后又会委派给自己的父类,最后都委派到启动类加载器。如果父类加载器收到请求后发现自己无法加载该类,则父类会将该信息反馈给子类并向下委托子类加载器加载该类,直到该类被加载成功。<br />双亲委派机制的核心是保障类的唯一性和安全性。