编译与代码优化

1 编译过程与优化

1.1 解析填充符号表

  • 词法分析
  • 语法分析
  • 填充符号表

1.2 注解处理器

  • 处理注解信息

1.3 语义分析与字节码生成

  • 标注检查(类型匹配、常量折叠等)
  • 数据与控制流分析(分支返回值检查、异常处理、局部变量的赋值等)
    • 局部变量的final含义在编译期保证,运行期常量没有访问标志
  • 解语法糖
    • 泛型、变长参数、自动拆装箱
  • 字节码生成
    • 除写入磁盘字节码信息外,额外修改了少量代码(父类的实例构造器、变量初始化、语句块等按顺序收缩到<init><clinit>方法中,或者如StringBuilder的自动生成等)

2 运行过程与优化

2.1 即时编译器

2.1.1 热点代码识别

  • 可识别热点方法与热点代码块(执行期间进行,栈上替换)
  • HotSpot使用计数器热点探测方式(还有采样的方式,即周期性的检查栈顶的方法,找到经常出现的)
  • 计数器分为方法调用计数器和回边计数器
  • 计数器阀值默认Client下1500,Server下10000
  • 执行方法时,先检查有没有JIT版本,存在使用,否则该方法的调用计数器+1,然后判断调用计数器与回边计数器值之和是否超过方法调用计数器阀值,是则向JIT编译器发出编译请求
  • 默认设置下方法调用计数器超过一定时间减半,该过程为热度衰减,时间段称为方法统计的半衰期
  • 回边计数器统计循环体中代码执行的次数(字节码中遇到控制流向后跳转称为回边)
  • 回边计数器阀值通过计算公式:
    • Client模式:方法调用计数器阀值 * OSR比率(OnStackReplacePercentage) / 100 = 13995(默认)
    • Server模式:方法调用计数器阀值 * OSR比率(OnStackReplacePercentage) - 解释器监控比率(InterpreterProfilePercentage) / 100 = 10700(默认)
  • 遇到回边指令时,检查是否有编译版本,没有则回边计数器+1并判断二计数器之和是否超过阀值,超过则发出OSR编译请求,并降低回边计数器值以继续使用解释模式运行来等待编译。

2.1.2 编译过程

2.2 编译优化

2.2.1 公共字表达式消除(类似合并同类项)

  • a * cc * a合并
  • a + a => a * 2

2.2.2 数组边界检查消除

  • 上下文判断索引不会越界,去掉检查数组越界的逻辑
  • 隐式异常优化

2.2.3 方法内联

  • 非虚方法直接内联
  • 虚方法查询CHA,如果只有一个版本使用,可以内联,但需要设置逃生门以切换会解释模式
  • 内联缓存,虚方法入口前,发生一次调用后,缓存方法接收者版本,每次调用都比较方法接收者版本,如果每次调用方法接收者版本一致,则内联继续使用
  • 逃逸分析,目前不成熟
    • 对象在方法中定义,可能会传入其他外部方法或赋值给其他线程可访问的变量,称之为方法逃逸或线程逃逸
    • 如果证明一个对象不会逃逸,则可进行特殊优化:
      1. 栈上分配(目前HotSpot不支持),直接在方法帧上分配对象内存,出栈直接销毁,避免GC
      2. 同步消除,对该变量的同步措施去除(没人来竞争)
      3. 标量替换,将变量拆散(如对象),需要时不创建对象,而创建可被该方法使用到的成员变量

2.3 Java编译器的劣势与优势

2.3.1 劣势

  1. 受制于编译成本
  2. 类型安全检查成本
  3. 多态选择频率极大
  4. 动态扩展,加载新的类改变整体继承关系,全局优化难以施展
  5. 对象堆内存分配,局部变量才栈上分配,C++多种方式
  6. GC

2.3.2 优势

  1. 动态安全
  2. 动态扩展
  3. 垃圾回收
  4. 别名分析
  5. 运行期间监控为基础的优化(调用频率预测,分支频率预测,裁剪未选中分支)

虚拟机执行系统

Class文件结构与字节码

Class文件结构(详细结构规则请Google)

  • 魔数与类文件版本:文件的头4字节表示Class文件:0XCAFEBABE
  • 常量池
  • 访问标志
  • 类索引、父类索引与接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

    字节码(列表请Google)

  • 大部分与数据类型有关的字节码都包含了类型信息,如iload、fload, 但底层可能是同样逻辑
  • 大多数对于boolean、short、byte、char等类型的操作,实际使用int类型作为运算类型
  • 运算指令中,只有除0操作抛出ArithmeticException,其他操作不会出现异常
  • 窄化类型转换指令直接丢弃最低N个字节以外的东西,不会有异常
  • 指令类型主要有(加载存储、运算、类型转换、对象创建与访问、操作数栈、控制转移、方法调用与返回、异常处理、同步(管程))

类加载

时机

  1. 遇到new getstatic putstatic invokestatic这4条指令,如果类没有初始化,则进行初始化
  2. 使用reflect包对类方法反射调用,如果没有初始化则初始化
  3. 初始化某类,若父类未初始化,则初始化父类
  4. jvm启动时,用户需要指定一个主类(main方法),jvm首先初始化这个类
  5. 使用JDM1.7的动态语言支持,MethodHandler实例的相关使用时,方法句柄对应类未初始化时
  • 以上5中情况称为对类的主动引用,被动引用时不触发初始化类(创建数组,引用别的类的静态常量,通过子类引用父类的静态变量)
  • 接口的初始化中,不要求父接口初始化,只有需要使用时才进行初始化父接口

类加载过程

  • 类的生命周期:加载->验证->准备->解析->初始化->使用->卸载
  • 加载:类的二进制字节流保存在方法区中,并生成一个Class对象作为方法区这个类数据的入口,对于HotSpot虚拟机来说Class对象存放在方法区中而不是堆中(如果是数组类型,则递归加载去掉一个维度的组件类型,如果组件类型为引用类型,则数组在加载这个组件的类加载器的类空间名称上被标识)
  • 验证:文件格式、元数据、字节码、符号引用
  • 准备:设置类变量(非实例变量)初始值,方法区中进行,如果不是final static, 则首先按照类型赋值0值,初始化用户指定值包装在类构造器<clinit>方法中,在初始化阶段执行,final static则直接赋值为用户指定值
  • 解析:常量池中的符号引用替换为直接引用(类或接口、字段、类方法、接口方法)
  • 初始化:执行类构造器<clinit>()方法,
    • <clinit>()由类变量的赋值动作和static语句块合并产生,static语句块不可以赋值定义在其后的变量
    • jvm保证父类的<clinit>()方法先执行,static语句块也是父类的先执行
    • <clinit>()方法不是必须的,如果既没有类变量的赋值也没有static语句块
    • 接口也会有该方法,但执行时不需要先执行父接口(使用时才执行)中的该方法,接口的实现类初始化不会执行接口的该方法
    • jvm保证执行该方法多线程安全,如果耗时,则造成阻塞

类加载器

  • 类加载器用于获取某个类的二进制字节流
  • 每一个类加载器,都有一个类名称空间,判断两个类是否相等,需要在两个类都为同一个类加载器加载的情况下才有意义,否则必定不等
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class TestUserClassLoder {
    public static void main(String[] args) throws Exception {
    ClassLoader MyClassLoader = new ClassLoader() {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
    try {
    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
    InputStream is = getClass().getResourceAsStream(fileName);
    if (is == null) {
    return super.loadClass(name);
    }
    byte[] b = new byte[is.available()];
    is.read(b);
    return defineClass(name, b, 0, b.length);
    } catch (Exception e) {
    throw new ClassNotFoundException(name);
    }
    }
    };
    Object a = MyClassLoader.loadClass("jvm.classloader.TestUserClassLoder").newInstance();
    Object b = new TestUserClassLoder();
    System.out.println(a instanceof TestUserClassLoder);//false 不是同一个类加载器实例加载 必然不相等 包括instance
    System.out.println(b instanceof TestUserClassLoder);
    }
    }

双亲委派

  • 对于JVM来说,只存在两种不同的类加载器:一种是启动类加载器(jvm一部分,c++实现,用户获取不到),另一种由Java实现,独立于JVM

  • 从开发角度,主要有三种类加载器:

    1. 启动类加载器:加载JAVA_HOME/lib目录下面的复合条件的类(jar包)
    2. 扩展类加载器:加载JAVA_HOME/lib/ext目录下面的所有类库
    3. 应用程序类加载器:用于加载用户类路径上的类库,程序中默认的类加载器
  • 类加载器之间通过双亲委派规则进行配合,如图

  • 双亲委派模型中:

    1. 除了顶层启动类加载器,其他类加载器都应当有自己的父类加载器(一般通过组合实现)
    2. 类加载器接收到类加载请求,首先将请求委派给父类加载器完成,只有父类加载器反馈无法加载才尝试自己加载
    3. Java类随着类加载器具备了优先级的层次关系(Object类存在于rt.jar中,用户定义的Object即使全类名一致也不会加载,因为启动类加载器已经加载了该类,所有任何环境中Object类都是同一个)
  • 双亲委派模型的破坏,JVM团队为了解决某些问题引入了线程上下文加载器,破坏了双亲委派请求链路,直接使用下层类加载器加载目标类(如Spring框架可以放在共享的jar目录中,则Spring类的加载由共享类加载器加载,而此时Spring需要管理用户的类,加载Spring类的共享类加载器无法加载用户的类,此时Spring则使用线程上下文加载器,即谁调用Spring去管理Bean,谁就要提供给Spring类加载器)

案例:Tomcat类加载结构

  • 要解决的问题

    1. 部署在同一个服务器上的多个应用Java类库实现相互隔离
    2. 部署在同一个服务器上的应用间Java类库可以共享(例如公共的类库)
    3. 服务器需要保证自身的安全不受应用影响,服务器所使用的类库与应用使用的类库应该隔离
    4. JSP服务器需要支持HotSwap(JSP文件修改,JSP对应的生成类需要热替换)
  • Tomcat的类文件目录结构

    • /common/*:放在这个下面的类库,可以被Tomcat和多有的Web应用程序共同使用。
    • /server/*:类库可以被Tomcat使用,对所有的Web应用程序都不可见。
    • /shared/*:类库可被所有web应用程序共同使用,但对Tomcat自己不可见
    • /WEB-INF/*:类库仅仅可以被此Web应用程序使用,对Tomcat和其他web程序都不可见。
  • Tomcat设计的类加载器结构,不同的类加载器加载不同的部分,实现了隔离

  • JasperLoader的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class, 它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

  • 对于 Tomcat 的 6.x 版本,只有指定了 tomcat/conf/catalina.properties配置文件的 server.loader 和 share.loader 项后才会真正建立 CatalinaClassLoader 和 SharedClassLoader 的实例,否则会用到这两个类加载器的地方都会用 CommonClassLoader 的实例代替,而默认没有设置,所以 Tomcat 6.x 顺理成章地把/common、/server 和/shared 三个目录默认合并到一起变成一个/lib 目录。这个目录里的类库相当于以前/common目录中类库的作用。这是Tomcat设计团队为了简化部署所做的一项改进。

字节码执行引擎

运行时栈帧结构

  • 栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
  • 每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
  • 每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。
  • 对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧。

局部变量表

  • 存放方法参数和方法内部的局部变量
  • 局部变量表容量以变量槽为最小单位(Slot),每个Slot可以存放32位以内的数据,64存放在两个Slot中(long double)
  • 编译程序时,需要使用多少局部变量表已经确定,不受代码运行期间影响
  • 方法执行时,使用局部变量表完成传参,如果是实例方法,第0位Slot存放对象的实例引用(this指针),其他参数从1开始传入,之后是内部的局部变量
  • Slot会复用,会对垃圾回收产生影响,但编译成JIT代码后枚举GC Roots行为会与解释执行有不同,不会有问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Test{
    public static void main(String[] args){
    {
    byte[] bytes=new byte[64 * 1024 * 1024];
    }
    //此处不一定回收内存,回收bytes本质是要看Slot中是否存在对bytes数据对象的引用,GC Roots包含局部变量表
    //此时占用的Slot没有被其他变量使用,栈帧中的存放bytes的Slot中仍然存在对数据的引用
    // int i=0; 此处加本行则会回收,i会复用bytes的Slot
    System.gc();
    }
    }
  • 局部变量初始化才可以使用,不存在准备阶段(赋初值,参考类变量)

操作数栈

  • 编译程序时,需要使用深的操作数栈已经确定,不受代码运行期间影响
  • 操作数栈的数据类型必须与字节码指令序列严格匹配,编译器保证
  • Java虚拟机的解释执行引擎称为基于栈的执行引擎,栈即指操作数栈
  • 一般的虚拟机会对操作数栈进行优化,让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠,省去了额外的参数复制

动态连接

  • 指向运行时常量池中该栈帧方法的引用,用户支持调用过程中的动态连接(Dynamic Linking)

方法返回地址

  • 保存方法被调用的位置
  • 方法可以正常返回或异常返回,异常返回没有返回值
  • 方法退出操作大致:恢复上层方法的局部变量表和操作数栈 -> 返回值压入调用者操作数栈 -> PC计数器指向下一条地址

方法调用

  • 确定被调用方法的版本过程而不是执行过程
  • 类加载阶段的解析阶段,将一部分方法转换为直接引用,这些方法在执行前可确定版本,且运行期不可变(见下)
  • Java关于方法调用的字节码:

    1. invokestatic:调用静态方法 (执行前可确定版本)
    2. invokespecial:调用实例构造器方法,私有方法和父类方法 (执行前可确定版本)
    3. invokevirtual:调用虚方法 (final方法也是该字节码调,但是其不是虚方法)
    4. invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
    5. invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
  • 方法版本的确定-分派:

    • 静态分派:根据依赖静态类型选择方法版本(方法重载)
    • 动态分派:运行期根据实际类型确定方法执行版本
  • Java: 静态多分派(根据静态类型与参数列表选择方法,编译期确定),动态单分派(此时只需要确定具体对象类型的方法版本,不同参数列表导致的方法版本确定已经在编译期完成)
  • 动态分派实现方式:虚方法表,保存各个方法的实际入口,没有重写父类方法就指向父类实现入口(还有其他方法,如内联缓存Inline Cache、基于继承关系分析CHA的守护内联Guard Inlining等非稳定手段)

  • Java7之后提供了MethodHandle机制实现动态方法调用

基于栈的字节码执行引擎

  • Java 编译器输出的指令流,基本上是一种基于栈的指令集架构,大部分都是零地址指令。
  • 基于栈的指令集主要的优点就是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。
  • 栈架构指令集的主要缺点是执行速度相对来说会稍慢一些。

案例:远程调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ClassExecutor {
static class MyClassLoader extends ClassLoader {
public MyClassLoader() {
super(MyClassLoader.class.getClassLoader());
}
public Class loadBytes(byte[] bytes) {
return defineClass(null, bytes, 0, bytes.length);
}
}
/**
* 执行外部传过来的代表一个Java类的Byte数组<br>
*
* @param classByte
* 代表一个Java类的Byte数组
* @param methodName
* 方法名称
* @return 执行结果
*/
public static Object execute(byte[] classByte, String methodName) {
Class clazz = new MyClassLoader().loadBytes(classByte);
try {
Object obj=clazz.newInstance();
Method method = clazz.getMethod(methodName);
return method.invoke(obj);
} catch (Throwable e) {
e.printStackTrace();
return "Err" + e.getMessage();
}
}
}

自动内存管理机制

Java的内存区域

1、程序计数(寄存)器

  • 可看作当前线程执行的字节码行数指示器,各线程私有,线程切换时可以恢复当前的状态
  • 此内存区域是唯一一个不定义OutOfMemoryError的区域
  • 如果执行的是Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址,本地方法则为空

2、Java虚拟机栈

  • 线程私有,生命周期与线程相同
  • 此区域描述的是Java方法执行的内存模型 - 每个方法执行同时在栈中创建一个新的栈元素,即栈帧(Stack Frame), 用于存储局部变量表、操作数栈、动态链接、方法出口等信息(递归调用如果没有终止条件或次数过多,每次递归调用都会创建栈帧,栈就会无法容纳足够的栈帧而溢出)
  • 局部变量表在编译期间完成分配,进入方法时,这个方法在栈帧中的局部变量表分配空间完全确定,运行期间不会改变
  • 当线程请求的栈深度超过允许抛出StackOverflowError
  • 若是虚拟机允许动态扩展此区域(大多数都可以),那么扩展时申请不到足够的内存,抛出OOM

3、本地方法栈

  • 类似于虚拟机栈,为Native方法服务
  • 也会抛出两种异常SOE、OOM

4、Java堆

  • JVM管理的最大一块内存,所有线程共享
  • 分配所有的对象实例与数组
  • JVM的优化技术如逃逸分析、栈上分配、标量替换会导致上条规则不绝对
  • 进一步可分为新生代与老年代或Eden、FromSurvivor、ToSurvivor空间
  • 内存分配时也可能会分出线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)
  • Java堆划分的目的都是为了更好更快的回收与分配内存
  • 堆分配可抛出OOM
  • 多线程创建过程中,如果每个线程分配的内存过大,更容易OOM,此时需要减少最大堆与栈容量来换取更多线程(每个进程OS分配的内存是有上限的)

5、方法区

  • 线程共享,存储被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码
  • HotSpot虚拟机使用永久代来实现方法区,这样可以使得GC一起管理这部分内存
  • 未来逐步将采用Native Memory来实现,避免内存溢出,Java8已经使用元空间来实现这一部分
  • 分配可抛出OOM

6、运行时常量池

  • 方法区的一部分
  • 存放Class文件中的各种字面量和符号引用,以及翻译出来的直接引用
  • 具备动态性,运行期间也可以将新的常量放入
  • 可抛出OOM

7、直接内存

  • 不属于运行时数据区,不是Java虚拟机规范中定义的内存区域
  • 此部分内存也频繁的使用且会抛出OOM
  • NIO类可以使用Native函数直接分配堆外内存,通过存储在Java堆中的DirectByteBuffer对象作为内存的引用进行操作,这样可以避免在Native堆与Java堆复制数据
  • 此区域内存不受Java堆限制,当各个区域内存总和大于物理内存时,动态扩展时出现OOM

Java对象

1、对象的创建

2、对象的内存布局

  • 对象结构包含对象头(Header)、示例数据(Instance Data)、对齐填空(Padding)
  • 32位机器上,非数组对象,对象头一共4 + 4 = 8字节,64为机器为8 + 8 = 16字节,数组长度始终为4字节
  • 其中MarkWord结构如下

  • 由于HotSpot要求对象的起始地址为8字节的整数倍,所有需要Padding来补全

3、对象的访问

  • 直接指针访问

    本地变量表中存储Java堆中的对象引用,堆中对象存储指向方法区类型数据

  • 句柄访问

    本地变量表中存储指向句柄池中的对象句柄地址,句柄中包含实例数据地址与类型数据地址,对象移动时,只改变句柄池中的指针,本地变量表指向的是稳定的句柄地址

垃圾收集器与内存分配

1、需要收集的内存

  • 程序计数器、虚拟机栈、本地方法栈随线程消亡而消亡(栈帧入栈与出栈)
  • 堆(包括方法区)需要垃圾回收器参与

2、寻找可回收的对象

  • 引用计数:难以解决循环引用问题
  • 可达性分析:Java、C#使用, 通过GC Roots系列对象进行可达性分析,不可用的对象就可以回收,GC Root对象主要包括:栈中的引用(本地变量表)、方法区类静态属性引用、方法区常量引用、本地方法栈中JNI(Native方法)的引用
  • 引用的加强:JVM对引用进行了进一步划分
    1. 强引用:一般情况下的引用,Object o = new Object()
    2. 软引用:系统发送OOM之前将这些引用再次回收,如果还不够才OOM, 参见类SoftReference
    3. 弱引用:存活至下一次垃圾回收,参见类WeakReference
    4. 虚引用:无法通过虚引用来获取一个对象,为一个对象设置虚引用关联唯一目的是在其回收时收到一个通知,参见类PhantomReference

3、最后一次挣扎

  • 对象判定不可达之后不会马上回收,会进行判定需要不需要执行finalize()方法,如果需要执行,进入F-Queue,Finalizer线程会进行处理,如果finalize()方法中对象成功“自救”则不会回收(重新与引用链挂钩,例如将自己挂在某个类的静态变量下)
  • 强制!!finalize方法不允许使用!

4、关于方法区

  • 存在垃圾回收,主要有废弃常量与无用类
  • 废弃常量判断条件:如果没有地方引用某个常量,则垃圾回收时可以回收
  • 废弃类判断条件按:该类所有实例被回收、该类的ClassLoder被回收、该类对应的Class对象没有被引用,无法在任何地方通过反射该类的方法
  • JVM创建的3个默认的类加载器(BootStrap Classloader、ExtClassLoader、AppClassLoader)加载的系统类(如String)或应用类都不能再运行时释放,无法满足上面的条件

5、垃圾回收算法

1. 标记清扫(Mark-Sweep)
    * 效率低、碎片化
    * 分配大对象找不到连续内存引发二次清扫

2. 复制(Copying)
    * 现代商业虚拟机都采用这种方式
    * 将内存分成两块,把存活的对象复制到另一块,整体清理前一块内存
    * IBM研究表明大部分对象“朝生夕死”,将内寸分为一大(Eden)二小(Survivor),清理时
      将Eden和S1存活的对象复制到S2中,清理掉Eden和S1,HotSpot默认的比例是8:1
    * 上条提到的S2不足时,需要老年代进行分配担保,即将无法存放的对象放置老年代
    * 两个Survivor区域的操作逻辑目的是为了始终保留一个S区域是空的

3. 标记整理(Mark-Compact)
    * 标记后,让存活的对象都向一端移动,直接清理端边界以外的内存

4. 分代回收
    * 当前商业虚拟机均采用
    * 将内存分代,不同代内存采用不同方式,新生代有大量对象死亡,采用复制算法,而老年代
      对象存活率高,没有额外的空间做分配担保,使用“标记清理”或“标记整理”方式

6、HotSpot算法实现

1. 枚举根节点
    * 在枚举根节点时,需要停顿所有Java执行线程以保证结果的准确性(Stop The World)
    * 使用OopMap数据结构快速直接得知那些地方存在对象引用,类加载完成的时候,HotSpot把对象什么偏移量是什么类型计算出来,
      JIT编译过程也会记录栈与寄存器哪些位置是引用,GC扫描时直接获取该信息
2. 安全点(Safepoint)
    * 对于具备长时间执行特征的程序,程序执行在到达安全点时才暂停,依靠指令序列重复判定(方法调用、循环跳转、异常跳转)
    * 采用主动式中断,在执行指令中加入test执行使得线程自陷,进而中断
3. 安全区域(Saferegion)
    * 对于无法进入安全点的程序,如Sleep、Block中的线程,通过判定当前是否进入安全区并标识自身,此时GC时将跳过这些线程
    * 线程离开安全区时,检查系统是否完成根节点枚举或GC过程,完成则继续执行,否则等待完成的信号

7、垃圾收集器

  • 垃圾收集器是搭配使用的
收集器 算法 用于分代 多线程 配对收集器 暂停所有用户线程 备注
Serial 复制 新生代 N Serial Old; CMS Y
ParNew 复制 新生代 Y Serial Old; CMS Y
Parallel Scavenge 复制 新生代 Y Serial Old; Parallel Old Y
Serial Old 标记整理 老年代 N Serial; ParNew; Parallel Scavenge; Y ( CMS 在Server模式下的备用预案)
Parallel Old 标记整理 老年代 Y Parallel Scavenge Y
CMS(Concurrent Mark Sweep) 标记清扫 老年代 Y Serial; ParNew part 初始标记与重新标记停所有用户线程
G1 标记整理(整体)
复制(局部)
新生代; 老年代 Y - part 初始标记、最终标记、筛选回收暂停所有用户线程

8、内存分配与回收策略

  • 对象首先在Eden上分配,空间不足时,JVM发起Minor GC(新生代GC 对应的老年代GC称为 Major/Full GC)
  • 大对象直接进入老年代(例如大的字符串或数组)
  • 长期存活的对象将进入老年代(每个对象定义了一个Age计数器,每次Minor GC存活Age+1,超过阀值进入老年代)
  • Survivor中相同年龄所有对象大小总和超过一半的,年龄大于该数的进入老年代,不需要阀值条件
  • JDK 1.6U24之后,分配对象时若老年代连续空间大于新生代对象总大小或每次晋升的平均值就会发生Minor GC,否则进行Full GC, 之前版本依赖HandlePromotionFailure参数进行判断
  • Perm区域的回收由Full GC触发