JVM
2021-05-07 16:54:18 129 举报
AI智能生成
JVM(Java虚拟机)是Java技术的核心,它是一个虚拟的计算机,负责执行Java字节码。JVM的主要任务是加载、验证和执行Java程序,同时管理内存、垃圾回收和多线程等资源。JVM具有跨平台特性,可以在各种操作系统和硬件平台上运行Java程序。JVM的架构包括类加载器、运行时数据区、执行引擎等组件,它们共同确保了Java程序的高效运行和稳定可靠。
作者其他创作
大纲/内容
jvm整体运行原理
java文件
编译成.class文件
类加载器吧class文件加载jvm中执行
类什么时候加载
1.实例化对象时,就像spring管理的bean一样,在tomcat启动时就实例化了bean,那么这个对象bean的类就加载了
2.通过类名调用静态变量的时候(类名.class除外)
类加载过程
加载
在内存中生成一个代表这个类的java.lang.Class对象
验证
验证class文件是否符合jvm的规范,防止文件被篡改
准备
给类分配内存空间,static变量分配内存空间,给类变量赋初始值
解析
符号引用替换为直接引用,把赋值等号指向地址。
初始化
static int interval=Configuration.getInt("flush.interval");
准备阶段,仅仅赋值为0
执行static静态代码块
什么时候执行初始化,在new对象,实例化的时候执行
重要规则:实例化的类的时候。假如有父类,必须先初始化父类
使用
卸载
类加载器分类
启动类加载器
Bootstrap ClassLoader, 主要负责加载java目录下的核心类,lib目录下
扩展类加载器
负责加载lib\ext目录下类
应用类加载器
加载classPath环境变量所制定路径中的类,加载我们写好的Java代码
自定义类加载器
根据自己需求加载你的类
双亲委派
假设应用程序类加载器加载一个类,首先委派父类加载器去加载也就是扩展类加载器,
依次往上,最终传导到启动类加载器,如果父类加载器没找到这类,就下推子类加载器
依次往上,最终传导到启动类加载器,如果父类加载器没找到这类,就下推子类加载器
避免多层级的加载器重复加载某些类
Tomcat打破了双亲委派机制,每个WebApp负责加载自己对应的那个Web应用的class文件,
也就是我们写好的某个系统打包好的war包中的所有class文件,不会传导给上层类加载器去加载
也就是我们写好的某个系统打包好的war包中的所有class文件,不会传导给上层类加载器去加载
Jvm运行原理
存放类的方法区
JDK1.8之后改成metaspace(元数据空间),存放一些类信息和常量池
执行代码指令用的程序计数器
class文件存放的都是字节码执行,机器能识别的,所以java文件被翻译成字节码指令
类信息加载到内存后,由字节码执行引擎去执行字节码指令
因jvm支持多线程,所以每个线程都有一个程序计数器,记录线程执行到哪条字节码指令了
Java虚拟机栈
每个线程都有自己的java虚拟机栈
执行每个方法的时候,都会创建一个栈帧,局部变量都放到这个栈帧中,方法执行完了,出栈,局部变量也就失效
栈帧如果没有执行完时,其实都是GC Root
默认大小是1M
局部变量保存的都是对象的地址,地址指向了JVM堆内存
jvm垃圾回收机制
垃圾的产生
方法入栈,局部变量压入栈帧,同时指向堆内存地址
堆内存存放实例化对象,在方法运行时创建的加载到堆内存
执行方法完毕后,方法出栈,堆内存至此无局部变量引用,变成垃圾。
jvm进程自带一个后台垃圾回收的线程
一般static变量会长期存活,但也会先出现在新生代里
新生代回收触发条件:当对象分配新内存不足时
对象在java堆内存里占用空间
1,对象自己本身的一些信息
对象头
在64位linux系统中,占用16位字节
2,对象的实例变量作为数据占用空间
实例变量内部假如有一个是int的变量,则占用4个字节
long的占用8个字节
案例实战:设置堆内存大小
案例场景:每天1百万QPS,比如支付请求,下单请求
抛开其他针对并发,性能的架构措施,单单分析百万请求,应该用到几台机器,每台需要多大内存你空间
每台机器堆内存设置多大合适,怎么不让内存崩溃,溢出
每台机器堆内存设置多大合适,怎么不让内存崩溃,溢出
分析:高峰时期,限流每秒最大100个订单。假设三台机器,每台处理30个订单,每个请求占500字节大小,15KB,
实际运行期间,产生大量对象,扩大10倍,就是1M左右。
实际运行期间,产生大量对象,扩大10倍,就是1M左右。
选用机器:假如2核4G,机器本身占用2G内存,堆内存可能就只有1G分配空间,新生代可能就几百M,建议改用4核8G机器,给堆内存分配2G,可能半个小时到一个小时期间才发生一次minor GC
问题:假如用4G机器,大促上来后,每秒处理1000订单,那就是每秒10M,但是有些订单可能需要几秒才能处理完,所以每次Minor GC有几十M对象存活。并且性能可能突然下降,处理很慢,这时,新生代频繁GC,少数存活对象移入老年代,老年代也会很快填满。老年代一旦频繁GC,悲剧了
老年代一般设置个几百M大体够用,栈内存,一般就是512K,或者1M,就够用
引用类型
强引用
就是普通的代码,一个变量引用一个对象
软引用
SoftReference泛型把对象包裹起来
正常情况下不会回收软引用对象,但是垃圾回收之后,内存空间仍然不够,会把软引用对象给回收掉
弱引用
WeakReference泛型把对象包裹起来
弱引用就跟没引用是类似的
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期,垃圾回收时,不管当前内存空间足够与否,都会回收它的内存
应用场景
ThreadLocalMap有个静态内部类Entry,entry对象里的 k是弱引用指向这个ThreadLocal对象
在方法中新建一个ThreadLocal对象,就有一个强引用指向它,在调用set()后,线程的ThreadLocalMap对象里的Entry对象又有一个引用 k 指向它。如果后面这个引用 k 是强引用就会使方法执行完,栈帧中的强引用销毁了,对象还不能回收,造成严重的内存泄露
虚引用
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收
区别:
1,虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中
2,程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收
3,如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动
怎么找到需要回收的对象
引用计数法
通过引用是否存在的方法就叫做引用计数法
存在一个问题就是无法解决对象循环引用的问题
可达性分析
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索
当一个对象到GC Roots没有任何引用链相连(从GC Roots到这个对象不可达)时,则证明此对象是不可用的。所谓的gc root,你可以理解为我们正在使用的对象
GC Roots的对象
虚拟机栈(栈帧中的本地变量表)中引用的对象
通常意义上Java代码new一个对象引用,这个对象引用所在的地方
方法区中类静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(即一般说的Native方法)引用的对象
ParNew垃圾回收器
为什么是复制清除算法:
1,假如使用标记算法,会产生大量的内存碎片,因为碎片不是连续的,对象大的话,存不下,就会造成内存浪费。
2,普通复制算法的缺点,使用效率太低,假如1G内存,只能每次使用512M
3,在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用“复制算法”,只需要付出少量存活对象的复制成本就可以完成收集
4,如果存活对象的数量比较大,coping的性能会变得很差
1,假如使用标记算法,会产生大量的内存碎片,因为碎片不是连续的,对象大的话,存不下,就会造成内存浪费。
2,普通复制算法的缺点,使用效率太低,假如1G内存,只能每次使用512M
3,在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用“复制算法”,只需要付出少量存活对象的复制成本就可以完成收集
4,如果存活对象的数量比较大,coping的性能会变得很差
优化复制清除算法:
1个eden,2个survivor,默认比例,8:1:1,所以只浪费了10%及其中一个survivor,回收时会Stop the World
ParNew是一个多线程的垃圾回收器,线程数量默认情况下和cpu个数相同,一般应用于服务端(比如,电商服务,业务系统,app后台系统),因多核cpu
Serial回收器是单线程的回收器,一般应用于客户端,比如百度云盘windows客户端
进入老年代的条件
存活年龄太大,默认超过15次
大对象存不下
JVM参数里是可以设置的,一般我们都设置1M
动态年龄判定规则
Minor GC后发现survivor区域中几个年龄对象加起来大于Survivor区域50%,比如年龄1+2+3对象大小总和超过50%,则把3年龄以上对象放入老年代
Minor GC后对象太多无法放入survivor
空间担保机制
-XX:-HandlePromotionFailure ,必须设置才起作用
在Minor GC之前,判断老年代可用内存是否已经小于新生代全部对象大小。如果小于,
继续判断,老年代大小是否小于之前每次Minor GC后进入老年代的对象平均大小,
如果上面两次判断都成功,冒险试一下Minor GC
继续判断,老年代大小是否小于之前每次Minor GC后进入老年代的对象平均大小,
如果上面两次判断都成功,冒险试一下Minor GC
1,Minor GC后,剩余存活对象小于survivor大小,直接进入survivor区域
2,Minor GC后,剩余存活对象大于survivor大小,但是小于老年代可用内存,直接进入老年代
3,Minor GC后,剩余存活对象大于survivor大小,也大于老年代可用内存,进行一次full GC
4,假如Full GC后仍然没有足够内存放Minor GC的剩余对象,就会OOM
在JDK1.6之后废弃参数
只是参数废弃,但是机制仍在
只是参数废弃,但是机制仍在
只要判断,老年代可用空间>新生代对象总和
或者,老年代可用空间>历次Minor GC升入老年代对象的平均大小
2者任意一个满足,直接进行Minor GC,不会提前触发Full GC
CMS垃圾回收器
为什么是标记-清理”或“标记-整理算法:
老年代存活对象的数量比较大,复制的性能会变得很差,会进行很多次的复制操作
在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收
标记-清除 + 整理 算法,JVM参数默认是,标记-清除(5次之后,才会去整理内存空间),有可能会产生内存碎片,大对象就有可能直接进入老年代,导致fullGC,一般会设置为0(标记-清除 + 整理效果),就不会产生内存碎片
1,初始标记:标记出来所有GC Roots直接引用的对象,会stop the world
2,并发标记:这阶段会让系统线程可以随意创建新对象,并且对已有的对象进行GC Roots追踪,很耗时
3,重新标记:重新标记第2阶段里新创建的对象和垃圾对象,会stop the world
4,并发清理:清理掉标记的垃圾对象
Full GC的条件
1,老年代自身设置阈值,一旦超过,触发,一般设置大一些,92%
2,Young GC前,jdk1.6之后,判断老年代可用空间小于历次Young GC平均对象大小。先Full GC,再Young GC
3,Young GC后存活对象太多,survivor放不下,老年代也放不下。就会触发
优化jvm核心就是减少full GC频率,正常频率,几十分钟或者几个小时一次,一次耗时大约几百毫秒。
频繁Full GC表现形式
1,机器cpu负载过高
某个进程耗尽了cpu资源,导致自己的服务线程始终无法得到cpu资源,也就无法响应接口调用的请求,可能会造成进程被杀死
2,,频繁Full GC报警
3,系统无法处理请求或者处理过慢
频繁Full GC的原因
1,内存分配不合理,快速进入老年代
2,内存泄漏,内存驻留大量的对象。比如本地缓存没LRU算法之类的释放
3,永久代里类太多,比如反射造成的。
ParNew + CMS 调优
如何保证只做MinorGC
1,加大分代年龄,比如默认15,加到30
2,新生代老年代比例改成2:1
3,新生代,edn区和S区比例,改成6:2:2。默认8:1:1
系统太卡
说白了就是Stop the World 太久。减少stw时间
YoungGC一般STW时间特别短,Old GC时间一般会是Young GC的几倍到几十倍,优化的重点是让系统减少Old GC的次数
jstat 优化
Eden 区对象的增长速度
Minor GC 频率
Minor GC 耗时
Young GC 后多少对象存活
老年代对象增量速度
Full GC 频率多高
Full GC次数
一次Full GC 的耗时
每两秒输出一次,一共输出10次
jstat -gc 【PID】2000 10;
JVM调优案例
1,System.gc(),高并发的情况下出问题
2,高并发场景下young gc后存活对象过多,导致对象快速进入老年代。
解决办法:增加集群机器,增加survivor区,对象不进入老年代
XX:CMSFullGCCsBeforeCompaction=5改成=0,解决老年代标记清除算法时产生的内存碎片问题
XX:CMSFullGCCsBeforeCompaction=5改成=0,解决老年代标记清除算法时产生的内存碎片问题
3,反射机制会创建软引用的一些类
解决办法:不能设置-XX:SoftRefLRUPolicyMSPerMB=0,参数改大2000,3000,或者5000毫秒,让反射创建的软引用类不要被随便回收
4,突然有几百M的大对象进入老年代。
解决办法:jmap导出dump快照,mat分析大对象。有可能就是查询数据库全表造成的。调大survivor区
5,CPU负载过高的原因
第一个场景:系统创建了大量的并发线程
第二个场景:JVM频繁的Full GC,频繁Full GC的原因
1,内存分配不合理,快速进入老年代
2,内存泄漏,内存驻留大量的对象。比如本地缓存没LRU算法之类的释放
案例1
大促活动访问量突然变大
重启服务后,立马又打满cpu
进程耗尽了cpu资源,导致自己的服务线程始终无法得到cpu资源,也就无法响应接口调用的请求
使用mat工具查看
原因:系统里维护大量的jvm缓存没有及时释放内存
案例2
一次性加载几十万条数据
并且做了String.split(),切割为N个小字符串,暴增10多倍
引发每分钟执行minor gc,2分钟执行full gc
去掉split操作,或者开启多线程执行
3,永久代里类太多,比如反射造成的。
服务类加载过多引发的OOM问题
如果服务出现⽆法调⽤接⼝假死的情况
(1)第⼀种问题
个服务可能使⽤了⼤量的内存,内存始终⽆法释放,因此导致了频繁GC问题
也许每秒都执⾏⼀次Full GC,结果每次都回收不了多少
导致系统因为频繁GC,频繁Stop the World,接⼝调⽤出现频繁假死的问题
(2)第⼆种问题
机器的CPU负载太⾼了,也许是某个进程耗尽了CPU资源,导致你这个服 务的线程始终⽆法得到CPU资源去执⾏
OOM的条件
Metaspace溢出:java.lang.OutOfMemoryError:Metaspace
一般程序运用aop实现动态代理,bug导致类加载过多
栈溢出:java.lang.StackOverFlowError
递归调用方法 ,或者死循环
堆溢出:java.lang.OutOfMemoryError:java heap space
高并发请求量过大,导致大量对象存活 ***内存泄漏,不停的创建对象,被引用
实操
jps
查看java进程
jmap -histo 1346 |head 20
查看占比最多的前20个对象
不能用在生产环境,因为卡顿很严重,假如内存32G,可能出来结果需要1个小时
如何在JVM内存溢出的时候⾃动dump内存快照
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/local/app/oom
可能发送oom的区域
meta space
第一种原因
默认的metaspace才几十M,当系统很大,在加载类的时候,很容易就满了
一般推荐设置metaspace的大小是512M
第二种原因
cglib动态代理生成一些类,代码控制不好,也容易溢出
java.lang.OutOfMemoryError:Metaspace
虚拟机栈内存
每次调用方法时,栈帧都是占用内存的,当方法递归调用时,一般是bug导致
即便没有任何的变量,栈帧也是占内存的,很快也会吧栈内存塞满,发生溢出
模拟
java.lang.StackOverflowError
堆内存
第一种原因
高并发请求,导致大量对象存活来不及回收
第二种原因
内存泄漏
模拟
java.lang.OutOfMemoryError:Java heap space
生产案例1
MQ宕机,数据存内存,且内存没回收,导致full gc后仍然无法释放,堆内存OOM
mq宕机故障后,把消息存在内存里,等待mq中间件重启,导致占用内存越来越大
内存的数据越来越多,eden区塞满后,大量存活对象转入老年代,而老年代里这些对象在full gc后仍然无法释放
生产案例2
递归造成栈内存OOM
im客服系统,高并发的情况下,分配等待中的用户,在负载分配的情况下,一旦获取到负载最低的客服,正打算分配的时候,突然发现客服被占满了,然后重新进行递归选取另一个负载最小的客服,由于代码没控制好。造成无限递归
生产案例3
RPC调用超时时间设置为4秒,100并发量情况下,400个线程同时执行
上百并发的系统发生oom
tomcat本身即是一个jvm进程
tomcat有很多自己的工作线程,少则一百,多则三四百
日志:
Exception in thread "http-nio-8080-exec-1089"
java.lang.OutOfMemoryError: Java heap space
Exception in thread "http-nio-8080-exec-1089"
java.lang.OutOfMemoryError: Java heap space
分析日志:发现占据内存最大的是大量的“byte[]”数组,一大堆的byte[]数组就占据了大约8G左右的内存空间
大约有800个数组,大小都是一致的10MB,总共8G
发现Tomcat的工作线程大致有400个左右,也就是说每个Tomcat的工作线程会创建2个byte[]数组,每个byte[]数组是10MB左右
每秒才100个请求,怎么可能Tomcat的400个线程都处于工作状态
只有一种可能,那就是每个请求处理需要4秒钟的时间
系统的RPC调用超时的配置,惊讶的发现,负责这个系统的工程师居然将服务RPC调用超时时间设置为了刚好是4秒
一定是在这个时间里,RPC调用的远程服务自己故障
最核心的问题就是那个超时时间设置的实在太长了,因此立马将超时时间改为1秒即可
生产案例4
老年代太大,回收不频繁,导致堆外内存持续变大,且恰好jvm参数,禁止了system.gc
netty堆外内存溢出
java.lang.OutOfMemory:Direct buffer memory
堆外内存
同样需要回收操作
使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝
netty不停的创建堆外内存去申请内存空间
原因
内存分配及其不合理
年轻代一两百M,老年代800M,survivor只有10M 内存
jvm中的DirectByteBuffer关联了很多堆外内存,因为老年代一直没塞满,所以没触发full gc,也就是不会回收老年代里的DirectByteBuffer
这些没回收的buffer一直关联着大量的堆外内存
要继续使用堆外内存时,结果所有的堆外内存都被老年代里大量的directByteBuffer占用,虽然可以被回收,但是老年代一直没有触发full gc
NIO中每次分配新的堆外内存的时候,如果进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则进行 System.gc(),主动去回收掉一些DirectByteBuffer,释放堆外内存
但是我们在jvm参数配置中设置禁止System.gc()
-XX:+DisableExplictGC
解决方案
给年轻代设置更多内存,增大Survivor区域空间
放开-XX:+DisableExplictGC的限制,让System.gc生效
生产案例5
excel导入文件太大,导致对象序列化一个大的byte[]数组(大对象,GB级别),导致oom
一次微服务RPC调用引发oom
系统做了一个excel导入功能,但是没有对excel文件的大小做控制。
造成突然有几百M的大对象进入老年代。
传输过来的对象进行序列化,把对象变成一个byte[]数组
假如一个大文件导入,直接在系统中生成一个超大的byte[]数组,差不多有几GB大小
解决方案
限制文件大小
生产案例6
mysql全表查询几百万数据,引发oom
sql语句导致oom
使用mybatis写sql语句,进行了全表扫描,没有加where条件,一下子查询出上百万条数据引发系统oom
问题是堆内存溢出,然后分析快照
发现一个线程创建了一个巨大ArrayList,这个ArrayList里面有一个java.long.Object[]数组,继续展开,看到大量的对象
通过工具继续追查是哪个线程,哪个方法创建的这些对象
生产案例7
工程师自定义类加载器,重复加载大量的数据,导致进程被杀死,脚本重启系统,期间假死
web系统假死
问题:
经常一段时间内无法访问这个服务的接口,过了一会儿又可以访问了,会假死一小段时间
假死第一种问题
服务可能使用大量的内存,内存始终无法释放,导致频繁的GC问题
也许每秒都执行一次full gc,频繁stop the world,进而导致假死
假死第二种问题
机器的cpu负载太高,某个进程耗尽了cpu资源,导致自己的服务线程始终无法得到cpu资源,也就无法响应接口调用的请求
分析排查
因为不能根据日志立马定位问题
首先用linux的top命令检查机器的资源使用情况(cpu和内存)
通过top,发现cpu消费很少,但是内存资源占用超过50%以上
一台4核8G的机器,jvm总内存6G,堆内存大概会给到4G,所以超过50%的内存占用,就得引起注意了
jstat分析jvm运行情况,确实内存使用率很高,经常发生gc,但是也正常,也没有oom
第三个问题:jvm运行期间申请内存过多,结果内存不足了,有时候os会直接杀掉这个进程,一旦进程被删掉,会有脚本自动把进程重新启动起来,过了一会儿就又可以访问了
最终原因敲定:
工程师做了自定义加载器,没有做限制,创建了大量的自定义类加载器,去重复加载了大量的数据。结果内存耗尽,进程被杀死
解决方案
修改代码,避免重复创建几千个自定义的类加载器,避免加载大量的数据到内存里
生产案例8
mq消费快,但业务处理慢,导致阻塞队列过大,占用内存太多,oom
mq生产消费速率没控制好导致oom
分析工具,发现老年代使用率100%
原因:代码每次消费几百条数据出来给做成一个list,每个list都有几百条数据,然后慢慢消费
消费出来的数据放入队列速度很快,但是处理业务的速度慢,导致内存队列挤压数据,内存溢出
解决方案
把上述内存队列修改为定长阻塞队列ArrayBlockingQueue,比如最多1024个元素,而不是做成一个list放入队列作为一个元素
这样一旦内存队列满了,就不消费了,不让内存里数据过多
解决方案
最好公司有一套监控平台,比如zabbix,发送短信报警
cpu
磁盘
内存
网络
内存溢出时,dump内存快照
1,用工具mat分析溢出原因
2,使用 eclipse memory analyzer 分析dump文件
第一件事:重启服务,并且一定是登录到线上机器看日志
JVM使用的默认的垃圾收集器
cmd执行命令:
java -XX:+PrintCommandLineFlags -version
java -XX:+PrintCommandLineFlags -version
java8,新生代(Parallel Scavenge),老年代( Parallel Old)组合
parallel Scavenge和年老代Parallel Old收集器的搭配策略。在 JDK1.8及后(Parallel Scavenge + Parallel Old )
在默认情况下,会开启-XX:UseParallelGC参数,此时,新生代使用了Parallel New ,老年代使用了Parallel Old
在JDK1.6之前,新生代使用Parallel Scavenge收集器只能搭配年老代的Serial Old收集器
串行垃圾回收器(Serial):
它为单线程环境设计并且只使用一个线程进行垃圾回收,会暂停所有的用户线程。所以不适合服务器环境
在进行垃圾收集的时候必须停下所有的工作
并行垃圾回收器(Parallel):多个垃圾回收线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理等弱交互场景。
并发垃圾回收器(CMS):用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程。互联网公司多用它,适用于对响应时间有要求的场景
G1垃圾回收器:G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收
0 条评论
下一页
为你推荐
查看更多