JVM
TIOBE语言热度排行表:https://www.tiobe.com/tiobe-index/
1995年5月23日,Java语言诞生。
1998年2月,JDK1.1被下载超过2000000次
JVM是一个抽象的计算机,就意味着一定遵循冯·诺依曼计算机体系结构
冯·诺依曼计算机体系结构
Java结构图
Java源文件 Xxx.java
二进制文件 Xxx.class
所有class文件都是以 ca fe ba be开头的(魔数开头) 0xCAFEBABE
常量池 >> 静态常量池、运行时常量池、字符串常量池,常量池是从1开始的,count-1
静态常量池:字面量(文本、字符串、final修饰的静态变量),符号引用(类的描述信息)
运行时常量池:
字符串常量池:
javap -p -v Xx.class > xx.txt 反编译class文件到文本文件
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1
- 通过javac将源文件编译成二进制文件,交给jvm运行,jvm可以屏蔽操作系统的区别
类加载
字节码问价 --> 加载到内存 --> 生成数据访问入口(对象)
加载、连接、初始化
加载字节码的方式
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将Java源文件动态编译为.class文件,也就是运行时计算而成
- 从加密文件中获取
类加载过程(按照顺序开始,不一定按照顺序结束)
-
装载
- 通过全限定名获取二进制字节流(虚拟机外部实现,实现方式为类加载器)
-
链接-验证
-
文件格式验证
- 是否以16进制cafebabe开头
- 版本号是否正确
-
将获取的数据放入方法区
-
元数据验证(验证Java语法)
- 是否有父类
- 是否继承了final类
-
字节码验证
- 运行检查
- 栈数据类型和操作码操作参数是否吻合
-
符号引用验证
- 常量池中描述类是否存在
- 访问的方法或者字段是否存在且具有足够的权限
idea vmoptions参数:-Xverify:none 取消验证
-
-
链接-准备
-
为ConstantValue赋值
-
为变量赋值初始值
被final修饰的静态变量会编译成ConstantValue
-
-
链接-解析(该步骤顺序不固定,Java中存在运行时绑定)
-
将符号引用转换为直接引用
可能多次解析符号引用,第一次解析失败后续解析直接返回失败,invokedynamic指令除外(Java7引入,Java8开始使用,用于lambda表达式)
invokedynamic指令允许动态解析
-
-
初始化(将初始化得到的对象存入Java堆中)
-
为类变量设置初始值(只有两种方式)
指令类变量 int i = 1;
静态代码块(静态变量必须写在静态代码块之前)
-
类初始化(jvm按需加载)
- 父类静态字段,静态代码块
- 子类静态字段,静态代码块
- 父类字段,代码块
- 父类构造函数
- 子类字段,代码块
- 子类构造函数
-
-
使用
-
卸载
- 该类所有的实例都已经被回收,Java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
初始化什么时候会被触发
-
主动使用
-
创建类的实例,new
-
访问静态变量,或者对静态变量赋值
-
调用静态方法
-
反射(如:Class.forName(“xx”))
-
初始化子类,则父类被初始化
-
Java虚拟机启动时被标明为启动类的类,直接使用‘java.exe’命令来运行的主类
-
通过子类调用父类的静态字段或方法不会初始化子类
-
引用类的ConstantValue不会引起初始化
-
定义类数组不会引起初始化
-
类加载器
- Bootstrap ClassLoader 启动类加载器 --> 加载$JAVA_HOME中jre/lib/rt.jar包里所有的class或者Xbootclasspath选项指定的jar包
- ExtClassLoader 扩展类加载器 --> 加载Java平台中扩展功能的jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包
- AppClassLoader 系统类加载器 --> 加载classpath中指定的jar包及-Djava.class.path所指定目录下的类和jar包
- Custom ClassLoader --> 自定义类加载器 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如Tomcat,jboss都会根据j2ee规范自行实现类加载器
按顺序从顶层往下的父子关系
类加载机制
全盘负责 加载所有相关连的类
父类委托(双亲委派) 由父类优先加载
缓存机制 所有加载过的类会缓存到内存中
SPI必须打破双亲委派
OSGI模块化热部署 代码热部署 代码热替换,每一个程序模块拥有自己的类加载器
Java运行时数据区
方法区
线程共享的区域
存储类的结构信息、运行时常量池、字段、方法数据、结构方法
方法区是和虚拟机一起创建的,生命周期和虚拟机进程绑定,是堆的逻辑的一部分,也叫非堆内存
内存不够用时抛出OutOfMemoryError
Java1.7及之前方法区的实现为Perm Space,使用JVM自身内存空间
Java8开始实现方式为元空间(元数据区Meta Space),使用直接内存空间
运行时常量池
虚拟机在完成类装载之后会将class文件中的constant_pool载入到内存中,并存入方法区
是方法区的一部分
内存不够用时抛出OutOfMemoryError
包括字符串常量池和其他基本类型的封装类常量池(不包含浮点类型)
*静态常量池
*字符串常量池
存储String常量
堆
线程共享的区域
是Java虚拟机管理的最大的一块内存,在虚拟机启动时创建,生命周期和虚拟机进程绑定
Java中对象数组和实例数据都会在堆上分配
默认大小
客户端JVM默认初始最大堆大小
物理内存192兆字节(MB)之内,默认的最大堆大小是物理内存的一半,否则物理内存的四分之一,初始堆大小至少为8MB,否则物理内存的1/64,分配给年轻代的最大空间是总堆积面积的三分之一
服务器JVM默认初始和最大堆大小
在32位JVM上,如果有4GB或以上的物理内存,默认的最大堆大小最高可达1GB。在64位JVM上,如果有128GB或以上的物理内存,默认的最大堆大小可以高达32GB。
指定初始和最大堆大小
您可以使用标志-xms (初始堆大小)和-xmx(最大堆大小)指定初始和最大堆大小。如果您知道应用程序需要多少堆才能正常工作,您可以将置为相同的值。如果没有,JVM将从使用初始堆大小开始,然后增长Java堆,直到它在堆使用和性能之间找到平衡
内存不够用时抛出OutOfMemoryError
栈
本地方法栈
执行native方法的区域,通常在创建线程时为每一个线程分配属于自己的本地方法栈
虚拟机栈
执行Java方法的区域,保存线程执行的运行状态
栈空间不足时会抛出StackOverflowError
程序计数器
每个线程都有自己的程序计数器
记录线程指令执行位置
如果是Java方法则记录线程执行的字节指令位置,如果是本地方法则为空
栈桢
代表着一个方法的执行invoke
用于存储数据和部分结果,以及执行动态链接、返回方法值和调度异常
默认大小1M,设置栈桢大小的JVM参数 :-Xss256K
局部变量表
存储方法参数和变量
操作数栈
存储操作数
动态链接
支持方法的动态调用的过程
符号引用变成直接引用
返回地址
方法的返回
即时信息
不同版本的高度信息
动态链接、返回地址、即时信息都是栈桢的信息
Java对象在内存中的存储
Java对象内存布局
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
jol 查看类对象内存布局的工具
小端存储 便于数据类型转换
大端存储 便于数据符号判断
句柄池访问对象
在内存中维护一个句柄池,每一个句柄维护指向实例对象的指针和指向对象类型数据的指针
直接指针访问对象
在内存中的对象实例自身维护一个指向对象类型数据的指针
Hotsport使用的就是直接指针访问对象的方式
直接引用比句柄池块一倍,但是需要额外的指针存储空间,空间还换时间
JVM内存模型
Eden区
新对象生成的区域
Survivor 0/1(Survivor From/To)
对象在Eden区minor gc后存活的对象进入Survivor区
Old区(老年代)
- 对象默认在经过15次gc后还存活则进入老年代
- Survivor空间中相同年龄的对象总和大于设定阈值则将年龄大于等于该年龄的对象进入Old区
- Survivor区中年龄大于设定年龄限制则进入Old区
- 担保机制,无法在Eden区或Survivor区中无法生存的对象会在Old区生成(新生代空间不够用时或者对象大于设定阈值)
默认划分比例 1:1:8:20 S0:S1:Eden:Old
Java对象生成过程
垃圾回收机制分类
Partial GC 部分GC
Young GC/Minor GC
- Eden区和Survivor区剩余空间不够
- Full GC触发Minor GC
- CMS Old GC触发
Old GC
Full GC 全GC
悲观策略
- 之前Minor GC每一次平均晋升对象大小大于Old区剩余空间时,这一次Minor GC触发Full GC
- Minor GC之后存活的对象总大小大于Old区剩余空间时,触发Full GC
常规策略
- MetaSpace空间不足直接触发Full GC,一般情况下MetaSpace触发GC一定是出现内存泄露
- 调用System.gc()通知GC操作,触发的一定是Full GC
jvisualvm 打开Java VisualVM
垃圾回收
垃圾回收策略
引用计数法
一个对象的引用计数为零时,该对象可以被回收
引用计数会出现循环引用问题
Java已经不再使用引用计数法
可达性分析/根搜索算法
从根出发单条引用链可到达的对象就是存活对象,否则为可回收对象
图论
GC Root
- 虚拟机栈中栈桢的局部变量表中的元素
- 方法区的静态变量和常量
- 本地方法栈的JNI中的元素
STW
Stop the world 垃圾回收和业务线程不同时执行,执行GC操作时,暂停所有用户线程执行操作
引用
- 数据类型必须是Reference
- 储存的数据指向另一块内存
强引用
在Java中最常见的就是强引用,也是我们在开发过程中经常会使用到的引用.把一个对象赋给一个引用变量,这个引用变量就是一个强引 用。当一个对象被强引用变量引用时,它处于可达状态,是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之 一
软引用(Soft)
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
弱引用(Weak)
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
虚引用(Phantom)
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态
Java对象的生命周期
创建阶段
为对象分配存储空间,开始构造对象,从超类到子类对static成员进行初始化,超类成员变量按顺序初始化,递归调用超类的构造方法,子类成员变量按顺序初始化,子类构造方法调用,一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到了应用阶段
应用阶段
对象至少被一个强引用持有着
不可见阶段
程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的
简单说就是程序的执行已经超出了该对象的作用域了
这种情况下,该对象仍可能被 JVM 等系统下的某些已装载的静态变量或线程或 JNI 等强引用持有着,这些特殊的强引用被称为GC root
。存在着这些GC root
会导致对象的内存泄露情况,无法被回收
不可达阶段
该对象不再被任何强引用所持有
收集阶段
当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了 finalize()
方法,则会去执行该方法的终端操作。
这里要特别说明一下:不要重载
finazlie()
方法! 原因有两点:会影响JVM的对象分配与回收速度在分配该对象时,JVM需要在垃圾回收器上注册该对象,以便在回收时能够执行该重载方法;在该方法的执行时需要消耗CPU时间且在执行完该方法后才会重新执行回收操作,即至少需要垃圾回收器对该对象执行两次GC。
可能造成该对象的再次“复活” 在
finalize()
方法中,如果有其它的强引用再次持有该对象,则会导致对象的状态由“收集阶段”又重新变为“应用阶段”。这个已经破坏了Java对象的生命周期进程,且“复活”的对象不利用后续的代码管理。
终结阶段
当对象执行完finalize()
方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收
空间重分配阶段
垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了
什么时候会垃圾回收
GC是由JVM自动完成的,根据JVM系统环境而定,所以时机是不确定的。 当然,我们可以手动进行垃圾回收,比如调用System.gc()方法通知JVM进行一次垃圾回收,但是 具体什么时刻运行也无法控制。也就是说System.gc()只是通知要回收,什么时候回收由JVM决 定。但是不建议手动调用该方法,该方法执行的是Full GC,消耗的资源比较大
- 当Eden区或者S区不够用了
- 老年代空间不够用了
- 方法区空间不够用了
- System.gc()
垃圾收集算法
标记-清除(Mark-Sweep)
-
标记
找出内存中需要回收的对象,并且把它们标记出来
堆中所有的对象都会被扫描一遍,从而才能确定需要回收的对象,比较耗时
-
清除
清除掉被标记需要回收的对象,释放出对应的内存空间
缺点
- 标记和清除两个过程都比较耗时,效率不高
- 会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
标记-复制(Mark-Copying)
将内存划分为两块相等的区域,每次只使用其中一块,当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次清除掉
Survivor区
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都有100%存活的极端情况,所以老年代一般不能直接选用这种算法。
缺点
空间利用率降低
标记-整理(Mark-Compact)
标记过程仍然与"标记-清除"算法一样,但是后续步骤不是直接对可回收对象进行清理,而是让所有存活 的对象都向一端移动,然后直接清理掉端边界以外的内存
整理
任意顺序
以任意顺序将内存整理到一起
双指针算法
线性顺序
相关联的整理到一起
滑动顺序
所有的滑动到前面,剩下的全部回收
Lisp2
单次遍历算法 需要用到额外的表记录,标记位向量,偏移位向量,内存索引号
分代收集算法
-
Young区:复制算法(对象在被分配之后,可能生命周期比较短,Young区复制效率比较高)
-
Old区:标记清除或标记整理(Old区对象存活时间比较长,复制来复制去没必要)
*空间分配算法
新对象创建时为其分配内存空间
*最佳适应性算法
*首次适应性算法
*最差适应性算法
JVM参数
标准参数
-version
-help
-server
-cp
非标准参数
-X参数
-Xint 解释执行
-Xcomp 第一次使用就编译成本地代码
-Xmixed 混合模式,JVM自己来决定
-XX参数
布尔类型
格式:-XX:[±] +或-表示启用或者禁用name属性
比如:-XX:+UseConcMarkSweepGC 表示启用CMS类型的垃圾回收器
-XX:+UseG1GC 表示启用G1类型的垃圾回收器
name=value类型
格式:-XX= 表示name属性的值是value
比如:-XX:MaxGCPauseMillis=500 每次GC最大的停顿毫秒数
-XX:initialHeapSize=100M 设置堆初始大小为100M
-XX:maxHeapSize=100M 设置堆最大大小为100M
其他参数
-Xms1000M等价于-XX:InitialHeapSize=1000M
-Xmx1000M等价于-XX:MaxHeapSize=1000M
-Xss100等价于-XX:ThreadStackSize=100
*默认以K为单位
所以这块也相当于是-XX类型的参数
查看参数
java -XX:+PrintFlagsFinal -version > flags.txt
设置参数的常见方式
- 开发工具中设置比如IDEA,eclipse
- 运行jar包的时候:java -XX:+UseG1GC xxx.jar
- web容器比如tomcat,可以在脚本中的进行设置
- 通过jinfo实时调整某个java进程的参数(参数只有被标记为manageable的flags可以被实时修改)
常用参数含义
参数 | 含义 | 说明 |
---|---|---|
-XX:CICompilerCount=3 | 最大并行编译数 | 如果设置大于1,虽然编译速度会提高, 但是同样影响系统稳定性,会增加JVM崩溃的可能 |
-XX:InitialHeapSize=100M | 初始化堆大小 | 简写-Xms100M |
-XX:MaxHeapSize=100M | 最大堆大小 | 简写-Xms100M |
-XX:NewSize=20M | 设置年轻代的大小 | |
-XX:MaxNewSize=50M | 年轻代最大大小 | |
-XX:OldSize=50M | 设置老年代大小 | |
-XX:MetaspaceSize=50M | 设置方法区大小 | |
-XX:MaxMetaspaceSize=50M | 方法区最大大小 | |
-XX:+UseParallelGC | 使用UseParallelGC | 新生代,吞吐量优先 |
-XX:+UseParallelOldGC | 使用UseParallelOldGC | 老年代,吞吐量优先 |
-XX:+UseConcMarkSweepGC | 使用CMS | 老年代,停顿时间优先 |
-XX:+UseG1GC | 使用G1GC | 新生代,老年代,停顿时间优先 |
-XX:NewRatio | 新老生代的比值 | 比如-XX:Ratio=4,则表示新生代:老年代=1:4, 也就是新生代占整个堆内存的1/5 |
-XX:SurvivorRatio | 两个S区和Eden区的比值 | 比如-XX:SurvivorRatio=8,也就是(S0+S1):Eden=2:8, 也就是一个S占整个新生代的1/10 |
-XX:+HeapDumpOnOutOfMemoryError | 启动堆内存溢出打印 | 当JVM堆内存发生溢出时,也就是OOM, 自动生成dump文件 |
-XX:HeapDumpPath=heap.hprof | 指定堆内存溢出打印目录 | 表示在当前目录生成一个 heap.hprof文件 |
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:g1-gc.log | 打印出GC日志 | 可以使用不同的垃圾收集器, 对比查看GC情况 |
-Xss128k | 设置每个线程的堆栈大小 | 经验值是3000-5000最佳 |
-XX:MaxTenuringThreshold=6 | 提升年老代的最大临界值 | 默认值为 15 |
-XX:InitiatingHeapOccupancyPercent | 启动并发GC周期时堆内存使用占比 | G1之类的垃圾收集器用它来触发并发GC周期, 基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示“一直执行GC循环”. 默认值为 45. |
-XX:G1HeapWastePercent | 允许的浪费堆空间的占比 | 默认是10%,如果并发标记可回收的空间小于10%,则不会触 发MixedGC |
-XX:MaxGCPauseMillis=200ms | G1最大停顿时间 | 暂停时间不能太小,太小的话就会导致出现G1跟不上垃圾产生的速度。 最终退化成Full GC。所以对这个参数的调优是一个持续的过程,逐步调整到最佳状态 |
-XX:ConcGCThreads=n | 并发垃圾收集器使用的线程数量 | 默认值随JVM运行的平台不同 而不同 |
-XX:G1MixedGCLiveThresholdPercent=65 | 混合垃圾回收周期中要包括的旧区域设置占用率阈值 | 默认占用率为 65% |
-XX:G1MixedGCCountTarget=8 | 设置标记周期完成后,对存活数据上限为 G1MixedGCLIveThresholdPercent 的旧区域执行混合垃圾回收的目标 次数 | 默认8次混合垃圾回收,混合回 收的目标是要控制在此目标次 数以内 |
-XX:G1OldCSetRegionThresholdPercent=1 | 描述Mixed GC时,Old Region被加入到CSet中 | 默认情况下,G1只把10%的Old Region加入到CSet中 |
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
Serial
最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。
它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程。
优点:简单高效,拥有很高的单线程收集效率
缺点:收集过程需要暂停所有线程
算法:复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器
Serial Old
Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,不同的是采用标记-整理算法
,运行过程和Serial收集器一样。
ParNew
可以把这个收集器理解为Serial收集器的多线程版本
优点:在多CPU时,比Serial效率高。
缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。
算法:复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器
Parallel Scavenge
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集 器,看上去和ParNew一样,但是Parallel Scanvenge更关注系统的吞吐量
吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。
若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务。
-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间
-XX:GCTimeRatio直接设置吞吐量的大小
Parallel Old
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法
进行垃圾回收,也是更加关注系统的吞吐量。
CMS
官网
: https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#concurrent_mark_sweep_cms_collector
CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间 为目标的收集器。
采用的是标记-清除算法
,整个过程分为4步
- 初始标记 CMS initial mark 标记GC Roots直接关联对象,不用Tracing,速度很快
- 并发标记 CMS concurrent mark 进行GC Roots Tracing
- 重新标记 CMS remark 修改并发标记因用户程序变动的内容
- 并发清除 CMS concurrent sweep 清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为 浮动垃圾
由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。
优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量,还会并发失败
backgroud模式为正常模式执行上述的CMS GC流程
forefroud模式为Full GC模式
相关参数:
//开启CMS垃圾收集器
-XX:+UseConcMarkSweepGC
//默认开启,与-XX:CMSFullGCsBeforeCompaction配合使用 -XX:+UseCMSCompactAtFullCollection
//默认0 几次Full GC后开始整理
-XX:CMSFullGCsBeforeCompaction=0 //辅助CMSInitiatingOccupancyFraction的参数,不然CMSInitiatingOccupancyFraction只会使 用一次就恢复自动调整,也就是开启手动调整。
-XX:+UseCMSInitiatingOccupancyOnly
//取值0-100,按百分比回收
-XX:CMSInitiatingOccupancyFraction 默认-1
注意:CMS并发GC不是“full GC”。HotSpot VM里对concurrent collection和full collection有 明确的区分。所有带有“FullCollection”字样的VM参数都是跟真正的full GC相关,而跟CMS并发GC无关 的。
G1(Garbage-First)
官网
: https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html#garbage_first_garbage_collection
使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂
如果对象太大,一个Region放不下[超过Region大小的50%],那么就会直接放到H中
设置Region大小:-XX:G1HeapRegionSize=M
所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域
- 分代收集(仍然保留了分代的概念)
- 空间整合(整体上属于
标记-整理
算法,不会导致空间碎片) - 可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)
工作过程
初始标记(Initial Marking) 标记以下GC Roots能够关联的对象,并且修改TAMS的值,需要暂停用户线程
并发标记(Concurrent Marking) 从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行
最终标记(Final Marking) 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程
筛选回收(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据 用户所期望的GC停顿时间制定回收计划
TLAB流程
相关参数
-XX: +UseG1GC 开启G1垃圾收集器
-XX: G1HeapReginSize 设置每个Region的大小,是2的幂次,1MB-32MB之间 -XX:MaxGCPauseMillis 最大停顿时间
-XX:ParallelGCThread 并行GC工作的线程数
-XX:ConcGCThreads 并发标记的线程数
-XX:InitiatingHeapOcccupancyPercent 默认45%,代表GC堆占用达到多少的时候开始垃圾收集
ZGC
官网
: https://docs.oracle.com/en/java/javase/11/gctuning/z-garbage-collector1.html#GUID-A5A42691-095E-47BA-B6DC-FB4E5FAA43D0
JDK11新引入的ZGC收集器,不管是物理上还是逻辑上,ZGC中已经不存在新老年代的概念了,会分为一个个page,当进行GC操作时会对page进行压缩,因此没有碎片问题
只能在64位的linux上使用,目前用得还比较少
- 可以达到10ms以内的停顿时间要求
- 支持TB级别的内存
- 堆内存变大后停顿时间还是在10ms以内
垃圾收集器分类
- **串行收集器 ** -> Serial和Serial Old
只能有一个垃圾回收线程执行,用户线程暂停
适用于内存比较小的嵌入式设备
- 并行收集器[吞吐量优先] -> Parallel Scanvenge、Parallel Old
多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
适用于科学计算、后台处理等若交互场景
- 并发收集器[停顿时间优先] -> CMS、G1
用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行
适用于相对时间有要求的场景,比如Web
常见问题
-
吞吐量和停顿时间
-
停顿时间 -> 垃圾收集器进行垃圾回收终端应用执行响应的时间
-
吞吐量 -> 运行用户代码时间/(运行用户代码时间+垃圾收集时间)
-
停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验;
高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
这两个指标也是评价垃圾回收器好处的标准
-
如何选择合适的垃圾收集器
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28
- 优先调整堆的大小让服务器自己来选择
- 如果内存小于100M,使用串行收集器
- 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
- 如果允许停顿时间超过1秒,选择并行或JVM自己选
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器
-
对于G1收集
JDK 7开始使用,JDK 8非常成熟,JDK 9默认的垃圾收集器,适用于新老生代
是否使用G1收集器?
- 50%以上的堆被存活对象占用
- 对象分配和晋升的速度变化非常大
- 垃圾回收时间比较长
-
G1中的RSet
全称Remembered Set,记录维护Region中对象的引用关系
试想,在G1垃圾收集器进行新生代的垃圾收集时,也就是Minor GC,假如该对象被老年代的Region中所引 用,这时候新生代的该对象就不能被回收,怎么记录呢? 不妨这样,用一个类似于hash的结构,key记录region的地址,value表示引用该对象的集合,这样就能知 道该对象被哪些老年代的对象所引用,从而不能回收。
-
如何开启需要的垃圾收集器
串行 -XX:+UseSerialGC -XX:+UseSerialOldGC
并行(吞吐量优先): -XX:+UseParallelGC -XX:+UseParallelOldGC
并发收集器(响应时间优先) -XX:+UseConcMarkSweepGC -XX:+UseG1GC
常用命令
jps
查看java进程
jinfo
-
实时查看和调整JVM配置参数
-
查看用法
jinfo -flag name PID 查看某个java进程的name属性的值
jinfo -flag MaxHeapSize PID
jinfo -flag UseG1GC PID -
修改
参数只有被标记为manageable的flags可以被实时修改
jinfo -flag [+|-] PID
jinfo -flag = PID -
查看曾经赋过值的一些参数
jinfo -flags PID
jstat
-
查看虚拟机性能统计信息
-
查看类装载信息
jstat -class PID 1000 10 查看某个java进程的类装载信息,每1000毫秒输出一次,共输出10次
-
查看垃圾收集信息
jstat -gc PID 1000 10
jstack
-
查看线程堆栈信息
-
用法
jstack PID
-
排查死锁案例
jmap
-
生成堆转储快照
-
打印出堆内存相关信息
jmap -heap PID
jinfo -flag UsePSAdaptiveSurvivorSizePolicy 35352 -XX:SurvivorRatio=8
-
dump出堆内存相关信息
jmap -dump:format=b,file=heap.hprof PID
-
要是在发生堆内存溢出的时候,能自动dump出该文件就好了
一般在开发中,JVM参数可以加上下面两句,这样内存溢出时,会自动dump出该文件 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
执行引擎
解释执行
Interpreter,解释器逐条把字节码翻译成机器码并执行,跨平台的保证。
刚开始执行引擎只采用了解释执行的,但是后来发现某些方法或者代码块被调用执行的特别频繁时,就会把这些代码认定为“热点代码”。
即时编译器
Just-In-Time compilation(JIT),即时编译器先将字节码编译成对应平台的可执行文件,运行速度快。
即时编译器会把这些热点代码编译成与本地平台关联的机器码,并且进行各层次的优化,保存到内存中。
JVM采用哪种方式
JVM采取的是混合模式,也就是解释+编译的方式,对于大部分不常用的代码,不需要浪费时间将其编译成机器码,只需要用到的时候再以解释的方式运行;对于小部分的热点代码,可以采取编译的方式, 追求更高的运行效率
即使编译器类型
-
HotSpot虚拟机里面内置了两个JIT:C1和C2
C1也称为Client Compiler,适用于执行时间短或者对启动性能有要求的程序
C2也称为Server Compiler,适用于执行时间长或者对峰值性能有要求的程序
-
Java7开始,HotSpot会使用分层编译的方式
AOT和Graal VM
AOT
在Java9中,引入了AOT(Ahead-Of-Time)编译器
即时编译器是在程序运行过程中,将字节码翻译成机器码。而AOT是在程序运行之前,将字节码转换为机器码
优势:这样不需要在运行过程中消耗计算机资源来进行即时编译
劣势:AOT 编译无法得知程序运行时的信息,因此也无法进行基于类层次分析的完全虚方法内联,或 者基于程序 profile 的投机性优化(并非硬性限制,我们可以通过限制运行范围,或者利用上一次运行 的程序 profile 来绕开这两个限制)
Graal VM
官网
: https://www.oracle.com/tools/graalvm-enterprise-edition.htmlGraalVM core features include:
- GraalVM Native Image, available as an early access feature –– allows scripted applications to be compiled ahead of time into a native machine-code binary
- GraalVM Compiler –– generates compiled code to run applications on a JVM, standalone, or embedded in another system
- Polyglot Capabilities –– supports Java, Scala, Kotlin, JavaScript, and Node.js
- Language Implementation Framework –– enables implementing any language for the GraalVM environment
- LLVM Runtime–– permits native code to run in a managed environment in GraalVM Enterprise
在Java10中,新的JIT编译器Graal被引入
它是一个以Java为主要编程语言,面向字节码的编译器。跟C++实现的C1和C2相比,模块化更加明显,也更加容易维护。
Graal既可以作为动态编译器,在运行时编译热点方法;也可以作为静态编译器,实现AOT编译。
除此之外,它还移除了编程语言之间的边界,并且支持通过即时编译技术,将混杂了不同的编程语言的代码编译到同一段二进制码之中,从而实现不同语言之间的无缝切换。
工具
jconsole
JConsole工具是JDK自带的可视化监控工具。查看java应用程序的运行概况、监控堆信息、永久区使用 情况、类加载情况等。
命令行中输入:jconsole
jvisualvm
命令行中输入:jvisualvm
Visual GC插件下载地址 :https://visualvm.github.io/pluginscenters.html
监控本地Java进程
监控远程Java进程
-
在visualvm中选中“远程”,右击“添加”
-
主机名上写服务器的ip地址,比如39.100.39.63,然后点击“确定”
-
右击该主机"39.100.39.63",添加“JMX”,也就是通过JMX技术具体监控远端服务器哪个Java进程
-
要想让服务器上的tomcat被连接,需要改一下Catalina.sh这个文件
注意下面的8998不要和服务器上其他端口冲突
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote - Djava.rmi.server.hostname=39.100.39.63 -Dcom.sun.management.jmxremote.port=8998 -Dcom.sun.management.jmxremote.ssl=false - Dcom.sun.management.jmxremote.authenticate=true - Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access - Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password"
-
在…/conf文件中添加两个文件jmxremote.access和jmxremote.password
jmxremote.access
guest readonly
manager readwritejmxremote.password
guest guest
manager manager授予权限:chmod 600 jmxremot
-
将连接服务器地址改为公网ip地址
hostname -i 查看输出情况
172.26.225.240 172.17.0.1
vim /etc/hosts
172.26.255.240 39.100.39.63 -
设置上述端口对应的阿里云安全策略和防火墙策略
-
启动tomcat,来到bin目录
./startup.sh
-
查看tomcat启动日志以及端口监听
tail -f …/logs/catalina.out
lsof -i tcp:8080 -
查看8998监听情况,可以发现多开了几个端口
lsof -i:8998 得到PID
netstat -antup | grep PID
-
在刚才的JMX中输入8998端口,并且输入用户名和密码则登录成功
端口:8998
用户名:manager
密码:manager
arthas
github :https://github.com/alibaba/arthas
Arthas 是Alibaba开源的Java诊断工具,采用命令行交互模式,是排查jvm相关问题的利器
下载安装
curl -O https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar
or
java -jar arthas-boot.jar -h
常用命令
version:查看arthas版本号
help:查看命名帮助信息
cls:清空屏幕
session:查看当前会话信息
quit:退出arthas客户端
dashboard:当前进程的实时数据面板
thread:当前JVM的线程堆栈信息
jvm:查看当前JVM的信息
sysprop:查看JVM的系统属性
sc:查看JVM已经加载的类信息
dump:dump已经加载类的byte code到特定目录 jad:反编译指定已加载类的源码
monitor:方法执行监控
watch:方法执行数据观测
trace:方法内部调用路径,并输出方法路径上的每个节点上耗时
stack:输出当前方法被调用的调用路径
…
内存分析
MAT
Java堆分析器,用于查找内存泄漏
Heap Dump,称为堆转储文件,是Java进程在某个时间内的快照。 它在触发快照的时候保存了很多信息:Java对象和类信息。 通常在写Heap Dump文件前会触发一次Full GC。
下载地址 :https://www.eclipse.org/mat/downloads.php
获取dump文件
-
手动
jmap -dump:format=b,file=heap.hprof 44808
-
自动
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof
Dump的信息
-
All Objects
Class, fields, primitive values and references
-
All Classes
Classloader, name, super class, static fields
-
Garbage Collection Roots
Objects defined to be reachable by the JVM
-
Thread Stacks and Local Variables
The call-stacks of threads at the moment of the snapshot, and per-frame information about
local
objects
使用
- Histogram:可以列出内存中的对象,对象的个数及其大小
Class Name:类名称,java类名
Objects:类的对象的数量,这个对象被创建了多少个
Shallow Heap:一个对象内存的消耗大小,不包含对其他对象的引用
Retained Heap:是shallow Heap的总和,即该对象被GC之后所能回收到内存的总和
右击类名—>List Objects—>with incoming references—>列出该类的实例
右击Java对象名—>Merge Shortest Paths to GC Roots—>exclude all …—>找到GC Root以及原因
JVM中GC Roots的大致分类
-
Class 由System Class Loader/Boot Class Loader加载的类对象,这些对象不会被回收。需 要注意的是其它的Class Loader实例加载的类对象不一定是GC root,除非这个类对象恰好 是其它形式的GC root;
-
Thread 线程,激活状态的线程;
-
Stack Local 栈中的对象。每个线程都会分配一个栈,栈中的局部变量或者参数都是GC root,因为它们的引用随时可能被用到;
-
JNI Local JNI中的局部变量和参数引用的对象;可能在JNI中定义的,也可能在虚拟机中定义
-
JNI Global JNI中的全局变量引用的对象;同上
-
Monitor Used 用于保证同步的对象,例如wait(),notify()中使用的对象、锁等。
-
Held by JVM JVM持有的对象。JVM为了特殊用途保留的对象,它与JVM的具体实现有关。
比如有System Class Loader, 一些Exceptions对象,和一些其它的Class Loader。对于这些 类,JVM也没有过多的信息。
Leak Suspects:查找并分析内存泄漏的可能原因
Reports—>Leak Suspects—>Details
Top Consumers:列出大对象
heaphero
https://heaphero.io/
perfma
https://console.perfma.com/
GC日志分析
要想分析日志的信息,得先拿到GC日志文件才行,所以得先配置一下。比如打开windows中的catalina.bat,在第一行加上
XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps
-Xloggc:$CATALINA_HOME/logs/gc.log
不同收集器日志
Parallel GC
2019-06-10T23:21:53.305+0800: 1.303: [GC (Allocation Failure) [PSYoungGen: 65536K[Young区回收前]->10748K[Young区回收后](76288K[Young区总大小])] 65536K[整个 堆回收前]->15039K[整个堆回收后](251392K[整个堆总大小]), 0.0113277 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
注意
如果回收的差值中间有出入,说明这部分空间是Old区释放出来的
CMS
参数设置:-XX:+UseConcMarkSweepGC -Xloggc:cms-gc.log
日志格式和上面差不多
G1
G1日志格式参考链接:https://blogs.oracle.com/poonam/understanding-g1-gc-logs
参数设置:-XX:+UseG1GC -Xloggc:g1-gc.log
-XX:+UseG1GC # 使用了G1垃圾收集器
# 什么时候发生的GC,相对的时间刻,GC发生的区域young,总共花费的时间,0.00478s, # It is a stop-the-world activity and all
# the application threads are stopped at a safepoint during this time. 2019-12-18T16:06:46.508+0800: 0.458: [GC pause (G1 Evacuation Pause) (young), 0.0047804 secs]
# 多少个垃圾回收线程,并行的时间
[Parallel Time: 3.0 ms, GC Workers: 4]
# GC线程开始相对于上面的0.458的时间刻
[GC Worker Start (ms): Min: 458.5, Avg: 458.5, Max: 458.5, Diff: 0.0]
# This gives us the time spent by each worker thread scanning the roots
# (globals, registers, thread stacks and VM data structures).
[Ext Root Scanning (ms): Min: 0.2, Avg: 0.4, Max: 0.7, Diff: 0.5, Sum: 1.7]
# Update RS gives us the time each thread spent in updating the Remembered
Sets.
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
...
# 主要是Eden区变大了,进行了调整
[Eden: 14.0M(14.0M)->0.0B(16.0M) Survivors: 0.0B->2048.0K Heap: 14.0M(256.0M)->3752.5K(256.0M)]
GCViewer
java -jar gcviewer-1.36-SNAPSHOT.jar
gceasy
http://gceasy.io
gcplot
https://it.gcplot.com/
性能优化
JVM的性能优化可以分为代码层面和非代码层面。
在代码层面,大家可以结合字节码指令进行优化,比如一个循环语句,可以将循环不相关的代码提取到循环体之外,这样在字节码层面就不需要重复执行这些代码了。
在非代码层面,一般情况可以从内存、gc以及cpu占用率等方面进行优化。
注意,JVM调优是一个漫长和复杂的过程,而在很多情况下,JVM是不需要优化的,因为JVM本身 已经做了很多的内部优化操作。
但是要注意的 是不要为了调优和调优。
内存
内存分配
正常情况下不需要设置
内存溢出(OOM)
一般会有两个原因:
(1)大并发情况下
(2)内存泄露导致内存溢出
大并发[秒杀]
浏览器缓存、本地缓存、验证码
CDN静态资源服务器
集群+负载均衡
动静态资源分离、限流[基于令牌桶、漏桶算法]
应用级别缓存、接口防刷限流、队列、Tomcat性能优化
异步消息中间件
Redis热点数据对象缓存
分布式锁、数据库锁
5分钟之内没有支付,取消订单、恢复库存等
GC
G1调优
常见问题
内存泄漏与内存溢出的区别
内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。
内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。
young gc会有stw吗?
不管什么 GC,都会发送 stop-the-world,区别是发生的时间长短。而这个时间跟垃圾收集器又有关系,Serial、PartNew、Parallel Scavenge 收集器无论是串行还是并行,都会挂起用户线程,而 CMS 和 G1 在并发标记时,是不会挂起用户线程的,但其它时候一样会挂起用户线程,stop the world 的时 间相对来说就小很多了。
major gc和full gc的区别
Major GC在很多参考资料中是等价于 Full GC 的,我们也可以发现很多性能监测工具中只有 Minor GC 和 Full GC。一般情况下,一次 Full GC 将会对年轻代、老年代、元空间以及堆外内存进行垃圾回收。触 发 Full GC 的原因有很多:当年轻代晋升到老年代的对象大小,并比目前老年代剩余的空间大小还要大时,会触发 Full GC;当老年代的空间使用率超过某阈值时,会触发 Full GC;当元空间不足时(JDK1.7 永久代不足),也会触发 Full GC;当调用 System.gc() 也会安排一次 Full GC。
什么是直接内存
Java的NIO库允许Java程序使用直接内存。直接内存是在java堆外的、直接向系统申请的内存空间。通 常访问直接内存的速度会优于Java堆。因此出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。由于直接内存在java堆外,因此它的大小不会直接受限于Xmx指定的最大堆大小,但是系统内存是 有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。
垃圾判断的方式
引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为0就会回收但是JVM没 有用这种方式,因为无法判定相互循环引用(A引用B,B引用A)的情况。
引用链法: 通过一种GC ROOT的对象(方法区中静态变量引用的对象等-static变量)来判断,如果有 一条链能够到达GC ROOT就说明,不能到达GC ROOT就说明可以回收。
不可达的对象一定要被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真 正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行 一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。 被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个 对象建立关联,否则就会被真的回收
为什么要区分新生代和老年代?
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不 同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合 适的垃圾收集算法。 比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制 成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分 配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
G1与CMS的区别是什么
CMS 主要集中在老年代的回收,而 G1 集中在分代回收,包括了年轻代的 Young GC 以及老年代的 Mix GC;G1 使用了 Region 方式对堆内存进行了划分,且基于标记整理算法实现,整体减少了垃圾碎片的 产生;在初始化标记阶段,搜索可达对象使用到的 Card Table,其实现方式不一样。
方法区中的无用类回收
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢? 判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。 类需要同时满足下面 3 个条件才能算是 “无用的类” :
a-该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
b-加载该类的 ClassLoader 已经被回收。
c-该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。