JVM

# JVM内存分区

# PC寄存器

#

# 栈帧

# 局部变量表

# 操作数栈

# 本地方法栈

#

# 堆空间

# 代码优化策略

# 序列化与深拷贝

# TLAB

# 方法区

# 字符串常量池(全局串池)

JDK1.6中串池存在于方法区(永久代)中,只有当进行fullGC时才进行垃圾回收;JDK1.7中串池脱离方法区,存在于中,只要进行minorGC时就进行垃圾回收

类加载解析阶段,在堆中生成字符串对象实例,并将该字符串对象实例的引用值存于串池(JDK1.7后位于堆)中

HotSpot VM中的具体实现为StringTable(串池map),其中存储驻留字符串引用,在每个HotSpot VM实例只有一份,被所有的类共享

String a = "a";
String b = "b";
String ab1 = "ab";
String ab2 = a + b; // ab2 != ab1
String ab3 = "a" + "b"; // ab3 == ab1

String abi = ab2.intern(); // abi != ab2且abi == ab1 == ab3(JDK1.7)
  • 字面值常量(a、b、ab1)

    若在串池中未找到目标字符串对象,则创建对应的字符串对象存储于堆中,将其引用放入串池(整个过程为懒加载)

  • 字符串拼接表达式(ab2、ab3)

    • 若表达式中包含变量,创建StringBuilder对象,分别拼接表达式变量,最后将toString()结果(新字符串)保存于堆中,并将其引用赋值给ab2而不放入串池
    • 若表达式中均为常量,则在编译期进行字符串拼接,并执行字面值常量初始化流程
  • intern()

    • JDK1.7:尝试将调用该方法的字符串对象引用放入串池,并返回串池中该对象的引用
      • 若串池中不存在该对象引用,则返回的对象引用 == 当前对象引用
      • 若串池中已存在该对象引用,则无需将当前字符串关联串池,返回的对象引用 != 当前对象引用
    • JDK1.6:尝试将调用该方法的字符串对象的副本引用放入串池,并返回串池中该对象引用,因此返回的对象引用 != 当前对象引用
# 串池调优

# class文件常量池

类加载阶段,编译器生成的各种字面量于符号引用存储于class文件常量池(表)中。其中在类加载解析阶段,运行时常量池中符号引用被替换为直接引用,此时可能需要查询串池,以保证串池中的字符串引用值与class文件常量池中的字符串引用值一致

# 运行时常量池

类加载阶段结束后,class文件常量池中的内容转移至运行时常量池中

JDK1.6中运行时常量池存在于**方法区(永久代)中,JDK1.8中运行时常量池存在于元空间(直接内存)**中

# 垃圾回收

# 基本概念

# 根对象与可达性分析

# 根对象:
  • 强引用
  • 软引用
  • 弱引用
  • 虚引用
  • 终结器引用

# GC算法

  • 标记-清除

    1. 标记可达对象
    2. 清除不可达对象
    # 缺陷
    • STW较长,性能较差
    • 碎片化严重
  • 标记-整理

  • 复制

    # 缺陷
    • 存货对象较多时影响性能(故不适用于老年代)
    • 只使用一半空间进行对象填充,内存使用率较低
  • 分代收集(新生代使用复制算法,老年代使用标记-清除/标记-整理算法)

# GC类型

  • MinorGC

    新生代满时触发

    以下对象将提前晋升至老年代:

    • 长期存活的对象
    • Survivor区无法容纳的大对象
    • Survivor区可容纳,但复制过程过于消耗性能的大对象
  • FullGC

    老年代满时触发

  • OOM:某线程发生OOM将清空其占用的堆内存,而不影响其他线程

# 垃圾回收器

# 串行(不推荐使用)

-XX:+UseSerialGC:Serial New(新生代,复制) + Serial Old(老年代,标记-清除

# 并行(JDK6、7、8默认)

-XX:+UseParallelGC/-XX:+UseParallelOldGC:Parallel Scavenge(新生代+老年代,复制

吞吐量优先,GC线程对CPU占用高

-XX:GCTimeRatio=GC时间占总吞吐时间百分比(默认值99%)

-XX:MaxGCPauseMillis=GC最大STW时间

# CMS(并发,JDK9废弃)

-XX:+UseConcMarkSweepGC:CMS(老年代,标记-清除;ParNew Old为备用)+ -XX:+UseParNewGC:ParNew New(【并行】新生代,复制

  1. 初始标记(STW)
  2. 并发标记
  3. 预清理
  4. 重新标记(STW)
  5. 并发清理
  6. 并发重置

响应时间优先,GC线程对CPU占用低

若并发GC失败,则CMS退化为SerialOld

-XX:ParallelGCThreads=并行线程(工作线程)数

-XX:ConcGCThreads=并发线程(GC线程)数(t2 = t1 / 4)

-XX:CMSInitiatingOccupancyFraction当老年代浮动垃圾占比达到该值时,开始执行FullGC

-XX:+CMSScavengeBeforeRemark执行FullGC前先使用ParNewGC对新生代进行清理,减少老年代对新生代的引用,降低重新标记阶段的资源开销

-XX:UseCMSCompactAtFullCollection+-XX:CMSFullGCsBeforeCompaction在执行FullGC之后进行老年代内存压缩,降低内存碎片出现频率

# G1(JDK9默认)

-XX:+UseG1GC :G1(区域间复制 + 标记-整理

# 回收原理
# 堆内存分区

在进行区域间复制的过程中,同时实现了标记-整理功能,降低内存碎片化概率

  • Humongous区(巨型对象区)
# 回收流程
# STW

-XX:MaxGCPauseMillis

默认STW时间为200ms

  1. # Young Collection
    # 跨代引用、Remembered Set与卡表
  2. # Mixed Collection

    触发时机控制:-XX:InitiatingHeapOccupancyPercent(默认值45%)

    1. 初始标记(STW)
    2. 根区域扫描
    3. 并发标记(STW)
    4. 最终标记(STW)
    5. 拷贝存活(Evacuation(STW):老年代垃圾进行选择性回收,优先回收巨型对象)
# pre-write barriersatb_mark_queue
  • # ZGC

  • # Epsilon、Shenandoah

# GC性能衡量

# 衡量指标

# 吞吐量

$\Huge{\frac{GC耗时}{应用程序耗时 + GC耗时}}$

# 停顿时间
# GC频率

# 衡量工具

**top**查看进程的内存使用情况

# jstack

jstack [PID]查看具体线程的堆栈与状态信息

# jstat

jstat -class [PID]/jstat -compiler [PID]查看类加载/类编译的数量

jstat -gc [PID] [打印间隔/ms] [打印次数]查看堆中各个分区的内存占用情况及GC情况

# jmap

jmap -heap [PID]查看堆中各个分区的内存占用情况,以及所用垃圾收集器的设置类型

jmap -histo[:live] [PID] | more查看堆内存中的对象数目、大小统计直方图

jmap -dump:format=b,file=./heap.hprof [PID]输出堆dump文件结合jhat/MAT进行分析

-XX:HeapDumpOnOutOfMemoryError在发生 OOM 时输出堆dump文件

# jhat

jhat -port [PORT] ./heap.hprof

# jinfo

jinfo -flags [PID]

# ==调优==

# GC调优

-XX:+PrintGC 输出 GC 日志
-XX:+PrintGCDetails 输出 GC 的详细日志
-XX:+PrintGCTimeStamps 输出 GC 的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出 GC 的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行 GC 的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径

可使用GC Viewer/GC Easy等工具对输出的GC日志文件进行分析,根据分析结果进行内存与GC调优

#

无用对象的回收

-XX:MaxTenuringThreshold新生代对象晋升年龄阈值(最大值15)

-XX:PretenureSizeThreshold 新生代对象直接晋升大小阈值

# 降低Minor GC频率
  • 通过进一步增加新生代内存以降低Minor GC频率

    若新生代对象存活时间普遍较长,则不可贸然增加新生代内存,否则反而会增加Minor GC时间

# 降低Full GC频率
  • 若新生代Surviver区内存紧张,则对象晋升年龄/大小可能降低(-XX:+UseAdaptiveSizePolicy),增大老年代Full GC概率

    故可通过适当增大堆内存空间,提高新生代对象晋升年龄与大小阈值进行缓解

# 方法区

无用常量与类的回收

# 内存调优

  • -Xms-Xmx建议配置相同的值,使得在GC后无需重新分隔计算堆区的大小
  • -Xmn新生代内存分配一般为堆内存空间的25%~50%(Sun推荐配置为整个堆内存大小的3/8)
  • 在JDK1.7中,新生代与老年代的默认内存占比为1:2,可通过–XX:NewRatio进行显式配置;年轻代中Eden区与To Survivor区、From Survivor区的比例为8:1:1,可通过-XX:SurvivorRatio进行显式配置
  • 在JDK1.8中,默认开启-XX:+UseAdaptiveSizePolicy配置项,将对堆中各个区域的空间大小(不再遵循以上默认值)以及进入老年代的晋升年龄进行动态调整
    • 若新生代(Eden区)对象创建较多,可考虑将该配置项关闭并显式配置–XX:NewRatio-XX:SurvivorRatio,固定Eden区空间占用比例

# 类加载

将类Class文件中的二进制数据读入内存,并放入运行时数据区的方法区内,然后在内存(HotSpot实现为方法区而非堆)中创建对应的Class对象

  1. # 加载

  2. # 连接

    1. # 验证
    2. # 准备:

      为类静态变量分配内存并赋默认值

    3. # 解析:

      将类中符号饮用转换为直接引用

  3. # 初始化

    为类静态变量赋初值(按照初始化语句的声明顺序执行)

  4. # 类实例化

    Java编译器为编译的每一个类都生成一个实例初始化方法(<init>方法)(对静态实例生成<clinit>静态初始化方法)

# 类初始化的时机

# 主动使用
  • 启动类
  • 创建类的实例或子类实例(子类进行初始化,前提是其所有父类都已经完成初始化过程)(这一结论不适用于接口
  • 访问或修改类的静态成员变量,访问类的静态成员方法(严格区分类静态变量的定义场所,若访问子类继承而来的父类静态变量,则父类被初始化,而子类不会被初始化)
  • 通过反射访问类
# 被动使用

创建该类型的数组或集合对象(JVM在运行时动态创建数组类型:前缀带有[L表示一维数组,带有[[L表示二维数组)

类加载器在类可能被主动使用之前将会对其进行预先加载,如果字节码文件缺失,则只有当该类被首次主动使用时,类加载器才会报告LinkageError错误

# 类加载器

# JVM自带的类加载器
  • Bootstrap类加载器
  • 扩展类加载器
  • 系统(应用)类加载器
# 用户自定义的类加载器

java.lang.ClassLoader的子类

# 双亲委派

# 字节码

# 字节码结构

常量池

方法表集合

# 解释与编译

默认mixed模式

  • -Xint
  • -Xcomp

前端编译

运行时编译

# 即时(JIT)编译

# JIT类型与分层编译
  • C1
  • C2
# JIT触发时机
  • 方法调用计数器
  • 回边计数器
    • 栈上替换(OSR)编译
# JIT常用优化策略
  • 方法内联

    JIT是否进行内联优化,与方法体大小及方法调用频率相关

    在编程实践中,避免在一个方法中写大量代码,优先考虑将代码分散至多个小方法体中

  • 逃逸分析

    • 栈上分配(未实现❓)
    • 锁消除
    • 标量替换
# AOT编译器
# Graal编译器

# 助记符

  • # ldc

    int/float/String类型的常量值从常量池推送至()栈顶

    # bipush

    short/char/int等单字节(-128~127)常量值从常量池推送至()栈顶

    # sipush

    将短整型(-32768~32767)常量值从常量池推送至()栈顶

    # iconst_[m1-5]

    将[m1-5]从常量池推送至()栈顶

  • # anewarray

    创建一个引用类型(类、接口、数组)的数组,并压入栈顶

#