JVM知识点
2021-04-19 19:37:23 0 举报
AI智能生成
jvm导图
作者其他创作
大纲/内容
内存相关
内存分配
对象优先在Eden区分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间分配时,虚拟机将发起一次yong GC
大对象直接进入老年区
最典型的大对象是很长的字符串和数组
避免在Eden区和Survivore区之间的大量内存复制
就是“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数,比如“1048576”字节,就是1MB。大于1M的对象直接进入老年代
长期存活对象进入老年区
如果一个实例对象在新生代中,成功的在15次垃圾回收之后,还是没被回收掉,
也就是对象的年龄,每垃圾回收一次,如果一个对象没被回收掉,他的年龄就会增加1。当它的年龄增加到一定程度(默认为15)时,就会晋升到老年代
也就是对象的年龄,每垃圾回收一次,如果一个对象没被回收掉,他的年龄就会增加1。当它的年龄增加到一定程度(默认为15)时,就会晋升到老年代
对象动态年龄判定
假如说当前放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的50%,那么此时大 于等于这批对象年龄的对象,就可以直接进入老年代了。
实际这个规则运行的时候是如下的逻辑:年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区 域的50%,此时就会把年龄n以上的对象都放入老年代。
空间分配担保机制
执行任何一次Minor GC之前,JVM会先检查一下老年代可用的可用内存空间,是否大于新生代所有对象的总大小。如果说发现老年代的内存大小是大于新生代所有对象的,此时就可以放心大胆的对新生代发起一次Minor GC了,因为即使Minor GC之 后所有对象都存活,Survivor区放不下了,也可以转移到老年代去。
假如Minor GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小了,就会看一个“-XX:HandlePromotionFailure”的参数是否设置了。
如果有这个参数,那么就会继续尝试进行下一步判断。
下一步判断,就是看看老年代的内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小。如果大于则尝试进行yong GC.如果担保失败则进行一次full GC.
如果有这个参数,那么就会继续尝试进行下一步判断。
下一步判断,就是看看老年代的内存大小,是否大于之前每一次Minor GC后进入老年代的对象的平均大小。如果大于则尝试进行yong GC.如果担保失败则进行一次full GC.
如果小于,或者“-XX:HandlePromotionFailure”没有设置,那么要进行一次full GC
内存回收
yong GC
特点
发生在新生代上,发生的比较频繁,执行速度较快
触发条件
Eden区不足
空间分配担保
Full GC
特点
发生在老年代上,较少发生,执行速度较慢
触发条件
调用System.gc()
老年代区域空间不足
空间分配担保失败
JDK1.7及以前的永久(代方法区)空间不足
CMS GC处理浮动垃圾时,如果新生代空间不足,则采用空间分配担保机制,如果老年代空间不足,则触发Full GC
内存泄漏
程序申请内存后,无法使用已申请的内存空间
原因
长生命周期的对象持有短生命周期对象的引用
如将Arraylist设置为静态变量,则容器中的对象在程序结束之前将不能释放,从而造成内存泄漏
连接未关闭
如数据库连接、网络连接和IO连接等,只有链接被关闭后,垃圾回收器才会回收对应的对象
变量作用域不合理
一个变量的定义的作用范围大于其使用范围
内部类持有外部类
java的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的
生命周期,程序就很容易产生内存泄漏
生命周期,程序就很容易产生内存泄漏
解决方法
将内部类定义为static
用static的变量引用匿名内部类的实例
或将匿名内部类的实例化操作放到外部类的静态方法中
Hash值改变
在集合中,如果修改了 对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄漏
内存溢出
程序在申请内存时,没有足够的内存空间
内存溢出的构造方式
堆溢出
OOM:不断创建对象
栈溢出
StackOverFlowError
增大本地变量表,例如不合理的递归
OOM:不断建立线程
方法区和运行时常量池溢出
本机内存直接溢出
其他知识
三色标记法
三色标记法
对象
对象的创建
根据new的参数是否能在常量池中定位到一个类的符号引用
如果没有,说明还未定义该类,抛出ClassNotFoundException
检查符号引用对应的类是否加载过
如果没有则进行类加载
根据方法区的信息确定为该类分配的内存空间大小
指针碰撞
java堆内存空间规整的情况下使用
java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定
空闲列表
java堆空间不规整的情况下使用
从堆中划分一块对应大小的内存空间给 该对象
对象中的成员变量赋上初始值
设置对象头信息
调用对象构造函数进行初始化
对象的内存布局
对象头
Mark Word
对象的hashCode
GC年代
锁信息(偏向锁、轻量级锁、重量级锁)
CG标志
Class Metadata Address
指向对象实例的指针
对象的实例数据
对齐填充
对象的访问方式
指针
reference中存储的直接就是对象地址
句柄
java堆中会划分一块内存来作为局秉持,reference中存储的是的对象的句柄地址,而句柄中包含了对象数据与类型数据各自的具体地址信息
两种方式的比较
使用句柄来访问的好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变举并重的实例数据指针,而reference本身不需要修改
使用直接指针访问方式的好处是速度更苦啊,节省了一次指针定位的时间开销、HotSpot虚拟机使用的是直接指针访问的方式
优化案例
高并发的系统(社交系统APP)
描述
QPS高
年轻代的Eden区会迅速的被填满,并且频繁的触发Young GC
每次yongGC的时候,很多请求没有处理完,导致很多对象存活
导致大量的对象进入老年代,频繁的触发老年代GC
过多的full GC,导致内存碎片
优化
1 加机器,减少每台机器的并发量
2 增加年轻代的Survivor更大的内存空间,让每次Young GC后的存活对象务必停留在Survior中,别进入老年代
3 设置-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=5
解决内存碎片的问题,空出来大片的连续可用内存空间
Metadata区 fullGC案列
描述
Metadata区域频繁的被塞满,进而触发Full GC
Metadata区存放class类信息
解决
JVM启动参数中加入如下两个参数了:
“-XX:TraceClassLoading -XX:TraceClassUnloading”
参数含义:就是追踪类加载和类卸载的情况
“-XX:TraceClassLoading -XX:TraceClassUnloading”
参数含义:就是追踪类加载和类卸载的情况
2 可能是 SoftRefLRUPolicyMSPerMB 参数导致
设置大些XX:SoftRefLRUPolicyMSPerMB=3000 or 5000
目的是让反射过程中JVM自动创建的软引用的一些类的Class对象不要被随便回收
System.gc()引起的系统卡死
特点
1 高并发的时候,系统卡死
2 JVM几乎每秒都执行一次Full GC,
每次都耗时几百毫秒
每次都耗时几百毫秒
3 通过jstat看了一下JVM各个内存区域的使用量,年轻代、老年代、永久代内存占用都正常
4 怀疑System.gc()显示调用
每次执行都会指挥JVM去尝试执行一次Full GC,连带年轻代、老年代、永久代都会去回收
解决
禁止显式使用System,gc()
JVM优化参数模板
8G
full GC引起的CPU过高
CPU过高的2个常见场景
1 系统里创建了大量的线程,这些线程同时并发运行,而且工作负载都很重,过多的线程同时并发运行就会导致
你的机器CPU负载过高
你的机器CPU负载过高
2 JVM在执行频繁的Full GC,Full GC是非常耗费CPU资源的
频繁Full GC三个场景
内存分配不合理,导致对象频繁进入老年代,进而引发频繁的Full GC
存在内存泄漏等问题,就是内存里驻留了大量的对象塞满了老年代,导致稍微有一些对象进入老年代就会引发Full GC
永久代里的类太多,触发了Full GC
错误的执行“System.gc()”导致的
描述
1 CPU过高
2 jstat发现full gc频繁
3 对象进入老年代也正常,每次fullgc后,老年代剩余对象还是很多
4 怀疑 老年代里驻留了大量的对象
5 导出dum日志,使用MAT分析,得出结论,本地缓存对象过多
jmap -dump:format=b,file=文件名 [服务进程ID]
类加载机制
概念
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后就在堆区创建一个java.lang.Class对象,用来封装类在方法区的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区的数据结构,并且向java程序员提供了访问方法区内的数据结构接口。
也就类的元数据(类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)
也就类的元数据(类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)
类生命周期以及加载过程
加载
代码中用到这个类的时候,类加载器将编译后的.class静态文件转换到内存中(方法区),然后暴露出来让程序员访问
验证
校验加载进来的“.class”文件中的内容,是否符合JVM规范,并且不会危害虚拟机自身安全
准备
给类分配内存空间
给类变量分配内存空间并设置初始值,使用的是方法区的内存
解析
把Class文件的常量池的符号引用替换为直接引用的过程(静态链接)
可能发生在初始化阶段之前,也可能发生在初始化阶段之后,后者是为了支持java的动态绑定
初始化
为类的静态变量赋予程序中指定的初始值,还有执行静态代码中的程序
初始化一个类的时候如果父类没有初始化,会先初始化其父类
类中有初始化语句,则系统依次执行这些初始化语句
使用
卸载
类加载方式
静态加载
new的方式
无法找到该类的时候,NoClassDefFoundError(错误)
进行class的初始化,执行static代码块。
调用一个类的静态方法
直接操作一个类的static属性
动态加载
通过Class.forName()方法动态加载
无法找到该类的时候,ClassNotFoundException(异常)
使用的是当前类加载器
进行class的初始化,执行static代码块。
通过ClassLoader.loadClass()方法动态加载
无法找到该类的时候,ClassNotFoundException(异常)
用户指定类加载器
该类加载到jvm中并对它进行了格式校验,并解析该文件,分配内存并赋值默认值,并且将符号引用替换成直接引用(内存地址);
不会初始化
不会初始化
类加载时机
遇到new、getStatic、putStatic、invokeStatic这四条指令
new一个对象时
调用一个类的静态方法
直接操作一个类的statics属性
使用java.lang.reflect进行反射调用
初始化类的时候,没有初始化父类,就先初始化父类
虚拟机启动时,用户指定的主类(main)
类加载器
类加载器
启动类加载器
Bootstrap ClassLoader
Bootstrap ClassLoader
C++实现,java虚拟机自带
负责加载<JRE_HOME>\lib目录中的类库加载到虚拟机内存中
扩展类加载器
Extension ClassLoader
Extension ClassLoader
1 、负责加载<JRE_HOME>\lib\ext目录中的类库加载到虚拟机内存中
2、被java.ext.dir系统变量所指定路径中的所有类库加载到内存中
应用程序类加载器
Application ClassLoader
Application ClassLoader
负责去加载“ClassPath”环境变量所指定的路径中的类
自定义类加载器
自定义类加载器,去根据你自己的需求加载你的类。
双亲委派模式
定义
如果一个类加载器收到类的加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,只有当父类加载器在自己的搜索范围内找不到指定的类时,子类加载器才会尝试自己去加载。
流程
1、首先检查类是否被加载
2、若未加载,则调用父类加载器的loadClass方法
3、若该方法抛出ClassNotFoundException异常,则表示父类加载器无法加载,则当前类加载器调用findClass加载类
4、若父类加载器可以加载,则直接返回Class对象
好处
保证java类库中的类不受用户类影响,防止用户自定义一个类库中同名的类,破坏java的类库,引起问题
破坏
基础类需要调用用户的代码
解决方式
线程上下文类加载器
也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性
实现方法
重写ClassLoader类的loadClass()方法
示例
JDBC
原生的JDBC中的类放在rt.jar包的,是由启动类加载器进行类加载的,JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类
以JDBC为例谈双亲委派模型的破坏
JNDI服务需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码
(父类加载器无法调用其他classLoader去加载第三方jar)
重写loadClass()方法
双亲委派模型的具体实现就在loadClass()方法中
用户对程序的动态性的追求
典型的打破双亲委派模型的框架和中间件有tomcat和osgi
tomcat类加载
Tomcat自定义了Common、Catalina、Shared等类加载器,其实就是用来加载Tomcat自己的一些核心基础类库的。
然后Tomcat为每个部署在里面的Web应用都有一个对应的WebApp类加载器,负责加载我们部署的这个Web应用的类
至于Jsp类加载器,则是给每个JSP都准备了一个Jsp类加载器。
而且大家一定要记得,Tomcat是打破了双亲委派机制的
每个WebApp负责加载自己对应的那个Web应用的class文件,也就是我们写好的某个系统打包好的war包中的所有class文 件,不会传导给上层类加载器去加载。
然后Tomcat为每个部署在里面的Web应用都有一个对应的WebApp类加载器,负责加载我们部署的这个Web应用的类
至于Jsp类加载器,则是给每个JSP都准备了一个Jsp类加载器。
而且大家一定要记得,Tomcat是打破了双亲委派机制的
每个WebApp负责加载自己对应的那个Web应用的class文件,也就是我们写好的某个系统打包好的war包中的所有class文 件,不会传导给上层类加载器去加载。
tomcat破坏双亲模式的原因
tomcat中的需要支持不同web应用依赖同一个第三方类库的不同版本,jar类库需要保证相互隔离;
同一个第三方类库的相同版本在不同web应用可以共享
tomcat自身依赖的类库需要与应用依赖的类库隔离
jsp需要支持修改后不用重启tomcat即可生效 为了上面类加载隔离
和类更新不用重启,定制开发各种的类加载器
和类更新不用重启,定制开发各种的类加载器
模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合实现,而不是继承
JVM内存结构
线程共享区域
堆
新生代
短期存活的,分配在Java堆内存之后,迅速使用完就会被垃圾回收
Eden区
Survivor(from)区
减少送到老年代的对象
Surcicor(to)区
2个S区,解决内存碎片化问题
Eden:survivor1:survivor2
8:1:1
8:1:1
老年代
是长期存活的,需要一直生存在Java堆内存里,让程序后续不停的去使用
老年代:新生代
2:1
2:1
方法区
运行时常量池
Class文件中的常量池(编译器生成的各种字面量和符号引用),会在类加载后放入这个区域
存储信息
符号引用
符号引用包含的常量
类符号引用
方法符号引用
字段符号引用
概念解释
一个java类(假设为Person类)被编译成一个Class文件时,如果Person类引用了Tool类,但是在编译的时候Person类并不知道Tool引用类的实际内存地址,
所以,只能使用符号引用来替代
所以,只能使用符号引用来替代
而在类装载器装载Person类时,此时可以通过虚拟机获取Tool类的实际内存地址,因为便可以将符号引用org.simple.Tool替换为Tool类的实际内存地址
及直接引用地址
及直接引用地址
即在编译时用符号引用来代替引用类,在加载时在通过虚拟机获取该引用类的实际地址
以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能定位到目标即可。符号引用于虚拟机实现的内存布局是无关的,引用的目标不一定已经加载到内存中
字面量
文本字符串
String a ="abc",这个abc就是字面量
八种基本类型
int a =1,1就是字面量
声明为final的常量
静态变量
final类型常量
类信息
类的完整路径名
返回值类型
修饰符信息(public、private)
变量名
方法名
方法代码
这个类直接父类的完整路径名(除非是interface或者object,两种情况没有父类)
类的直接接口的一个有序列表
方法区的GC回收情况
1 该类的所有实例对象都已经从Java堆内存里被回收
2 加载这个类的ClassLoader已经被回收
3 对该类的Class对象没有任何引用
2 加载这个类的ClassLoader已经被回收
3 对该类的Class对象没有任何引用
线程私有区域
虚拟机栈
栈帧
动态链接
符号引用和直接引用在运行时进行解析和链接的过程,叫动态链接
前提是每一个栈帧内部都要包含一个指向运行时常量池的引用,来支持动态链接的实现
操作数栈
保存着java虚拟机执行过程中的数据
局部变量表
局部变量表所需的内存哦空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的变量空间是完全确定的,
在方法运行期间不会改变局部变量表的大小
在方法运行期间不会改变局部变量表的大小
存放的信息
基本数据类型
对象引用
returnAddress类型
方法返回地址
方法被调用的位置
方法退出的过程实际上就等同于把当前栈出栈
异常
线程请求的栈深度大于虚拟机所允许的深度
StackOverflowError
JVM动态扩展时无法申请到足够的内存时
OOM
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定啦,并且写入到哦方法表哦的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现
本地方法栈
和虚拟机栈类似,区别是本地方法栈为使用到Native方法服务
程序计算器
如果线程正在执行的是一个java方法,则记录当前执行的字节码指令的位置
唯一一个不会出现OOM
如果是执行的是本地Native方法,这个计数器值则为空
上面3个区域的生命周期和线程相同
直接内存
使用Native函数库直接分配对外内存
并不是JVM运行时数据区域的一部分,但是会被频繁使用
避免了在java堆和Native堆中来回复制数据,能够提高效率
垃圾收集
判断对象是否存活
引用计数法
给对象添加一个引用计数器,当对象增加一个引用的时候计数器加1,引用失效时计算器减一,引用计数为0的对象可以被回收
缺陷:循环引用会导致内存泄漏
示例
可达性分析算法
该算法是通过一系列的成为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象
到GC Roots没有任何引用链相连(用图论的话就是从GC Roots到这个对象不可达)时,则证明此对象不可用。
到GC Roots没有任何引用链相连(用图论的话就是从GC Roots到这个对象不可达)时,则证明此对象不可用。
GC Root对象
当前虚拟机栈中局部变量表中的引用的对象(栈帧)
当前本地方法栈中局部变量表中的引用对象
方法区中类静态属性引用的对象 static对象
方法区中的常量引用的对象 final对象
经过根搜索算法的可达性分析某个对象没有和GC Roots的直接或间接联系(不可达),
并不意味着该对象将被回收,还需要经过两次标记和筛选工作,才能决定是否可以回收对象。
并不意味着该对象将被回收,还需要经过两次标记和筛选工作,才能决定是否可以回收对象。
1 判断是否有finalize方法或者执行过finalize,如果没有finalize方法或者已经执行过finalize方法,
不需要进行筛选则可以回收,否则需要进行筛选进行第二次标记和筛选。
不需要进行筛选则可以回收,否则需要进行筛选进行第二次标记和筛选。
2 执行对象的finalize方法,执行完成或者执行过程中判断对象是否和GC Roots是否有直接或者间接联系,
如果依然没有联系则把对象放入回收列表等待回收,否则对象复活。
如果依然没有联系则把对象放入回收列表等待回收,否则对象复活。
finilized函数执行的大致过程
对象引用类类型
强引用
垃圾回收器绝对不会回收它,当内存不足时宁愿抛出OOM错误,使得程序异常停止
软引用
垃圾回收器在内存充足的时候不会回收它,而在内存不足时会回收它
软引用非常适合于创建缓存。当系统内存不足的时候,缓存中的内存是可以被释放的
弱引用
垃圾回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存
ThreadLocal的key是弱引用
虚引用
如果一个对象只具有虚引用,那么它和没有任何引用一样,任何时候都可能被回收。
虚引用主要用来跟踪对象呗垃圾回收器回收的活动
垃圾收集算法
标记-清除
过程
1 将需要回收的对象标记起来
2 清除对象
缺陷
1 标记和清除的效率不高
会产生大量的不连续的内存碎片,造成内存浪费
虽然所有的内存碎片加起来其实有很大的一块内存,但是因为这些
内存都是碎片式分散的,所以导致没有一块完整的足够的内存空间来分配新的对象
内存都是碎片式分散的,所以导致没有一块完整的足够的内存空间来分配新的对象
复制算法
复制算法是将内存划分为两块大小相等的区域,每次使用时都只用其中一块区域,当发生垃圾回收时会将存活的对象全部复制到未使用的区域,
然后对之前的区域进行全部回收
然后对之前的区域进行全部回收
新生代使用的是复制算法
优点
简单高效,不会出现内存碎片问题
缺陷
内存利用率低,只有一半内存可以被使用
存活对象较多的时效率明显会降低
优化:
新生代内存区域划分为三块
1个Eden区,2个Survivor区,其中Eden区占80%内存空间,每一块Survivor区各占10%内存空间,比如说Eden区有800MB内存,每
一块Survivor区就100MB内存
一块Survivor区就100MB内存
标记-整理
原理和标记清除算法类似,只是最后一步的清除改为了将存活对象全部移动到一端,然后在将边界之外的内存回收
老年代使用的是标记-整理算法
缺陷
需要移动大量对象,效率不高
分代回收算法
根据各个年代的特点选取不同的垃圾收集算法
新生代使用复制算法
老年代使用标记-整理或者标记-清除算法
垃圾收集器
Serial收集器
串行单线程收集器
优点:简单高效
是Client模式下的默认新生代收集器
ParNew收集器
Seral收集器的多线程版本
是Server模式下的虚拟机首选新生代收集器,除了性能原因外,主要是原因是除了Seral收集器,只有它能与CMS收集器配合工作
默认给自己设置的垃圾回收线程的数量就是跟CPU的核数是一样的
Parallel Scaaenge收集器
Serial Old收集器
ParNew Old收集器
CMS收集器
CMS垃圾回收器采取的是垃圾回收线程和系统工作线程尽量同时执行的模式来处理的
流程
初始标记(STW)
仅仅只是标记一下GC Roots直接引用的对象,速度很快,
这个阶段会让系统的工作线程全部停止,进入“Stop the World”状态
这个阶段会让系统的工作线程全部停止,进入“Stop the World”状态
并发标记(无STW)并发
系统线程可以随意创建各种新对象,继续运行,在运行期间可能会创建新的存活对象,也可能会让部分存活对象失去引用,变成垃圾对象。
在这个过程中,垃圾回收线程,会尽可能的 对已有的对象进行GC Roots追踪(去看老年代的对象都被谁引用啦)。
在这个过程中,垃圾回收线程,会尽可能的 对已有的对象进行GC Roots追踪(去看老年代的对象都被谁引用啦)。
从GC ROOT 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿
是跟系统程序并发运行的,所以其实这个阶段不会对 系统运行造成影响的。
对老年代所有对象进行GC Roots追踪,其实是最耗时的
从GCROOT直接关联对象开始遍历整个对象图
重新标记(STW)
对在第二阶段中被系统程序运行变动过的少数对象进行标记,所以运行速度很快。需要停顿
并发清除(无STW)并发
清理掉之前标记为垃圾的对象即可。
这个阶段其实是很耗时的,因为需要进行对象的清理,和系统并发执行,不影响系统性能。不需要停顿
这个阶段其实是很耗时的,因为需要进行对象的清理,和系统并发执行,不影响系统性能。不需要停顿
缺陷
对处理器资源敏感
低停顿时间是以牺牲吞吐量为代价的,导致CPU利用率不高
面向并发设计的程序对处理器资源都敏感,因为并发阶段会占用一部分线程资源,导致应用程序变慢,降低总吞吐量
默认启动的回收线程数是(CPU核+3)/4
4核以上,并发收集线程只占用不超过25%的处理器资源,随着核数增加下降
4核以下,如果程序本来的处理负载就搞,还有分出一部分资源,会导致用户程序执行速度下降
无法处理浮动垃圾,可能出现Concurent Mode Failuer
浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到了下一次GC时才能被回收
由于浮动垃圾的存在,因此需要预留出一部分内存,意味着CMS收集不能像其它收集器那样等待老年代快满的时候再回收
JDK6的时候,CMS收集器的启动阈值默认为92%
如果预留的内存无法满足程序分配新对象,就会出现一次并发失败,Concurent Mode Failuer
这个时候将启动后备预案,冻结用户线程执行。临时使用Serial Old收集器重新进行老年代回收
会导致更多的停顿时间
会产生空间碎片
标记-清除会导致不连续的空间碎片
-XX:+UseCMSCompactAtFullCollection,开启整理空间碎片,是需要移动存活对象的,所以不能并发执行,会导致停顿时间变长
-XX:CMSFullGCsBeforeCompaction 使用这个参数配合,fullGC几次后再执行
以上2个参数JDK9废弃
为什么标记2次
采用增量更新来做并发标记
G1收集器
流程
初始标记
仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,
能再正确的Region中创建对象,此阶段需要停顿线程(STW),但耗时很短。
能再正确的Region中创建对象,此阶段需要停顿线程(STW),但耗时很短。
并发标记
从GC Root开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,因为要追踪全部的存活对象,但可与用户程序并发执行
JVM会对并发标记阶段对对象做出的一些修改记录起来,比如说哪个对象被新建了,哪个对象失去了引用。
最终标记
进入“Stop the World”,系统程序是禁止运行的,但是会根据并发标记 阶段记录的
那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象
那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象
筛选回收
首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。此阶段其实也可以做到与用户程序
一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率
一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率
垃圾回收不仅仅是回收老年代,还会回收新生代,还会回收大对象
最后一个阶段混合回收的时候,其实会停止所有程序运行,所以说G1是允许执行多次混合回收。
比如先停止工作,执行一次混合回收回收掉 一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收回收掉一
些Region
比如先停止工作,执行一次混合回收回收掉 一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收回收掉一
些Region
为什么要回收多次?
因为你停止系统一会儿,回收掉一些Region,再让系统运行一会儿,然后再次停止系统一会儿,再次回收掉一些Region,这样可以尽
可能让系统不要停顿时间过长,可以在多次回收的间隙,也运行一下。
可能让系统不要停顿时间过长,可以在多次回收的间隙,也运行一下。
特点
空间整合,不会产生内存碎片
可预测的停顿
核心设计思路
1、G1可以做到让你来设定垃圾回收对系统的影响,他自己通过把内存拆分为大量小Region,以及追踪每个Region中可以
回收的对象大小和预估时间,最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时
间内尽量回收尽可能多的垃圾对象、选择最少回收时间和最多回收对象的Region进行垃圾回收
回收的对象大小和预估时间,最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时
间内尽量回收尽可能多的垃圾对象、选择最少回收时间和最多回收对象的Region进行垃圾回收
2、Region可能属于新生代也可能属于老年代
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
新生代和老年代各自的内存区域是不停的变动的,由G1自动控制。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
新生代和老年代各自的内存区域是不停的变动的,由G1自动控制。
3 JVM最多可以有2048个Region,然后Region的大小必须是2的倍数,所以每个region的大小是堆/2048
4 新生代达到了设定的占据堆内存的最大大小60%,且Eden区满的时候触发young GC,用复制算法来进行垃圾回收
对象何时进入老年代
(1)对象在新生代躲过了很多次的垃圾回收,达到了一定的年龄了,“-XX:MaxTenuringThreshold”参数可以设置这个年龄,他就
会进入老年代
会进入老年代
2 动态年龄判定规则,如果一旦发现某次新生代GC过后,存活对象超过了Survivor的50%
G1中大对象的分配
G1提供了专门的Region来存放大对象,而不是让大对象进入老年代的Region中。
在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,
而且一个大对象如果太大,可能会横跨多个Region来存放
而且一个大对象如果太大,可能会横跨多个Region来存放
新生代、老年代在回收的时候,会顺带带着大对象Region一起回收
G1 回收失败full gc
采用原始快照做并发标记
ZGC收集器
JVM调优
常见参数
通用参数
Xms
最小堆
Xmx
Java整个堆内存的最大大小
Xss
每个线程的栈内存大小
-Xmn
java堆内存中的新生代大小扣除新生代剩下的就是老年代的内存大小了
-XX:PermSize
永久代大小
-XX:MaxPermSize
永久代最大大小
-XX:SurvivorRatio
新生代中Eden区域和Survivor区域(From幸存区或To幸存区)的比例,默认为8,也就是说Eden占新生代的8/10,From幸存区和To幸存区各占新生代的1/10
-XX:NewRatio
新生代和老年代的比值,默认是8:2。一般用Xmn控制
-XX:+DisableExplicitGC
禁止显式的执行GC 如System.gc()
ParNew垃圾回收器参数
-XX:+UseParNewGC
指定新生代使用ParNew垃圾回收器
-XX:ParallelGCThreads
调节ParNew的垃圾回收线程数量
-XX:PretenureSizeThreshold=1M
大于这个大小的对象直接进入老年代
只对串行回收器和ParNew有效,对ParallGC无效
只对串行回收器和ParNew有效,对ParallGC无效
-XX:MaxTenuringThreshold
新生代需要经历多少次GC晋升到老年代中的最大阈值
只对串行回收器和ParNew有效,对ParallGC无效
CMS垃圾收集器参数
XX:+UseConcMarkSweepGC
使用CMS垃圾收集器,,同时-XX:NewRatio=4的会失效,此时年轻代大小最好用-Xmn设置
-XX:+UseCMSInitiatingOccupancyOnly
只是用设定的回收阈值(上面指定的70%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整.与下面参数一起使用
-XX:CMSInitiatingOccupancyFaction
设置老年代占用多少比例的时候触发CMS垃圾回收,JDK 1.6里面默认的值是92%。
-XX:CMSFullGCsBeforeCompaction
fullGC几次之后清理空间内存碎片
-XX:+UseCMSCompactAtFullCollection
默认就打开了,意思是在Full GC之后要再次进行“Stop the World”,停止工作线程,
然后进行碎片整理,就是把存活对象挪到一起,空出来大片连续内存空间,避免内存碎片
然后进行碎片整理,就是把存活对象挪到一起,空出来大片连续内存空间,避免内存碎片
-XX:+CMSParallelInitialMarkEnabled
在CMS垃圾回收器的“初始标记”阶段开启多线程并发执行
初始标记阶段,是会进行Stop the World的,会导致系统停顿,所以这个阶段开启多线程并发之后,可以尽可能优化
这个阶段的性能,减少Stop the World的时间
这个阶段的性能,减少Stop the World的时间
-XX:+CMSScavengeBeforeRemark
会在CMS的重新标记阶段之前,先尽量执行一次Young GC
CMS的重新标记也是会Stop the World的,所以所以如果在重新标记之前,先执行一次Young GC,就会回收掉一
些年轻代里没有人引用的对象。
所以如果先提前回收掉一些对象,那么在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升CMS的重新标记阶段的性能,
减少他的耗时
些年轻代里没有人引用的对象。
所以如果先提前回收掉一些对象,那么在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升CMS的重新标记阶段的性能,
减少他的耗时
G1垃圾收集器参数
-XX:+UseG1GC
使用G1垃圾回收器
-XX:MaxGCPauseMillis
G1执行GC的时候最多可以让系统停顿多长时间,默认200ms
-XX:G1HeapRegionSize
手动设置每个region的大小
-XX:G1NewSizePercent
设置新生代初始占比的。默认占堆的5%
-XX:G1MaxNewSizePercent
新生代最大占比,默认不会超过60%
-XX:InitiatingHeapOccupancyPercent
默认值是45%
如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段,Mixed GC
如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段,Mixed GC
-XX:G1MixedGCCountTarget
在一次混合回收的过程中,最后一个阶段执行几次混合
回收,默认值是8次
回收,默认值是8次
-XX:G1HeapWastePercent
默认值是5%
混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他
Region,然后这个Region中的垃圾对象全部清理掉
Region,然后这个Region中的垃圾对象全部清理掉
这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会 立即停止混合回收,意
味着本次混合回收就结束了。
而且从这里也能看出来G1整体是基于复制算法进行Region垃圾回收的,不会出现内存碎片的问题,不需要像CMS那样标记-清理之
后,再进行内存碎片的整理。
味着本次混合回收就结束了。
而且从这里也能看出来G1整体是基于复制算法进行Region垃圾回收的,不会出现内存碎片的问题,不需要像CMS那样标记-清理之
后,再进行内存碎片的整理。
-XX:G1MixedGCLiveThresholdPercent
默认值是85%,意思就是确定要回收的Region的时候,必须是存
活对象低于85%的Region才可以进行回收
活对象低于85%的Region才可以进行回收
日志打印参数
-XX:+PrintGCDetails
-XX:+HeadDumpOnOutOfMemoryError
调优思路
确定是否有频繁Full GC现象
1.1 如果Full GC频繁,那么考虑内存泄漏的情况
内存泄漏角度
1 使用jps -l命令获取虚拟机的LVMID
2 使用jstat -gc pid命令获取虚拟机的执行状态,判断full GC次数
3 使用jmap -histo:pid 分析当前堆中存活对象数量
4 如果还能定位到关键信息,使用jmap -dump打印出当前堆栈dump文件
jmap -dump:format=b,file=usr/loca/02.hprof 12942
5 使用MAT等工具分析dump文件,一般使用Histogram或者Dominatoor Tree分析。分析各个对象的内存占有率
1.2 如果full gc不频繁,各个区域内存占用正常,那么考虑线程阻塞、死锁、死循环情况
线程角度
1 使用jps -l命令获取虚拟机的PID
2 使用jstack分析各个线程的堆栈使用情况,如果说系统慢,那么要特别关注Blocked、Waiting on condition,如果说系统的CPU耗的高,那么肯定是线程执行有死循环,那么要关注Runable状态
3 如果还定位不了,打印dump文件
4 使用MAT等工具分析dump文件,一般使用Histogram或者Dominatoor Tree分析。分析各个对象的内存占有率
尽可能减少垃圾回收的频率,降低垃圾回收的时间,减少垃圾回收对系统运行的影
响,这就是JVM调优的目的
响,这就是JVM调优的目的
JVM核心知识点总结
JVM堆内存分代
新生代
短期存活的对象
存在Eden区,内存不够时候,触发yongGC
采用ParNew垃圾收集器
Eden:s1:s2 =8:1:1
老年代
长期存活的对象
在old区
采用CMS垃圾收集器
采用标记整理算法
老年代的垃圾回收算法的速度至少比新生代的垃圾回收算法的速度慢10倍
新生代与老年代比例:2:1
对象何时进入老年代
大对象直接进入老年区
就是“-XX:PretenureSizeThreshold”,可以把他的值设置为字节数,比如“1048576”字节,就是1MB。大于1M的对象直接进入老年代
长期存活对象进入老年区
如果一个实例对象在新生代中,成功的在15(默认)次垃圾回收之后,还是没被回收掉,
也就是对象的年龄,每垃圾回收一次,如果一个对象没被回收掉,他的年龄就会增加1。当它的年龄增加到一定程度(默认为15)时,就会晋升到老年代
也就是对象的年龄,每垃圾回收一次,如果一个对象没被回收掉,他的年龄就会增加1。当它的年龄增加到一定程度(默认为15)时,就会晋升到老年代
通过JVM参数“-XX:MaxTenuringThreshold”来设置,默认是15岁
对象动态年龄判定
无需等待15岁之后
假如说当前放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的50%,
那么此时大 于等于这批对象年龄的对象,就可以直接进入老年代了。
那么此时大 于等于这批对象年龄的对象,就可以直接进入老年代了。
实际这个规则运行的时候是如下的逻辑:
年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区 域的50%,此时就会把年龄n以上的对象都放入老年代。
年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区 域的50%,此时就会把年龄n以上的对象都放入老年代。
新生代垃圾回收(yonggc)之后,因为存活对象太多,无法放入Survivor区,导致大量对象直接进入老年代
老年代触发垃圾回收的时机(fullgc)
1 没有打开“ -XX:HandlePromotionFailure”选项(jdk1.6以后废弃)
2 Minor GC之前,都检查一下“老年代可用内存空间” < “历次Minor GC后升入老年代的平均对象大小
3 Minor GC之后,发现剩余对象太多放入老年代都放不下了。
4 设置了“-XX:CMSInitiatingOccupancyFaction”参数
比如设定值为92%,那么此时可能前面几个条件都没满
足,但是刚好发现这个条件满足了,比如就是老年代空间使用超过92%了,此时就会自行触发Full GC
足,但是刚好发现这个条件满足了,比如就是老年代空间使用超过92%了,此时就会自行触发Full GC
老年代空间担保机制
什么是空间担保机制
执行任何一次Young GC之前,JVM会先检查一下老年代可用的可用内存空间,是否大于新生代所有对象的总大小。
如果说发现老年代的内存大小是大于新生代所有对象的,此时就可以放心大胆的对新生代发起一次Minor GC了,因为即使Young GC之 后所有对象都存活,Survivor区放不下了,也可以转移到老年代去。
如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Young GC,但这次Young GC依然是有风险的;
如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Young GC,但这次Young GC依然是有风险的;
如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC。
yonggc过后的三种情况
1 Young GC过后,剩余的存活对象的大小,是小于Survivor区的大小的,那么此时存活对象进入Survivor
区域即可
区域即可
2 Young GC过后,剩余的存活对象的大小,是大于 Survivor区域的大小,但是是小于老年代可用内存大小
的,此时就直接进入老年代即可。
的,此时就直接进入老年代即可。
3 Young GC过后,剩余的存活对象的大小,大于了Survivor区域的大小,也大于了老年代可用内
存的大小。此时老年代都放不下这些存活对象了,就会发生“Handle Promotion Failure”的情况,这个时候就会触
发一次“Full GC”。
存的大小。此时老年代都放不下这些存活对象了,就会发生“Handle Promotion Failure”的情况,这个时候就会触
发一次“Full GC”。
full gc过后内存空间还不够,则产生OOM
为什么进行空间担保
是因为新生代采用复制收集算法,假如大量对象在Minor GC后仍然存活(最极端情况为内存回收后新生代中所有对象均存活),而Survivor空间是比较小的,这时就需要老年代进行分配担保,把Survivor无法容纳的对象放到老年代。老年代要进行空间分配担保,前提是老年代得有足够空间来容纳这些对象,但一共有多少对象在内存回收后存活下来是不可预知的,因此只好取之前每次垃圾回收后晋升到老年代的对象大小的平均值作为参考。使用这个平均值与老年代剩余空间进行比较,来决定是否进行Full GC来让老年代腾出更多空间。
G1和CMS区别
算法不一样
1 CMS基于标记-清除,产生空间碎片
G1采用标记-整理、标记-复制算法,不会产生空间碎片
回收STW不同
CMS初始标记、重新标记需要STW
G1除了并发标记外,都需要STW
内存占用不同
为了解决跨代引用问题,在新生代引入的记录集(Remember Set)的数据结构(记录从非收集区到收集区的指针集合),避免把整个老年代加入GCRoots扫描范围。
卡表是记忆集的具体实现
G1每个region都有一个卡表,导致G1的记忆集可能占整个堆的20%或者更多
CMS只有一份卡表,而且只需要处理老年代到新生代的引用,反过来不需要
为了解决跨代引用问题,在新生代引入的记录集(Remember Set)的数据结构(记录从非收集区到收集区的指针集合),避免把整个老年代加入GCRoots扫描范围。
卡表是记忆集的具体实现
只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0.
GC时,只要筛选卡表中变脏的元素加入GCRoo
GC时,只要筛选卡表中变脏的元素加入GCRoo
堆内存6-8G分界点,小内存适合CMS,大内存适合G1
合理的JVM参数设置
永久代:几百M
栈
xss:1m
类何时被回收
1 首先该类的所有实例(堆中)都已经被回收
2 其次该类的ClassLoader已经被回收
3 对该类对应的Class对象
没有任何引用
没有任何引用
满足上面三个条件就可以回收该类
GCRoots对象
方法局部变量
类的静态变量
对象何时被回收?
有GC Roots引用的对象不能回收,没有GC Roots引用的对象可以回收,如果有GC Roots引用,但是如果是软引用或者弱引用的,也
有可能被回收掉
有可能被回收掉
为什么新生代回收效率高?
老年回收效率慢?
为什么新生代不用标记整理算法
老年回收效率慢?
为什么新生代不用标记整理算法
(时间换空间s1、s2)新生代复制算法比较快。Eden区回收时直接全部清空,存活的对象存放到内存容量比较小的s1,少了解决内存碎片整理 加上直接copy的速度,效率很高。
新生代存活率低不需要预留太多内存
新生代存活率低不需要预留太多内存
(空间换时间),老年代标记清除算法会导致内存碎片化,因此就引入了标记整理算法,执行完毕后,存活的对象会按序放置,移动对象的内存地址(重点),来解决碎片化,但是执行时间较长。
因为老年代存活率高,你回收需要预留比较大的空间。这样的话内存利用率就比较低,所以考虑到这个老年代就用标记清除或者标记整理的方法。
因为老年代存活率高,你回收需要预留比较大的空间。这样的话内存利用率就比较低,所以考虑到这个老年代就用标记清除或者标记整理的方法。
parnew+cms的gc,如何保证只做ygc,jvm参数如何配置?
加大分代年龄,比如默认15加到30;
修改新生代老年代比例,比如新生代老年代比例改成2:1
修改e区和s区比例,比如改成6:2:2
老年代Concurrent Mode Failure问题
OOM的几种情况
java.lang.StackOverflowErro
堆栈溢出,我们有最简单的一个递归调用,就会造成堆栈溢出,也就是深度的方法调用
java.lang.OutOfMemoryError
java heap space
创建了很多对象,导致堆空间不够存储
GC overhead limit exceeeded
GC回收时间过长时会抛出
Direct buffer memory
堆外内存引起的,Netty + NIO:这是由于NIO引起的
本地内存不足,但是堆内存充足的时候,就会出现这个问题
本地内存不足,但是堆内存充足的时候,就会出现这个问题
unable to create new native thread
不能够创建更多的新的线程了,也就是说创建线程的上限达到了
Metaspace
元空间内存不足,Matespace元空间应用的是本地内存
自由主题
自由主题
收藏
0 条评论
下一页