JVM
第一章 JVM的概念
一. JVM简介
-
虚拟机发展史
JVM将字节码解释成不同os下的机器指令,有了jvm,java语言在不同平台上运行时不需要重新编译
(1)Sun Classic
classic jvm要么采用纯解释器解释执行,要么采用JIT编译执行,一旦JIT进行编译执行,则解释器不再生效
如果使用JIT编译代码,则JIT会对每个方法,每行代码都进行编译,对于那种只需运行一次,不具有编译价值的代码,也会被JIT编译执行。迫于程序响应时间的压力,此阶段的JIT不敢采用编译耗时的优化技术,所以及时采用JIT输出本地代码,他的执行效率也和C代码有很大差距。被人诟病“java语言很慢”
(2)Exact VM
Exact VM进入了高性能虚拟机时代,开始使用编译器解释器混合模式
Exact VM使用了精确式内存管理,他能判定内存中摸个位置的数据具体是什么类型。eg:内存中有一个数是12345,ExactVM可以判断他是一个int数字,还是一个指向内存12345地址的引用。这种精确式内存管理,在gc时,可以直接回收引用类型的数据,少了像classic vm时代gc时的数据类型查找过程
(3)HotSpot虚拟机
Hot Spot是当前jdk采用的虚拟机 (1.4以后)
热点代码探测技术:通过计数器找出最具编译价值的代码,通知JIT以方法为单位进行编译。如果方法被频繁调用,则触发标准编译;如果方法中循环次数很多,触发栈上替换编译动作。
HotSpot无需等待本地代码输出后才能执行程序,使得即时编译压力减小,有助于采用更更多的代码优化技术。输出高质量的操作系统本地代码 -
JVM运行时数据区域
线程隔离的数据区
(1)程序计数器:
字节码的行号指示器。解释器根据程序计数器选取要执行下条指令。分支,循环,跳转,异常处理,线程恢复等基础功能都需要程序计数器来完成。
为了线程切换后能继续执行正确的代码,每条线程都要维护自己的程序计数器,各个线程间的程序计数器互不影响
如果线程执行的是native方法,则该线程的程序计数器值为空
(2)虚拟机栈:
虚拟机栈用来执行java方法
每个java方法执行的时候都会创建一个栈帧。栈帧用来存储局部变量表,操作数栈,动态链接,方法出口等信息。
(3)本地方法栈
本地方法栈用来执行native方法
线程共享数据区
(1)java堆
java堆存储对象实例
java堆分为新生代和老年代。新生代分为eden,from survive,to survive空间
(2)静态区
静态区也叫做永久带,存放类信息,常量池,静态变量,JIT编译后的代码等。
常量池技术:为了方便创建一个对象二出现的。当需要创建一个对象时,如果常量池中有该对象,则直接从池子中取出来,在重复创建相等变量时节省了很多时间。eg:str1=“abc”,str2=“abc”。这样直接声明的字符串会放到常量池中,所以str1和str2是静态区常量池中的同一块内存地址
二. JVM的4中引用
- 强引用:new出来的对象。其他引用方式都是在强引用失效后才有用
- 软引用
(1)一个对象被强引用new出来以后,在jvm中,可以存在软引用
(2)对象的强引用失效后,如果对象还存在软引用,则引用的对象在gc时不会被垃圾回收。只有当jvm发生内存溢出前,软引用引用的对象才被回收。因此,软引用可以被当做缓存Person p = new Person(); SoftReference<Person> softReference = new SoftReference<>(p); p = null; // 断开强引用 System.gc(); System.out.println(softReference.get().getName()); // 输出aaa
- 弱引用
触发gc时,就会被当做垃圾回收。弱引用的生命周期是强引用失效后,到gc触发时Person p = new Person(); WeakReference<Person> reference = new WeakReference<>(p); p = null; // 断开强引用 System.out.println(reference.get().getName()); // 输出aaa System.gc(); System.out.println(reference.get().getName()); // 抛出nullexception
- 幽灵引用
幽灵引用和没有引用一样,对象的生命周期和只有强引用时一样。只是在对象被回收时会收到一个系统通知Person p = new Person(); ReferenceQueue<Person> referenceQueue = new ReferenceQueue<>(); PhantomReference<Person> reference = new PhantomReference<>(p,referenceQueue); p = null; // 断开强引用 try{ System.out.println(reference.get().getName()); // 抛出nullpointer Exception }catch (Exception e){e.printStackTrace();} System.gc(); try{ System.out.println(reference.get().getName()); // 抛出nullpointer Exception }catch (Exception e){e.printStackTrace();}
- 关于引用的一些说明
(1)软引用和弱引用都延长了对象引用的生命周期,一个延长至oom发生,一个延长至gc发生时。幽灵引用只是提供对象gc的通知
(2)ThreadLocal使每个线程保存各自的变量副本,值得注意的是,这个变量要在每个线程内部初始化,因为ThreadLocal不会生成变量副本,如果在线程外初始化变量,仍然有线程安全问题
第二章 垃圾回收器
一. 判断对象已死的方法
-
引用计数法
(1)思路:给对象添加一个计数器,每当一个地方引用它,计数器就+1,当一个引用失效,计数器-1. 计数器值为0的对象,被判定为已死对象
(2)缺点:引用计数器无法处理互相引用的问题。当两个对象互相引用时,即使这两个对象被置为null,但是由于互相引用着对方,导致各自的引用计数器不为0,使得这2个对象无法被标记未死亡对象 -
可达性分析
(1)思路:通过一系列称为“GC ROOTs”的对象作为起点,从这些节点开始向下搜索,所走过的路径称作引用链。如果一个对象和GCRoots之间没有任何的引用链,则对象被看做已死对象
(2)可当做GCRoots的对象
1. java栈中引用的对象(栈帧的本地变量表)
2. 静态区中静态属性引用的对象
3. 静态区中常量引用的对象
4. native栈中引用的对象 -
Java采用可达性分析来区分对象到底是否死亡
二. 判断对象死亡的过程(2次标记)
-
不可达的对象并不是一定会被gc掉的。当对象被视作为不可达后,jvm对该对象进行第一次标记,如果jvm发现该对象的finalize()方法从未被调用过,则jvm会把对象放到F-Queue中,稍后,jvm会建立一个优先级很低的Finalizer线程执行该方法。如果对象在finalize()方法中把this与引用链上的任何一个对象关联,则他会被移出“即将回收集合”,没被移出集合的对象备进行二次标记,经过两次标记的对象在gc时会被垃圾回收
-
对象的finalize方法被一个优先级很低的线程执行,如果在该线程还未执行finalize方法前,就被gc垃圾回收,则该对象的finalize方法并不会产生任何效果。所以覆盖finalize方法拯救对象的方式,并不一定每次有效
三. GC策略
-
标记清除算法
2次标记后jvm标记了需要gc的对象,标记清除算法直接清空这些对象的内存地址。这样做带来的弊端就是,内存中产生大量不连续的空间,当需要产生大对象时,有雨没有足够的连续内存导致新一轮的gc产生 -
复制算法
(1)将内存分为等大小的2块,一块内存用完,就把存活的对象放到另一块内存中,然后清空该块内存。这种方法不会产生碎片,但是实际可用内存减少到原来的一半
(2)JVM采用复制算法回收新生代的对象,但jvm没有将新生代内存等分,而是划分为一个大的eden区和2个小的survive区。HotSpot中的比例时8:1:1,新生代中的内存浪费10%。每次gc时都会拷贝eden和from survive中的存活对象到to survive中,然后清空这两块内存。如果to survive空间不够用,则这些对象就进入老年代。这就是老年代的分配担保
-
标记整理
复制算法适合gc新生代,因为新生代的对象大部分不会存活,如果采用复制算法gc对象存活率很高的老年代,会使得复制操作开销很多。因此,老年代采用标记整理算法进行gc。标记整理算法在标记清除之后,把对象向内存的一端移动,然后清理掉边界以外的内存。
四. 内存分配策略
-
对象优先在新生代的eden区分配
对象先尝试在eden区生成,当eden区剩余空间不足以产生新的对象时,触发minorgc:eden和from survive的存活对像进入to survive中。如果to survive没有足够容量,则存放不下的对象放到老年代中,清空eden和from survive区域。 -
大对象直接在年老代产生
本来对象要在eden产生,但是大对象会在minorgc时产生大量的复制开销,不利于gc,因此jvm把大对象直接在年老代产生。以通过参数-XX:PretenureSizeThreshold设置进入年老代的对象大小上限。(该参数只对Serial和ParNew收集器有效,Paral ScanVege收集器不认识这个参数) -
长期存活对象进入年老代
(1)每次存活对象经历一次minorgc后,对象年龄就+1,默认经过15岁后,对象进入老年代,可以通过参数-XX:MaxTenuringThreshold设置。
(2)jvm在年龄判断时进行了优化,如果from survive
中相同年龄的对象总和大于from survive
总大小的一半,则大于等与该年龄的对象直接进入老年代,而不用等到年龄上线到达以后在进入老年代,节省了minorgc的复制开销 -
老年代的空间担保
(1)minorgc后,如果to survive无法存放eden和from survive的存活对像,则这些对象尝试进入老年代,如果老年代的剩余空间也无法存放这些对象,则会查看HandlePromotionFailure参数是否设置true。如果设置true,证明允许担保失败,接着如果发现,老年代的最大可用连续空间>历次进入老年代的对象平均大小
,则冒险进行一次minorgc;如果发现老年代的最大可用连续空间<历次进入老年代的对象平均大小
,或者不允许担保失败,则进行一次full gc(full gc触发stop the world,堆新生代,老年代,永久带全部进行一次gc)。若果还是无法分配到连续内存,则抛出oom
五. 垃圾回收器的实现
-
Serial单线程收集器
(1)Serial采用1个线程回收垃圾对象,Serial的gc触发时产生stop the world
(2)Serial回收器采用复制算法收集新生代,采用标记整理算法收集老年代
(3)Serial收集器时Client模式下的默认收集器,简单高效。虽然触发stop the world,但是回收一两百m的对象大概耗时100毫秒
(4)SerialOld是对应的老年代收集器 -
ParNew新生代收集器
(1)多线程版的Serial收集器,其他特点和Serial一模一样,例如stop the world
(2)多核心cpu下ParNew效果比Serial好,单核cpu下Serial比ParNew好
(3)只有Serial和ParNew新生代回收器可以和CMS老年代回收器组合使用
(4)parNew是server模式下的最佳新生代收集器
(5)ParOld是对应的老年代收集器 -
Parall Scavenge新生代收集器
(1)Parall Scavenge收集器侧重于jvm的吞吐量(吞吐量=\(\frac{用户代码运行时间}{用户代码运行时间+垃圾回收线程执行时间}\))
(2)Parall Scavenge可以自适应调节回收参数,需要把基本的内存数据(堆最大,最小量)设置好,然后设置更关注最大停顿时间或者更关注吞吐量,收集器会把细节参数自动调节。 -
CMS(concurrent mark sweep)并发老年代收集器
(1)基于标记清除算法回收老年代
1. 初始标记:仅标记GCRoots能直接关联到的对象,速度很快,但是“stop the world”
2. 并发标记:GCRoot Tracing,标记GCRoots的二级三级关联对象。4步中耗时最长但是和用户线程并发执行
3. 重新标记:修正并发标记时用户线程到只得标记变动,“stop world”且停顿稍长
4. 并发清除:耗时长,和用户线程同步执行
(2)CMS收集器的缺点
1. CMS收集器虽然是并发执行,但会降低用户线程的响应速度
2. 浮动垃圾无法清除:cms回收线程和用户线程并发执行,用户线程还会产生新的垃圾对象,旧垃圾对象还未清除完毕,新垃圾对象的到来导致新一次的gc,这些旧垃圾对象称为浮动垃圾。通过-XX+CMSInitiatingOccupancyFraction=value来设置老年代空间沾满百分之多少后就触发majorgc。如果该百分比设置的太高,cms预留的老年代空间无法承载新到来得老年代对象,则发生“Concurrent Mode Faliure”,jvm临时采用Serial old回收器回收,产生stop the world效果
3. cms采用标记清除算法回收对象,会在老年代产生内存碎片,分配大对象时找不到连续空间而触发full gc。通过设置XX+UseCMSCompactAtFullCollection(默认为开启) ,让cms触发fullgc时进行内存碎片整理,但使得停顿时间增大 -
g1收集器
g1收集器用来替代cms收集器
(1)并行与并发:利用多cpu缩短stop-the-world的时间,使用并发方式解决其它收集器需要停顿的gc动作。
(2)分代收集:新老代收集区分对待。
(3)空间整合:G1从整理看是基于标记-整理,但是局部看是基于复制算法实现的,不会产生碎片。
(4)可预测的停顿:能够让使用者指定在M毫秒的时间片段上,消耗在垃圾回收的时间不得超过N毫秒。过程:初始标记、并发标记、最终标记、筛选回放。前三个和CMS一致,筛选回放是根据用户设置的停顿目标来选择回收价值最高的进行回收。
六. fullgc频繁触发的原因
-
永久带空间沾满
解决方法:增大永久带,或选择cms收集器 -
CMS触发了promotion failed和concurrent mode failure
解决方法:增大老年代或减小cms进行majorgc时的百分比JAVA_OPTS="$JAVA_OPTS -Xms2g -Xmx2g -Xmn768m -Xss256K -XX:PermSize=256m -XX:MaxPermSize=256m -XX:SurvivorRatio=2" JAVA_OPTS="$JAVA_OPTS -XX:+DisableExplicitGC -XX:+UseParNewGC" JAVA_OPTS="$JAVA_OPTS -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+CMSClassUnloadingEnabled -XX:+UseCMSInitiatingOccupancyOnly" JAVA_OPTS="$JAVA_OPTS -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:CMSInitiatingOccupancyFraction=55" JAVA_OPTS="$JAVA_OPTS -XX:SoftRefLRUPolicyMSPerMB=0 -XX:LargePageSizeInBytes=128M" JAVA_OPTS="$JAVA_OPTS -XX:+UseFastAccessorMethods -XX:-OmitStackTraceInFastThrow -XX:-UseGCOverheadLimit" JAVA_OPTS="$JAVA_OPTS -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintAdaptiveSizePolicy -Xloggc:$ROOT/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$ROOT"
第三章 监控工具
一. 使用jmx让远程工具连接java进程:开启jmx:
- 无需验证的配置:
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.port=1234
-Dcom.sun.management.jmxremote.ssl=false - 将-Dcom.sun.management.jmxremote.authenticate=false 去掉就是需要验证信息
即:登录名和密码:验证用户的配置文件在$java_homejre/lib/management/jmxremote.password
默认有一个$java_home/jre/lib/management/jmxremote.password.template
修改下即可。
jmxremote.password.template默认是只读权限。
并更改为可写的权限 chmod a+w jmxremote.password
修改后把jmxremote.password的读权限取消 a-r 。否则会提示:Error: Password file read access must be restricted:
management/jmxremote.access配置下权限。
连接时指定ip:端口和jmx连接即可。
远程的线程dump可以dump查看,堆的dump是需要从远程拉到本地来查看。