JVM
2021-06-29 12:01:20 95 举报AI智能生成
JVM知识点大全
JVM
模版推荐
作者其他创作
大纲/内容
JVM基础
GC垃圾回收<br>
引用计数器
古老的确认垃圾方式,但是无法确认循环引用
根可达算法GCRoots
来源于jvms,java虚拟机规范
从根上找引用
比如main方法里面一定会有一些引用,Object a = new Object(); String b = new String();<br>a和b就是根。
分代
年轻代对象进入老年代
cms默认6次进入老年代
ps/po才是15次
垃圾清除算法
标记-清除
复制
标记-整理(压缩)
效率偏低
STW stop the world
工作线程全部停止
垃圾收集器
1.8默认Parallel Scavenge和paraller Old,PS/PO,UseParallel
建议1.8用G1
垃圾回收器发展路线是跟随内存的增长不断调整的
Serial+Serial Old
Serial
垃圾满了以后,停止一切工作线程,进行垃圾清理。
单线程的并且是stop the world的垃圾回收器
Serial Old
在Old区域的单线程的并且是stop the world
单线程这种支持个10-100M就差不多了,再大stw的时间会很长
parallel Scavenge/parallel Old<br>1.8默认
stop zhe world,然后多线程清理垃圾
默认线程数为CPU的核数
这种CPU算力比较强的话几个G就差不多了
ParNew+CMS
ParNew
原理跟PS是一样的,但是是主要配合CMS使用的
CMS
老年代的垃圾回收器,concurrent mark sweep,标记清除
一共7个阶段
1.initial mark 初始标记
还是先进行stw,然后只找到最根上的那些对象。所以stw时间会非常短
2.concurrent mark 并发标记
工作线程不停,同时进行标记GCRoot
遍历所有对象,看哪些对象是根GCRoot关联的,如果是的话,就不需要被清除,如果不是就清除
问题:并发标记的时候要把对象标记为垃圾了,但是后面有一个对象又把这个对象连上了。
这个时间是比较长的,但是工作线程也在执行,所以不会卡。
三色标记法
cms,g1等都是在这块下功夫。同时标记的时候能够找到误标的和漏标的。
CMS:Incremental Update
G1:SATB
3.remark 重新标记
由于上面的问题,有错标的,漏标的。所以使用重新标记进行修正。
也必须进行stw,但是由于漏标错标的情况比较少,所以stw的时间也不会很长
4.concurrent sweep 并发清理
cms是标记清理,其他两个是标记整理
三色标记算法
逻辑上分为三种类型对象:
白色
找到了对象,但是没有对这个对象标记
灰色
识别了对象不是垃圾,但是还没有找成员变量
黑色
识别了对象不是垃圾,而且成员变量也标记完了
问题
B指向D的标记没了,D编程浮动垃圾,不影响
漏标:灰的指向白的 没了,但是黑的指向了白的,但是扫描的时候A已经被标记为黑的了,不会扫面A的成员变量了
CMS对三色标记的优化方案 Incremental Update
A指向D后,把A从黑色变为灰色
为什么G1不用这种解决方案了
因为并发标记还是会有漏标问题
A 属性1,2标记完,但是另一个线程把2又指向了另一个未标记的对象,原来的线程不知道就把A标记为黑色了
所以在CMS remark阶段,必须从头扫描一遍
G1对三色标记的优化方案: SATB
解决B指向D消失的问题
把B指向D的指针保存到一个栈里面去
下次扫描的时候去栈里看看有没有放进去的指针,如果有就拿出来,然后把 对象重新扫描
RSet rememberSet
问题
浮动垃圾
并发清理的时候,在清理的时候也会产生新的垃圾
CMS用的是<font color="#ff0000">标记清除算法</font>,碎片化的问题会越来越严重
在碎片化特别严重的时候,<font color="#ff0000">居然用的是Serial Old进行整理</font>,STW会非常严重
G1
逻辑分代,物理不分代
把内存分成一个个的Region(区域)。Region个数默认2000个<br>
大小在1M-32M,并且必须是2的次方。
每个小区域可以是老年代,Eden,Servivor
有的对象特别大的时候,可以把区域连起来 Humongous大对象区
声明一个对象超过了单个Region大小怎么办?
1. 0.5Region<Object <1Region
直接存到Old区,并且这个Old区标记为Humongous
2. Object> 1Region
申请多个H区存放一个对象
概念
Rset RememberSet
每个Region会有一个区域给到Rset,存储别的Region引用当前Region的记录。<br>存储哪些Region引用了自己
Cset Collection Set
本次GC需要清理的Region集合
垃圾收集流程
YoungGC
复制算法
MixGC--OldGC
1.初次标记
标记GCRoot直接引用的对象,标记GCRoot所在的Region(Root Region)
2.RootRegion拿过来,去扫描整个Old区的所有Region,去看所有Old Region的Rset中是否含有RootRegion,如果有,就标识出来。<br>
3.并发标记
跟CMS作用相同,标记GCRoot指向的对象,只不过遍历范围缩小,只需要遍历上面标记的区域
4.重新标记
同CMS,G1用了新的算法SATB
5.清理
采用了复制清理的方式
只选垃圾较多Region进行清理
虽然清理不干净但是快
筛选回收
首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,回收没有存活对象的Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率
CMS和G1对比
1.Region化的内存结构,采用复制清理的算法,避免了内存碎片。但这种清理也会造成stw。但是只清理垃圾较多的Region,最大限度降低了STW的时间。
2.重新标记使用SATB算法速度更快了,提高了效率
3.初始标记和youngGC的STW一起了,提高了效率
3.在并发标记之前使用Rset缩小了扫描范围,提高并发标记速度
子主题
3.初始标记,并发标记,重新标记,清理垃圾四个阶段很像
回收GC
YCG/minorGC
youngGC年轻代回收
FGC/majarGC
FullGC老年代回收
一个对象从出生到消亡
1.首先根据逃逸分析和标量替换看是否能分配到栈上
分配到栈上,能够随着栈桢消失
速度会非常快,不需要GC介入
2.如果Object过大,就放到老年代
3.尝试放在TLAB上
TLAB的全称是Thread Local Allocation Buffer,即线程本地分配缓存区,这是一个线程专用的内存分配区域。
由于对象一般会分配在堆上,而堆是全局共享的。因此在同一时间,可能会有多个线程在堆上申请空间。因此,每次对象分配都必须要进行同步(虚拟机采用CAS配上失败重试的方式保证更新操作的原子性),而在竞争激烈的场合分配的效率又会进一步下降。<font color="#ff0000">JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。</font><br>
<font color="#ff0000">TLAB本身占用eEden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间</font>。参数-XX:+UseTLAB开启TLAB,默认是开启的。TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,当然可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
多个线程会去Eden抢存放对象的位置。所以会有一个线程同步的问题。<br>因此HotSpot会有一个优化,在Eden为每个线程分配一小块区域。线程先往TLAB尝试放进去,如果大小放不进去才会放到Eden其他位置<br>
4.放到Eden中
调优
java参数
1.-开头:标准参数,所有版本都支持
-Xms<size> 设置初始 Java 堆大小
-Xmx<size> 设置最大 Java 堆大小
最小堆和最大堆设置成一样的
如果确定整个空间大小,但是设置了最小最大。那么等真正分配内存的时候,JVM会动态计算需要多少内存,不断扩大缩小内存,这个过程很浪费资源
2.-X 开头:非标参数,有可能将来会被替换掉
3.-XX 开头:真正调优参数
java -XX:+PrintCommandLineFlags 打印出来启动参数
-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops <font color="#ff0000">-XX:+UseParallelGC</font>
java -XX:PrintFlagsFinal 打印出来所有的参数
调优基础概念
方向
1.根据需求进行JVM规划和预调优
2.优化运行JVM运行环境(慢,卡顿)
3.解决JVM运行过程中出现的各种问题(OOM)
java -Xms30M -Xmx30M -XX:+PrintGC com.xxx
top命令查看<br>jps java的进程号
命令
jstat -gc pid 时间
每隔一段时间动态打印虚拟机内存区域,最基础的最原始的
jstack pid
把java当前进程里所有的线程列出来,一般用来发现有没有死锁
waiting on condition[锁的编号],如果有多个线程都wait在同一个锁的编号上,有可能就产生死锁了
线程的名字:阿里开发规范:创建线程或线程池时请指定有意义的线程名称。就是为了问题的排查
1.cpu出现了飙高
先从jstack命令开始排查,可以看到哪个线程的cpu利用率时飙高的
具体得看时gc线程飙高还是业务线程的飙高
jmap -histo pid |head -20
生成对象图,查找有多少对象产生,把当前时间堆内存里所有的对象列出来
还能导出整个堆
注意:生产环境中不能随便用,用jmap,堆会暂停
小公司,或者压测
很多服务器高可用,停掉这台服务器对其他服务器不影响。
1.设定了参数HeapDump,OOM的时候会自动产生堆转储文件
java -Xms20M -Xmx20M -XX:+UseParallelGC <font color="#ff0000">-XX:+HeapDumpOnOutOfMemoryError</font> com.mashibing.jvm.gc.T15_FullGC_Problem01
生成堆转储文件
1.通过jmap -dump:format=b,file=xxx pid :
2.通过arthas生成
会暂停线程,FullGC
不要随便到处dump
很多服务器备份,停掉不影响
设置OOM自动产生堆转储文件
不是很专业,因为多有监控器,内存增长就会报警。
2.很多服务器备份(高可用),停掉这台服务器堆其他服务器不影响
图形工具
自带工具
jvisualvm
可以连远程,本地
不可以用图形
原因:1.linux 2.不可能随便开接口让连接
2.测试可以用,但是一般不用
arthas
阿里开源
使用JVMTI写的,可以用JVM写一些工具
命令
dashboard 查看仪表盘
查看线程是不是占用很高
查看是否内存占用百分比过高等问题
jvm 显示
thread 查看有哪些线程在运行<br>
thread 16 查看具体线程信息
thread | grep main 查看带main的所有线程
thread -b 直接查看死锁线程
sc 把所有加载的类的信息显示出来
sc * 显示所有
sc *com.xxx.xxx* 把包下的所有的信息显示出来
sm 查方法
sm *com.xxx*
trace 指定一个方法的调用,跟踪执行时间
monitor 跟踪方法的哪些值被传进去了,哪些值被返回了
jad 做反编译
版本不对,通过这个命令看服务器上的代码到底是哪个版本。
正常更新一个是修改代码,编译打包,更新重启
大公司redefine
redefine 直接在内存里把源码改了,重新放回内存
应急性操作
查找死锁
1.jstack
2.arthas 的thread -b 直接找到
内存泄露
1.怀疑哪快就用arthas跟踪
2.记录好日志,看哪些类在不断的产生
CPU飙高
1.jstack
2.Arthas thread
去看哪个线程CPU飙高
如果是GC线程,那说明在频繁GC,那有可能某些对象回收不掉,可能还是OOM问题
调优步骤
设定日志参数
-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause<br>
或者每天产生一个日志文件
-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log 设置日志文件名<br>-XX:+UseGCLogFileRotation 滚动日志 如果最后一个日志满了覆盖写第一个<br>-XX:NumberOfGCLogFiles=5 文件个数<br>-XX:GCLogFileSize=20M 每个文件大小<br>-XX:+PrintGCDetails <br>-XX:+PrintGCDateStamps <br>-XX:+PrintGCCause<br>
子主题
通过GClog 查看gc情况
jmap查看进程里生成的对象
jmap -histo 1659 |head 20
生成对象的个数,大小
内存泄漏与内存溢出
Memory leak 内存泄漏
内存里有个对象,没什么用也不释放
out of Memory 内存溢出
内存泄漏的对象多了,就容易产生内存溢出
OOM如何进行定位的
用了arthas,然后用了jmap命令
JVM<br>待细学
jvm运行原理
堆内存:执行方法的时候,方法里针对的对象放在堆内存。
栈内存:执行代码的时候,肯定会有很多的线程,tomcat里就有很多自己的工作线程,去执行我们写的代码,每个工作线程都会有自己的一块数据结构,栈内存。线程独有。
永久代:代码要被执行,类加载到永久代
java8以后内存分代的改进
<font color="#c41230">jdk1.7:永久代里放了一些常量池+类信息 ---jdk8:常量池放到堆里面,类信息放到metaspace元空间里。</font>
jvm如何运行起来的
一定有线程去执行我们写的代码
比如说我们有一个类里面包含了一个main方法,你去执行这个main方法,此时会自动启动一个jvm进程,它会默认就会有一个main线程,这个main线程就负责执行这个main方法的代码,进而创建各种对象。
tomcat 类都会加载到jvm里去,spring容器而言,都会对我们的类进行实例化成bean,有工作线程会来执行我们的bean实例对象里的方法和代码。进而也会创建其他的各种对象,实现业务逻辑。
基于tomcat启动:<br>1.自己写的系统war包,通过类加载器加载类放到元空间中。<br>2.Spring容器启动后会扫描代码,通过反射技术,基于类去创建Bean实例对象放到堆内存中<br>3.tomcat有很多线程,浏览器发送一个请求到线程,线程会有独享的内存空间,必然会去执行方法(堆内存中的bean实例对象里的方法)。
public void doRequest(){<br> MyService myService = new MyService();<br> myService.doService();<br>}
线程执行doRequest方法,会给doRequest创建一个栈桢 doRequest()
在方法里可能创建一些其他的对象创建在堆内存中,局部变量放在栈桢中,局部变量去堆内存引用对象。
<font color="#c41230">接下来执行doService(),压栈,doSerivce栈桢放到doRequest栈桢上面</font>。<font color="#c41230">doService执行完后会出栈,doRequest执行完也会出栈,相当于被销毁掉。</font><br>
jvm在什么情况下出发垃圾回收
新生代<br>2G
eden区<br>1.6G
S1区域<br>0.2G
S2区域<br>0.2G
默认8:1:1
垃圾回收算法:通过复制算法,来回在S1和S2之间复制
老年代<br>2G
经过15次垃圾回收也没被回收的对象会被放到老年代
如果对象的大小在S区放不下,那也直接放到老年代
在创建对象的时候,就发现特别大,那么直接放到老年代里
有一个对象自己就有100M,此时如果他是长期存活的,那么每次young gc,它都要在年轻代里反复移动。那么会很影响性能。
垃圾回收算法
老年代里的对象,很多都是被长期引用的,所以它里面的垃圾没那么多,所以标记-清理,标记没用的对象,然后清理掉
但是会有内存碎片的问题
所以最后使用的方式是标记-整理
统称为堆
垃圾回收器
parnew+cms
parnew回收年轻代
多线程进行回收
核心思想都是把存活的对象放到一个s区域,用多个线程并发的吧eden区域里的垃圾对象清理掉
cms清理老年代
分为好几个阶段
初始标记
并发标记
并发清理
年轻代在垃圾回收的时候会stop the world,导致系统一段时间不会运行。<br>老年代的垃圾回收是比较慢的,大概是年轻代的10倍以上
刚开始标记清理,清理掉一些垃圾对象。然后把一些存活的对象压缩到一起,避免内存碎片。
所以尽可能的让垃圾回收和工作线程并发的去运行
g1分代回收
jdk9以后,慢慢的主推g1垃圾回收器,以后会淘汰掉parnew+cms的组合
stop the world
停止tomcat的工作线程,然后扫面所有的对象,判断哪些可以回收,哪些不可以回收
tomcat如何设置参数的?如何检查JVM运行情况
进行压测,去观察jvm运行的情况,使用jstat工具去分析jvm运行的情况
jvm调优
OOM发生后,应该如何排查和处理线上系统的OOM问题
JMM内存模型
创建一个对象,对象是在堆内存里的,包含实际变量。<font color="#c41230">主内存</font>。
内存模型
每个线程都会对应自己的一块<font color="#c41230">工作内存</font>,对应的是cpu级别的缓存
线程2也在做这个事情
流程
read操作:先把data值从主内存读出来
load:读出来以后,加载到对应线程的工作内存中去
use:从工作内存中把data提出来,使用这个值进行运算
assign:把算好以后的值重新设置回工作内存中
store:把data值尝试往主内存写
write:把data值写入主内存中
原子性,有序性,可见性
可见性
两个线程,一个修改data值,一个读取data值
没有可见性
两个线程同时读取打他到内存中,线程1改成了data = 1,但是在线程2之前读取到线程内存里的还是data = 0;
有可见性
线程1更新完了以后,<font color="#c41230">强制要求线程2下次再读的时候,必须从主内存重新加载data值</font>。
也就是一个线程更新完后强制别的读取必须拿到最新值。
原子性
线程1执行的时候,别的线程不允许操作。
data必须是独立执行的,没人能影响,一定是执行完了之后,别人才能操作
有序性
具备有序性,就不会发生指令重排,不会导致代码出现问题
volatile关键字
主动从内存模型开始讲起
volatile是用来解决可见性和有序性,在有些罕见条件之下,可以有限的保证原子性,它主要不是用来保证原子性的。
可见性:volatile关键字,<font color="#000000">线程1从处理完数据从工作内存刷到主内存中后,</font><font color="#c41230">会让其他线程的工作内存中的data失效掉!这个时候线程use工作内存中的data发现失效了,就会重新从主内存中read load 重新拿这个data。<br></font>
在很多开源中间件系统的源码里,都大量的使用volatile
有序性
java中有一个happens-before原则
编译器,指令器可能对代码重排序,乱排,要守一定的规则,happens-before原则,只要符合happens-before原则,就不会胡乱排序。
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。比如代码里有先对一个lock.unLock(),然后再lock.lock();
volatile变量规则:<font color="#c41230">对一个volatile变量的写操作先行发生于后面的读操作。 先写再读。</font>
传递规格:操作A先发生于操作B,操作B又先行发生于操作C,则可得出A先行于C
线程启动规则:Thread对象的start()方法先行于此线程的每一个动作。thread.start()先发生
线程中断规则
线程终结规则
对象终结规则
如何基于内存屏障保证可见性和有序性
volatile不能够保证原子性,保证原子性还是得用sychronized
lock指令:volatile保证可见性
<ol><li>对volatile修饰的变量执行写操作的话,<font color="#c41230">JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存</font>,同时因为有<font color="#c41230">MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探</font>,自己本地缓存中的数据是否被人修改过,修改过的话就让它失效。</li></ol>
volatile禁止指令重拍:如果给一个变量 volatile,就会加内存屏障,避免指令重拍。
写操作前面加StoreStore屏障,禁止上面的普通写和他重排。<br>写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排。
读操作后面加LoadLoad屏障,禁止下面的普通读和volatile重排;<br>读操作后面加LoadStore屏障,禁止下面的普通写和volatile重排;
内存模型
栈
存储函数运行过程中的临时变量
堆
存对象
本地方法栈
C++方法
程序计数器
当前程序执行位置
方法区(元空间)
存储静态方法变量
类加载器ClassLoader
方法区
字符串常量池
jdk1.6 放在PermGen区中,也就是方法区中
jdk1.7 字符串常量池被移到堆中了。
类加载过程
类加载器
1.BootStrap ClassLoader 启动类加载器
加载JAVA核心类,用原生代码实现,所以没有继承自java.lang.ClassLoader
2.ExtenSion ClassLoader扩展类加载器
3.Application ClassLoader应用程序类加载器
4.自定义类加载器
双亲委派模型
两个包同时用到了一个类,但是版本不一样,如何正常使用
首先main方法的类加载器是AppClassLoader
Collect
Get Started
Collect
Get Started
Collect
Get Started
Collect
Get Started
评论
0 条评论
下一页