文章目录
1. Java运行时数据区域
- 堆: 线程共享,主要用来存储对象。
- 堆可分为:年轻代和老年代两块区域。使用NewRatio参数来设定比例;
- 年轻代可分为:一个Eden区和两个Suvivor区,使用参数SuvivorRatio来设定大小。
- 虚拟机栈/本地方法栈: 线程私有,主要存放局部变量表,操作数栈,动态链接和方法出口等。
- 程序计数器: 线程私有,记录当前线程的行号指示器,保障线程切换。
- 方法区: 线程共享,主要存储类信息、常量池、静态变量、JIT编译后的代码等数据。方法区理论上来说是堆的逻辑组成部分。
- JDK1.8之前HotSpot用永久代实现,JDK1.8之后用元空间(MetaSpace)实现,元空间不在虚拟机中,而在本地内存(因为永久代经常不够用或内存泄漏)。
- 运行时常量池: 方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
- 字符串常量池: 全局。
- jdk1.7 之前是方法区的一部分,存放字符串实例;
- jdk1.7 之后在堆内存之中,存储引用,字符串实例是在堆中。
2. 垃圾回收
2.1 判断存活对象
- 引用计数算法: 每当一个地方引用对象时,其引用计数器+1;引用失效则-1。为0则表示对象不再使用。(存在互相引用现象的对象无法被回收)
- 可达性分析算法: 以一系列 “GC Roots”对象作为起点向下搜索检查对象是否存在可达路径。GC Roots 到对象不可达则证明对象不可用。
Java中以下对象可作为GC Roots:- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI(即Native方法)引用的对象。
2.2 判断对象可回收
- 可达性分析进行第一次标记;
- 对象没有重写 finalize() 方法或者 finalize() 方法已被调用过,则认为该对象不可被救活,因此回收该对象。
- finalize()方法仅会被执行一次,如果对象已经通过执行 finalize() 方法被救火过一次,面对下一次回收时它将不会再次被调用。
- finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,应尽量避免使用,可用 try-finally 或其他方式来代替。
2.3 方法区的回收
- 常量: 常量池中一些常量、符号引用没有被引用,会被清理出常量池。
- 无用的类: 被判定为无用的类,会被清理出方法区,判定方法包括如下三个条件:
- 该类的所有实例被回收;
- 加载该类的 ClassLoader 被回收;
- 该类的 Class 对象没有被引用。
2.4 垃圾收集算法
2.4.1 标记-清除算法(Mark-Sweep)
- 不足:
- 标记和清除效率都不高;
- 产生大量不连续内存碎片,导致分配大对象时需要处罚另一次垃圾收集动作。
2.4.2 复制算法(copying)
- 优势:每次回收一整个半区,内存分配时无需考虑内存碎片,简单高效。
- 不足:
- 内存缩小为原来一半;
- 存活率高时赋值操作较多,效率变低。
- 应用:新生代回收,因为大多对象朝生夕死,通过默认 Eden 和 Survivor 大小比例为 8:1,解决内存浪费为题(只有10%内存浪费)。
2.4.3 标记-整理算法(Mark-Compact)
- 优势:解决了“标记-清除算法”的内存碎片问题,和“复制算法”对象存活率较高时低效问题。
2.4.4 分代收集算法(Generational Collection)
根据对象存活周期不同将内存划分为几块(一般是新生代和老年代)。
根据不同年代特点选用最恰当的收集算法:
- 新生代 - 每次只有少量对象存活 - 采用复制算法;
- 老年代 - 存活率高,无足够额外空间分配担保 - 采用“标记-清除”或“标记-整理”。
2.5 垃圾收集器
2.5.1 Serial 收集器
2.5.1.1 特点
- 最基本、最悠久。
- 单线程。
2.5.1.2 优缺点
优点:简单高效,无线程交互开销;
缺点:单线程,收集时必须暂停其他所有工作线程。
2.5.1.3 应用
可用于运行在 Client 模式下的虚拟机,因为这种一般分配给虚拟机管理的内存不会很大,可以将停顿时间控制在可接受范围内。
2.5.2 ParNew 收集器
2.5.2.1 特点
是 Serial 收集器的多线程版本,两者共用了许多代码。
2.5.2.2 优缺点
优点:多CPU环境下有效利用资源。
缺点:单CPU环境下甚至效果不如 Serial(由于线程交互开销)
2.5.2.3 应用
运行在 Server 模式下的虚拟机中首选的新生代收集器(除 Serial 外唯一能配合 CMS 收集器的新生代收集器)。
默认开启收集线程数与CPU数量相同。
并发与并行
并行(Parallel):多条垃圾收集线程并行工作,此时用户线程仍处于等待状态。
并发(Concurrent):用户线程和垃圾收集线程并行工作。
2.5.3 Parallel Scavenge收集器
2.5.3.1 特点
- “吞吐量优先收集器”,目标是能够达到一个可控制的吞吐量(Throughput)。
吞吐量 = CPU运行用户代码时间 / CPU运行用户代码时间 + 垃圾收集时间)
(其他收集器主要目标是尽可能缩短垃圾收集时用户线程停顿时间) - 新生代收集器,复制算法,并行的多线程收集器。
2.5.3.2 应用
- 其他收集器停顿时间越短越适合需要和用户交互的程序,提升用户体验。
- 高吞吐量可高效率利用CPU时间完成运行任务,适合后台运算而不需要太多交互的任务。
2.5.4 Serial Old 收集器
2.5.4.1 特点
Serial 收集器的老年代版本,单线程,标记-整理算法。
2.5.4.2 应用
- 主要用于 Client 模式下的虚拟机。
- 如果在Server模式下使用,有两大用途:
- 在JDK 1.5及之前版本中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备方案,并发收集发生 Concurrent Mode Failure 时使用。
2.5.5 Parallel Old 收集器
2.5.5.1 特点
Parallel Scavenge 的老年代版本,多线程,标记-整理算法,吞吐量优先。
2.5.5.2 应用
配合新生代 Parallel Scavenge 收集器使用。
避免 CMS 无法配合 Parallel Scavenge 而只能选择 Serial Old,单线程无法充分利用服务器应用多CPU的优势,从而无法实现吞吐量最大化效果的尴尬情况。
2.5.6 CMS 收集器
2.5.6.1 特点
- HotSpot 第一款真正意义上的并发收集器,实现垃圾收集线程与用户线程同时工作。
- 以获取最短回收停顿时间为目标,标记-清除算法。
- 耗时最长的 “并发标记” 和 “并发清除” 过程可以和用户线程一起并发执行。
- 缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾
- 产生大量空间碎片
2.5.6.2 应用
大部分的互联网网站或者 B/S 系统的服务端,重视服务响应速度,希望系统停顿时间最短,提升用户体验。
2.5.7 G1收集器(Garbage-First)
2.5.7.1 特点
当今收集器技术最前沿成果之一,包括如下特点:
- 堆内存划分为一个个Region,这样收集时不必在全堆范围内进行;
- 堆内存物理上分为多个Region,逻辑上标记为Eden,Survivor,Old,Humongous;
- 空间整合:整体上看基于”标记-整理“算法,局部(两个Region间)上看基于”复制“算法。
- 停顿时间可控,用户可设置多长时间内完成收集;
2.5.7.2 回收过程
- 初始标记(STW):标记 GC Roots 能直接关联到的对象,伴随着一次 Young GC(Stop The World)发生。
- 根区间扫描,标记所有Survivor区间的对象引用,扫描 Survivor 到 Old 的引用。(在下一次 Young GC 前完成)
- 并发标记:从 GC Roots 开始进行可达性分析,找出存活的对象,耗时长,与用户程序并发,可被 Young GC中断。(三色标记)
- 最终标记(STW):修正并发标记期间发生变动的标记记录,JVM将这段时间对象变化记录在Remebered Set Logs中,最终标记将这些数据合并到 Remembered Set 中,使用 snapshot-at-the-beginning(SATB)算法。
- 筛选回收:对各Region的回收价值和成本进行排序,根据用户设置的GC停顿时间制定回收计划,回收的Region还有存活对象则拷贝到新的Region中(无空间碎片)。
2.5.7.4 SATB
全称snapshot-at-the-beginning算法,应用于并发标记阶段解决CMS重新标记长时间STW的风险。
Region包含了5个指针,分别是bottom、previous TAMS、next TAMS、top和end。
两个TAMS(top-at-mark-start)指针标识前后两次并发标记时的位置。
1、第n轮并发标记开始,top指针赋给next TAMS,并发标记期间,都在[next TAMS,top]之间分配对象,SATB可确保这部分对象都会被标记,默认存活。
2、并发标记结束时,将next TAMS的地址赋给previous TAMS,SATB给[bottom,previous TAMS]之间的对象创建一个快照Bitmap,所有垃圾对象通过快照可被识别出。
3、第n+1轮并发标记开始,同上1、2过程。
2.5.7.3 应用
- 面向服务端应用,适合多CPU和大内存的服务器
- 未来可以替换掉 CMS 收集器
2.6 YoungGC 和 FullGC 发生的时机
2.6.1 Young GC的触发时机
新生代的Eden区域满了之后触发,采用复制算法回收新生代的垃圾,Survivor中年龄超过15的对象挪入老年代。
2.6.2 Full GC的触发时机
老年代空间不够,无法容纳更多对象,包括以下情况:
- Young GC之前检查,若 老年代可用的连续内存空间 < 新生代历次YoungGC后进入老年代的对象总和的平均大小,则认为本次YoungGC后进入老年代的对象总大小,可能会超过老年代当前可用空间,此时将先进行Old GC为老年代腾出更多空间后再执行Young GC。
- 老年代没有足够的内存空间来容纳 Young GC 之后需要放入老年代的对象,将触发一次Old GC。
- 老年代内存使用率超过了92%(可通过参数设置),将触发Old GC。
3. 类加载机制
3.1 类加载过程
3.1.1 加载
需要完成以下3件事情:
- 通过一个类的全限定名获取定义此类的二进制字节流;
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
3.1.2 验证
为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害自身安全,包括:
- 文件格式验证
- 元数据格式
- 字节码验证
- 符号引用验证
3.1.3 准备
正式为类变量分配内存并设置初始值,这些类变量所使用的内存将在方法区中进行分配。
3.1.4 解析
- 将常量池内的符号引用替换为直接引用。
- 针对7类符号引用:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符。
3.1.5 初始化
- 类加载过程的最后一步,真正开始执行类中定义的 Java 字节码。
- 执行类构造器() 方法的过程。该方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生。
3.2 类加载的时机
JVM规范规定有且只有5中情况必须立即对类进行"初始化"(加载、验证、准备需要在此之前开始):
- 遇到 new、getstatic、putstatic、invokestatic 这4条字节码指令时,产生这4条指令的Java代码场景是:
- 使用 new 关键字实例化对象;
- 读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外);
- 调用一个类的静态方法。
- 使用 java.lang.reflect 包的方法对类进行反射调用时;
- 初始化一个类的时候,其父类需要先初始化;
- 虚拟机启动时,用户需要指定一个主类(包含main方法的类),虚拟机会先初始化这个主类;
- 使用 JDK1.7 的动态语言支持时,若一个 Java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,需要先初始化这方法句柄对应的类。
3.3 类加载器
3.3.1 类与类加载器
- 对于任意一个类,需要由加载它的类加载器和这个类本身一同确立其在 Java虚拟机中的唯一性,每个类加载器,都拥有一个独立的类名称空间。
- 比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。
3.3.2 分类
3.3.2.1 从JVM角度:两种不同的类加载器
- 启动类加载器(Bootstrap ClassLoader),HotSpot 使用C++语言实现,是虚拟机自身的一部分。
- 所有其他的类加载器,都是由 Java 实现,独立于虚拟机外部,并且全都继承抽象类 java.lang.ClassLoader。
3.3.2.2 开发人员角度:三种类加载器
3.3.2.2.1 启动类加载器(Bootstrap ClassLoader)
负责加载存放在 <JAVA_HOME>\lib 目录中的、或者被 -Xbootclasspath 参数所指定路径中的,且文件名虚拟机能够识别的类库。
3.3.2.2.2 扩展类加载器(Extension ClassLoader)
由 sun.misc.Launcher$ExtClassLoader 实现,负责加载 <JAVA_HOME>\lib\ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。
3.3.2.2.3 应用程序类加载器(Application ClassLoader)
- 系统类加载器,默认的类加载器。
- 由 sun.misc.Launcher$AppClassLoader 实现,是 ClassLoader 中 getSystemLoader() 方法的返回值。
- 负责加载用户类路径(ClassPath)上所指定的类库。
3.3.3 双亲委派模型
- 除了顶层的启动类加载器外,其余类加载器都应当有自己的父类加载器。
- 类加载器的父子关系一般不会以继承(Inheritance)实现,而是以组合(Composition)关系来复用父加载器的代码。
- 工作过程: 一个类加载器收到类加载请求,不会自己先尝试加载这个类,而是把请求委派给父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传到顶层的启动类加载器中,只有当父加载器反馈无法完成这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
- 作用: 保证类在程序的各种类加载器环境中都是同一个类。