01 JVM 内存
1 内存分区
1)线程私有
- 程序计数器
- 行号指示器
- 虚拟机栈
- (方法执行)栈帧,局部变量表(基本数据类型、对象引用和 returnAddress 类型)
- 本地方法栈
2)线程共享
- 堆
- 对象实例
- 字符串常量池:JDK1.7 之后移到堆空间
- 方法区
- 已加载的类信息、类常量、即时编译后的代码
- 运行时常量池(类文件中字面量,符号引用,直接引用)
- HotSpot 实现
- 永久代(PermGen):jdk 1.8 之前,HotSpot 对方法区的永久代 GC 实现
- 元空间(Metaspace):jdk 1.8 之后元空间替代了永久代,使用本地内存
- 直接内存
- 非JVM内存;NIO
2 对象创建
- 类加载
- 先检查常量池,若未加载则执行类加载
- 内存分配
- (默认)同步分配(CAS + 失败重试)
- (需配置)本地线程分配缓冲(TLAB),各个线程独立分配
- 初始化零值
- 设置对象头
- 自身运行时数据
- 类型指针(直接指针,指向类元数据)
- 执行方法
- 按照程序员意愿初始化
3 内存溢出
通过内存映像分析工具对 dump 出来的堆转储快照进行分析
- 栈溢出
- StackOverflowError
- 栈深度超限、栈空间不够
- 堆溢出
- OutOfMemoryError: Java heap space
- 内存泄漏:查看泄漏对象到 GC Roots 的引用链,确认为什么无法回收
- 内存溢出:检查堆参数,或检查对象生命周期、持有时间
- 方法区溢出
- OutOfMemoryError: PermGen space
- 引用了大量的第三方 jar,对过多 JSP 进行 pre compile
02 GC 与内存分配
1 GC 判定
- 可达性分析算法
- 一个对象的根节点,不是 GC Roots 的话,就可以被回收。
- 引用类型
- 强引用:宁 crash 不收
- 软引用:内存不够就回收
- 弱引用:扫描到就回收
- 虚引用:随时回收
2 GC 算法
1)分代选择
- 新生代
- 默认 Eden : Survivor : Survivor = 8:1:1
- “复制”算法(使用 Eden 和一块 Survivor,回收时将存活对象复制到另一块 Survivor。需老年代担保)
- 老年代
- “标记-整理”算法(标记回收对象,移动存活对象到一端,清理另一端内存。)
2)算法实现
- OopMap:不需要每次 GC 都从头进行可达性分析,使用 OopMap 记录类型的映射
- 安全点:只在安全点(循环的末尾 、方法临返回前、可能抛异常的位置)更新 OopMap
- 主动式中断:设置一个标志,线程运行到安全点时轮询该标志,发现标志被设置为真时,线程中断挂起
- 安全区域:线程进入 Sleep 或 Blocked 状态时,标记进入了安全区域,不需要更新 OopMap
3 垃圾收集器
1)Java 9 以前
- 新生代
- Serial:单线程,复制,Java 9 以前默认收集器
- ParNew:多线程,复制
- Parallel Scavenge:多线程,复制,吞吐量优先
- 老年代
- Serial Old:单线程,标记整理。Java 9 以前,Client 模式下默认收集器。
- Parallel Old:多线程,标记整理,吞吐量优先
- CMS:多线程,标记-清除,低停顿
2)JDK 9 - G1
Java 9 开始作为默认的垃圾收集器(Java 7 发布)
- 并行与并发:支持多 CPU、多核
- 区域划分:将整个堆空间划分为多个大小相等的 Region,新生代和老年代不再物理隔离,都是一部分 Region 集合。
- 可预测的停顿:区域划分避免了全区域的 GC
- GC 算法:整体“标记-整理”,局部(两个Region之间)“复制”
3)JDK 11 - ZGC
- 仅支持 64 位平台,最大支持 4Tb 堆
- 承诺在数 TB 的堆上具有非常低的暂停时间(10ms 以内)
4 内存分配
- 触发 GC:分配对象时无法找到连续内存空间
- 对象优先在 Eden 分配:Eden 没有足够空间时会发起一次 Minor GC
- 大对象直接进老年代:对象大小阈值可以参数配置,默认是 0,直接进入 Eden
- 长期存活的对象将进入老年代:对象每次从 Eden 存活到 Survivor 时年龄加 1,默认年龄 15 之后移到老年代
- 动态对象年龄判定:如果 Survivor 中相同年龄的对象的大小达到总大小的一半,不小于这个年龄的对象直接进入老生代,无需等到年龄门槛
- 空间分配担保:Minor GC 前看老年代剩余空间够不够装新生代失败的,不够就 FULL GC
03 性能监控
- 命令行工具
- jps:列出正在运行的虚拟机进程
- jstat:监视虚拟机各种运行状态信息
- jinfo:实时地查看虚拟机各项参数
- jmap:生成堆转储快照(heapdump 或 dump 文件)
- jhat:分析 jmap 生成的 heapdump(比较简陋,用得少)
- jstack:生成线程快照(threaddump 或 javacore 文件),定位线程出现长时间停顿的原因
- 命令行工具
- JConsole:Java监视与管理控制台
- VisualVM:多合一故障处理工具,对应用程序的实际性能影响很小
04 类加载
类的整个生命周期:加载、连接(验证、准备、解析)、初始化、使用和卸载;
1)类加载过程
- 加载
- 读取二进制流
- 转化为运行时数据结构
- 生成 Class 对象
- 验证:确保Class文件的字节流信息符合虚拟机的要求
- 准备:为类变量分配内存并设置零值,类常量则是直接赋值;
- 解析:将常量池内的符号引用替换为直接引用
- 初始化
- 执行类构造器方法:由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生
- 类初始化时要求父类全部初始化过;接口初始化,只有真正用到父接口时才初始化
- 初始化顺序:先初始化静态成员,然后从父类到子类中,依次初始化非静态对象和构造器。
- 父类静态初始化块
- 子类静态初始化块
- 父类初始化块
- 父类构造器
- 子类初始化块
- 子类构造器
2)类加载器
- 类加载器:实现“通过一个类的全限定名来获取描述此类的二进制字节流”的代码模块
- 比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义;
- 双亲委派模型
- 除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器。
- 处理类加载的请求时,加载器先把请求委派给父类加载器去完成,只有父类加载器无法完成这个加载时,子加载器才会尝试自己去加载
- 主要是为了安全性,避免用户自己编写的类动态替换 Java 的一些核心类,也避免了类的重复加载
- 自定义类加载器
- 只需要继承 ClassLoader,并覆盖 findClass 方法
3)热部署与热加载
- 类层次划分、OSGI、热部署、代码加密
- 热加载
- 在容器启动的时候起一条后台线程,定时的检测类文件的时间戳变化,如果类的时间戳变掉了,则将类重新载入。
- 生产环境基本没有应用,开发环境可以提升开发效率(tomcat 热加载 JSP 生成类)
- 热部署
- 在服务器运行时直接重新加载整个应用
- 生产环境基本只在云计算领域有应用,开发环境基本热加载
- tomcat 默认支持热部署,好处是在一个 tomcat 多个项目时,不必因为 tomcat 停止而停止其他的项目