JVM
2021-06-29 12:01:20 72 举报
AI智能生成
JVM知识点大全
作者其他创作
大纲/内容
JVM基础
GC垃圾回收
引用计数器
古老的确认垃圾方式,但是无法确认循环引用
根可达算法GCRoots
来源于jvms,java虚拟机规范
从根上找引用
比如main方法里面一定会有一些引用,Object a = new Object(); String b = new String();
a和b就是根。
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
1.8默认
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用的是标记清除算法,碎片化的问题会越来越严重
在碎片化特别严重的时候,居然用的是Serial Old进行整理,STW会非常严重
G1
逻辑分代,物理不分代
把内存分成一个个的Region(区域)。Region个数默认2000个
大小在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的记录。
存储哪些Region引用了自己
存储哪些Region引用了自己
Cset Collection Set
本次GC需要清理的Region集合
垃圾收集流程
YoungGC
复制算法
MixGC--OldGC
1.初次标记
标记GCRoot直接引用的对象,标记GCRoot所在的Region(Root Region)
2.RootRegion拿过来,去扫描整个Old区的所有Region,去看所有Old Region的Rset中是否含有RootRegion,如果有,就标识出来。
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配上失败重试的方式保证更新操作的原子性),而在竞争激烈的场合分配的效率又会进一步下降。JVM使用TLAB来避免多线程冲突,在给对象分配内存时,每个线程使用自己的TLAB,这样可以避免线程同步,提高了对象分配的效率。
TLAB本身占用eEden区空间,在开启TLAB的情况下,虚拟机会为每个Java线程分配一块TLAB空间。参数-XX:+UseTLAB开启TLAB,默认是开启的。TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,当然可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
多个线程会去Eden抢存放对象的位置。所以会有一个线程同步的问题。
因此HotSpot会有一个优化,在Eden为每个线程分配一小块区域。线程先往TLAB尝试放进去,如果大小放不进去才会放到Eden其他位置
因此HotSpot会有一个优化,在Eden为每个线程分配一小块区域。线程先往TLAB尝试放进去,如果大小放不进去才会放到Eden其他位置
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 -XX:+UseParallelGC
java -XX:PrintFlagsFinal 打印出来所有的参数
调优基础概念
方向
1.根据需求进行JVM规划和预调优
2.优化运行JVM运行环境(慢,卡顿)
3.解决JVM运行过程中出现的各种问题(OOM)
java -Xms30M -Xmx30M -XX:+PrintGC com.xxx
top命令查看
jps java的进程号
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 -XX:+HeapDumpOnOutOfMemoryError 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 查看有哪些线程在运行
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
或者每天产生一个日志文件
-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log 设置日志文件名
-XX:+UseGCLogFileRotation 滚动日志 如果最后一个日志满了覆盖写第一个
-XX:NumberOfGCLogFiles=5 文件个数
-XX:GCLogFileSize=20M 每个文件大小
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCCause
-XX:+UseGCLogFileRotation 滚动日志 如果最后一个日志满了覆盖写第一个
-XX:NumberOfGCLogFiles=5 文件个数
-XX:GCLogFileSize=20M 每个文件大小
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCCause
子主题
通过GClog 查看gc情况
jmap查看进程里生成的对象
jmap -histo 1659 |head 20
生成对象的个数,大小
内存泄漏与内存溢出
Memory leak 内存泄漏
内存里有个对象,没什么用也不释放
out of Memory 内存溢出
内存泄漏的对象多了,就容易产生内存溢出
OOM如何进行定位的
用了arthas,然后用了jmap命令
JVM
待细学
待细学
jvm运行原理
堆内存:执行方法的时候,方法里针对的对象放在堆内存。
栈内存:执行代码的时候,肯定会有很多的线程,tomcat里就有很多自己的工作线程,去执行我们写的代码,每个工作线程都会有自己的一块数据结构,栈内存。线程独有。
永久代:代码要被执行,类加载到永久代
java8以后内存分代的改进
jdk1.7:永久代里放了一些常量池+类信息 ---jdk8:常量池放到堆里面,类信息放到metaspace元空间里。
jvm如何运行起来的
一定有线程去执行我们写的代码
比如说我们有一个类里面包含了一个main方法,你去执行这个main方法,此时会自动启动一个jvm进程,它会默认就会有一个main线程,这个main线程就负责执行这个main方法的代码,进而创建各种对象。
tomcat 类都会加载到jvm里去,spring容器而言,都会对我们的类进行实例化成bean,有工作线程会来执行我们的bean实例对象里的方法和代码。进而也会创建其他的各种对象,实现业务逻辑。
基于tomcat启动:
1.自己写的系统war包,通过类加载器加载类放到元空间中。
2.Spring容器启动后会扫描代码,通过反射技术,基于类去创建Bean实例对象放到堆内存中
3.tomcat有很多线程,浏览器发送一个请求到线程,线程会有独享的内存空间,必然会去执行方法(堆内存中的bean实例对象里的方法)。
1.自己写的系统war包,通过类加载器加载类放到元空间中。
2.Spring容器启动后会扫描代码,通过反射技术,基于类去创建Bean实例对象放到堆内存中
3.tomcat有很多线程,浏览器发送一个请求到线程,线程会有独享的内存空间,必然会去执行方法(堆内存中的bean实例对象里的方法)。
public void doRequest(){
MyService myService = new MyService();
myService.doService();
}
MyService myService = new MyService();
myService.doService();
}
线程执行doRequest方法,会给doRequest创建一个栈桢 doRequest()
在方法里可能创建一些其他的对象创建在堆内存中,局部变量放在栈桢中,局部变量去堆内存引用对象。
接下来执行doService(),压栈,doSerivce栈桢放到doRequest栈桢上面。doService执行完后会出栈,doRequest执行完也会出栈,相当于被销毁掉。
jvm在什么情况下出发垃圾回收
新生代
2G
2G
eden区
1.6G
1.6G
S1区域
0.2G
0.2G
S2区域
0.2G
0.2G
默认8:1:1
垃圾回收算法:通过复制算法,来回在S1和S2之间复制
老年代
2G
2G
经过15次垃圾回收也没被回收的对象会被放到老年代
如果对象的大小在S区放不下,那也直接放到老年代
在创建对象的时候,就发现特别大,那么直接放到老年代里
有一个对象自己就有100M,此时如果他是长期存活的,那么每次young gc,它都要在年轻代里反复移动。那么会很影响性能。
垃圾回收算法
老年代里的对象,很多都是被长期引用的,所以它里面的垃圾没那么多,所以标记-清理,标记没用的对象,然后清理掉
但是会有内存碎片的问题
所以最后使用的方式是标记-整理
统称为堆
垃圾回收器
parnew+cms
parnew回收年轻代
多线程进行回收
核心思想都是把存活的对象放到一个s区域,用多个线程并发的吧eden区域里的垃圾对象清理掉
cms清理老年代
分为好几个阶段
初始标记
并发标记
并发清理
年轻代在垃圾回收的时候会stop the world,导致系统一段时间不会运行。
老年代的垃圾回收是比较慢的,大概是年轻代的10倍以上
老年代的垃圾回收是比较慢的,大概是年轻代的10倍以上
刚开始标记清理,清理掉一些垃圾对象。然后把一些存活的对象压缩到一起,避免内存碎片。
所以尽可能的让垃圾回收和工作线程并发的去运行
g1分代回收
jdk9以后,慢慢的主推g1垃圾回收器,以后会淘汰掉parnew+cms的组合
stop the world
停止tomcat的工作线程,然后扫面所有的对象,判断哪些可以回收,哪些不可以回收
tomcat如何设置参数的?如何检查JVM运行情况
进行压测,去观察jvm运行的情况,使用jstat工具去分析jvm运行的情况
jvm调优
OOM发生后,应该如何排查和处理线上系统的OOM问题
JMM内存模型
创建一个对象,对象是在堆内存里的,包含实际变量。主内存。
内存模型
每个线程都会对应自己的一块工作内存,对应的是cpu级别的缓存
线程2也在做这个事情
流程
read操作:先把data值从主内存读出来
load:读出来以后,加载到对应线程的工作内存中去
use:从工作内存中把data提出来,使用这个值进行运算
assign:把算好以后的值重新设置回工作内存中
store:把data值尝试往主内存写
write:把data值写入主内存中
原子性,有序性,可见性
可见性
两个线程,一个修改data值,一个读取data值
没有可见性
两个线程同时读取打他到内存中,线程1改成了data = 1,但是在线程2之前读取到线程内存里的还是data = 0;
有可见性
线程1更新完了以后,强制要求线程2下次再读的时候,必须从主内存重新加载data值。
也就是一个线程更新完后强制别的读取必须拿到最新值。
原子性
线程1执行的时候,别的线程不允许操作。
data必须是独立执行的,没人能影响,一定是执行完了之后,别人才能操作
有序性
具备有序性,就不会发生指令重排,不会导致代码出现问题
volatile关键字
主动从内存模型开始讲起
volatile是用来解决可见性和有序性,在有些罕见条件之下,可以有限的保证原子性,它主要不是用来保证原子性的。
可见性:volatile关键字,线程1从处理完数据从工作内存刷到主内存中后,会让其他线程的工作内存中的data失效掉!这个时候线程use工作内存中的data发现失效了,就会重新从主内存中read load 重新拿这个data。
在很多开源中间件系统的源码里,都大量的使用volatile
有序性
java中有一个happens-before原则
编译器,指令器可能对代码重排序,乱排,要守一定的规则,happens-before原则,只要符合happens-before原则,就不会胡乱排序。
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。比如代码里有先对一个lock.unLock(),然后再lock.lock();
volatile变量规则:对一个volatile变量的写操作先行发生于后面的读操作。 先写再读。
传递规格:操作A先发生于操作B,操作B又先行发生于操作C,则可得出A先行于C
线程启动规则:Thread对象的start()方法先行于此线程的每一个动作。thread.start()先发生
线程中断规则
线程终结规则
对象终结规则
如何基于内存屏障保证可见性和有序性
volatile不能够保证原子性,保证原子性还是得用sychronized
lock指令:volatile保证可见性
- 对volatile修饰的变量执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被人修改过,修改过的话就让它失效。
volatile禁止指令重拍:如果给一个变量 volatile,就会加内存屏障,避免指令重拍。
写操作前面加StoreStore屏障,禁止上面的普通写和他重排。
写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排。
写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排。
读操作后面加LoadLoad屏障,禁止下面的普通读和volatile重排;
读操作后面加LoadStore屏障,禁止下面的普通写和volatile重排;
读操作后面加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
0 条评论
下一页