类加载
装载:将class文件转化为二进制文件,并将类描述信息放入方法区,将类对象存入堆
链接:验证文件是否正确后,将静态成员放入方法区并为静态变量赋初始值,将类的符号引用转换为直接引用
初始化:将静态变量赋值,并运行静态代码块
类加载器
启动类加载器:加载核心类库;
拓展类加载器:加载拓展类库;
应用类加载器:加载自己写的;
双亲委派模型:父类加载器先处理
打破双亲委派模型:自定义类加载器,继承继承 ClassLoader 类,重写 loadClass 方法(tomcat就是这样干的)
JVM内存模型
线程独享区:线程销毁后数据就被回收
线程共享区:线程销毁后数据不会立即回收,达到GC的阈值后回收
程序计数器:存放当前线程执行语句的地址
虚拟机栈:存放当前线程声明的变量
基本数据类型存值,引用数据类型存地址,都占4个字节
本地方法栈:存储非JAVA语言执行产生的数据
虚拟机栈
栈帧:执行到方法将方法压栈,完成后弹栈
每一个线程都会对应一个虚拟机栈,线程中的每个方法都会创建一个栈帧,存放本次方法执
行过程中所需要的所有数据。
如果我们一个线程中有多个方法的嵌套调用,虚拟机栈会对栈帧进行压栈和出栈操作。正在
执行的方法一定在栈顶,我们只能获取栈顶的栈帧,栈帧在虚拟机栈中先进后出。
栈帧结构:
常量数据存放在常量池中,常量池在方法区中
栈溢出:栈内存常为256K~1M
栈内存调优指令
-Xss256k //内存大小
栈内存太小容易溢出,栈内存太大影响线程数量
方法区(元空间)
存储类信息,静态变量,常量(jdk8后不存储字符串常量)
JVM 执行引擎
执行引擎是 Java 虚拟机核心的组成部分之一。JVM 的将字节码装载到内存,但字节码并不能够直接运行在操作系统之上。为了执行内存中的字节码文件指令,执行引擎(Execution Engine)就要将字节码指令
解释/编译为对应平台上的
本地机器
指令。
执行引擎的翻译过程有两种:1、通过解释器将字节码文件转为机器指令执行;2、使11用即时编译器(JIT)将字节码文件的二进制流编译成机器指令执行。
目前市面的主流 JVM 采用解释器与即时编译器并存的架构。在 Java 虚拟机运行时,解
释器
和
即时编译器相互协作。
解释器每次解释都会将字节码文件解释为机器指令。整体效率较低,但当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。
即时编译器则会将字节码文件编译为机器指令,存在方法区中,编译完成后直接执行本地机器指令即可。
当 Java 虚拟器启动时,解释器首先发挥作用,不必等待即时编译器全部编译完成后再执行。
堆内存
对象头
MarkWord:一系列标记位(哈希码、分代年龄、锁状态标记等),在 64 位系统中占 8 字节。
ClassPoint:对象对应的类信息的内存地址,在 64 位系统中占 8 字节。
Length:数组对象特有,表示数组长度,占 4 字节。
实例数据:包含了对象的所有成员变量
对其填充:将类内存填充为8字节的整数倍
内存划分
老年代:
对象会优先分配到新生代内存中,每次 GC 后没有回收的对象年龄加 1,年龄到 15 还没有被回收,对象会存放到老年代内存中;如果对象较大,超过新生代内存的一半,对象也会存放到老年代区域。
新生代:
为了减少young区垃圾回收后的空间碎片,新生代又分为Eden区和两个Survivor 区,且始终有一个 Suvivor 区保持闲置。对象会先存放到 Eden 区当中,Eden 区空间满了之后会进行 young区的垃圾回收,之后将 young 区所有存活的对象复制到闲置的 Suvivor 区中,并清空 Eden 区和正在使用的 Survivor 区。
老年代垃圾回收
浪费时间:老年代没有分区,垃圾回收后会整理碎片。JVM调优要尽可能减少oldGC次数。oldGC往往伴随着youngGC,oldGC + youngGC = fullGC。
新生代垃圾回收
新生代区域的垃圾回收称之为 YoungGC,也叫 MinorGC,Eden 区满后会触发 YoungGC
设置堆内存指令
-Xms10M(新生代) -Xmx10M(老年代)
面试3问:
1. Survivor 区空间并不大,如果满了怎么办?
一般情况下 GC 会回收 95%的对象,且超过 15 次 GC 的对象会存放到 old 区,所以 Survivor 区不容易满。
如果 Survivor 区满了,会触发担保机制,提前将对象存入 Old 区。
2. 为什么需要 Survivor 区?
为了减少垃圾回收带来的空间碎片,空间碎片过多会频繁触发 YoungGC。
3. 为什么需要两块 Survivor 区?
为了减少 Survivor 区的空间碎片。
垃圾回收
可达性分析
引用计数法
如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到。那么这个对象就成为可被回收的对象了。这种方式实现简单,效率较高,但是它无法解决循环引用的问题(python采用)
可达性分析
以一个 G
C Root
对象作为起点进行搜索,如果在 GC Roots 和对象之间没有可达路径,则称该对象是不可达的
GC Root 对象
-
栈帧中的本地变量表中引用的对象
-
-
方法区中常量引用的对象
-
本地方法栈中引用的对象
垃圾回收算法
标记——清除算法
效率较低,要扫描两次堆内存,第一次标记不可达对象,第二次清除标记的对象,有空间碎片。old 区使用的算法
复制算法
空间碎片少,但会浪费空间。存活对象较少才会使用的算法。young 区使用的算法
标记——整理算法
空间碎片少,效率较低。old 区使用的算法
垃圾收集器
垃圾收集器的执行效率
= 吞吐量 / 停顿时间
吞吐量
= 用户代码执行时间 / (用户代码执行时间 + 停顿时间)
串行收集器
只有一个垃圾回收线程,在垃圾回收时暂停用户代码线程,如 Serial 和 Serial Old 收集器
并行收集器
吞吐量优先,多个垃圾收集器线程共同工作, 尽快完成垃圾收集。如 ParNew,Parallel Scanvenge, Parallel Old 收集器
并发收集器
停顿时间优先,用户线程和垃圾回收线程一同工作,用户代码线程也会完全停止一小段时间,如 CMS,G1 收集器
CMS 收集器
CMS(concurrent mark sweep,并发标记扫描)收集器是并发收集器,是基于标记——清理的算法进行垃圾回收,用于 OldGC。
优点:并发收集、低停顿
缺点:会产生大量空间碎片,停顿时间虽然短但是不可控
G1 收集器
G1(garbage first,垃圾优先)收集器是并发收集器,从 JDK1.7 开始支持,能进行 oldGC 和 YoungGC。Old 区采用标记整理算法,Young 区采用复制算法。
G1 收集器没有固定的 Old、Young、Eden、Survivor 区,而是将内存分为一个个大小相等的 Region(格子,1Mb~32Mb)。每次垃圾回收后,Region 的用途可以发生改变,提高了内存的灵活性和利用率
G1 收集器可以根据开发者设置的参数,停顿时间的期望值,优先筛选回收存活的对象比较少,垃圾对象比较大的区域 Region,可以把更多空余的空间释放出来
ZGC 收集器
ZGC 从 JDK11 开始支持,目前还是一个实验性版本,原理类似 G1。是目前收集效率最高的垃圾收集器,平均暂停时间为 0.05 毫秒
JVM 参数设置
JVM 参数类型
标准参数
不随 jdk 版本的变化而变化的参数,如:-version
-X 参数
不能保证所有的 JVM 都支持。
如: -Xcomp:使用即时编译器执行字节码文件
-Xint:使用解释器执行字节码文件
-Xmixed:混合模式,先使用解释器,即时编译器编译好后执行机器指令。
-XX 参数
不能保证所有的 JVM 都支持
Boolean 类型参数: -XX:+UseG1GC:使用 G1 收集器 ;-XX:-UseG1GC:不使用 G1 收集器
Key-Value 类型参数:
-XX:MaxTenuringThreshold=15:对象年龄达到 15 就会进入老年代
jinfo:实时查看 JVM 参数
jinfo -flag InitialHeapSize PID:JAVA 进程堆内存大小
jinfo -flag UseG1GC PID:JAVA 进程是否使用 G1GC
jinfo -flag UseParallelGC PID:JAVA 进程是否使用 ParallelGC
jstat:虚拟机性能信息
jstat -class PID 1000:每秒查看一次虚拟机中类加载信息
jmap:打印快照
jmap -heap PID:查看堆存储快照
jmap -dump:format=b,file=heap.hprof PID:出现内存溢出异常时,将堆内存信息下载到文件中
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof //项目在发生 OOM 异常时自动下载堆内存信息
异常信息会生成在项目的根目录下
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:gc.log
打印GC相关信息,生成日志
GC 调优
不要手动设置新生代和老年代的大小,只设置堆的大小
不断调优暂停时间目标:一般情况设置到 100ms 或者 200ms 都是可以的,但如果设置成 50ms 就不太合理。暂停时间太短,会导致 GC 跟不上垃圾产生的速度
适当增加堆内存大小
高并发环境下配置堆和垃圾回收器
计算内存大小和高并发下的请求数,设置高并发时间内不要进行GC。将短时间高并发内将young区和old区内设置成能支撑的内存。
JVM 问题的排查