JVM初识

1 篇文章 0 订阅

注:本内容是看了石杉老师的jvm实战后总结的

1. JVM类加载

  1. 类的加载过程:加载-验证-准备-解析-初始化-使用-卸载
    (1)验证阶段:校验class文件是否符合JVM规范,是否被篡改。
    (2)准备阶段:给类分配一定的内存空间,给类变量(static修饰的变量)分配内存空间,设置初始值。
    (3)解析阶段:符号引用修改为直接引用。
    (4)初始化:给类变量(static变量)进行真正的赋值,还有执行static静态代码块。需要初始化的类,发现他的父类还没初始化,会先初始化他的父类
  2. JVM在什么情况下会加载一个类?
    JVM启动后先加载main方法所在的类,执行main方法时用到的类就会被加载进来。
  3. 类加载器和双亲委派机制
    (1)启动类加载器Bootstrap ClassLoader,主要负责加载我们在机器上安装的java目录下的核心类,就是安装jdk目录下的java/lib目录。
    (2)拓展类加载器Extension ClassLoader,Java安装目录下,有一个“lib\ext”目录这里面有一些类。
    (3)应用程序类加载器Application ClassLoader,这类加载器就负责去加载“ClassPath”环境变量所指定的路径中的类
    (4)自定义类加载器
    (5)双亲委派机制:应用程序类加载器需要加载一个类,他首先会委派给自己的父类加载器去加载,最终传导到顶层的类加载器去加载但是如果父类加载器在自己负责加载的范围内,没找到这个类,那么就会下推加载权利给自己的子类加载器。作用是避免多层级的加载器机构重复加载某些类。
  4. 如何对“.class”文件处理保证不被人拿到以后反编译获取公司源代码?
    使用工具对字节码加密,或者做混淆处理,在类加载的时候,可以自定义类加载器来解密文件,保证源代码不被人窃取。
  5. Tomcat的类加载机制
    Tomcat是打破了双亲委派机制的,每个WebApp负责加载自己对应的那个Web应用的class文件,也就是我们写好的某个系统打包好的war包中的所有class文件,不会传导给上层类加载器去加载。

2.JVM内存模型

  1. 元数据空间(1.8之前叫方法区)
    存放类加载进来的class字节码。
  2. 程序计数器
    线程私有,记录当前线程执行的字节码指令的位置。
  3. java虚拟机栈
    线程私有,存放线程执行方法的栈帧,每调用一个方法,就生成一个对应的栈帧入栈,栈帧里存放着局部变量。
  4. java堆内存
    线程共享,存放创建的对象。栈帧里的局部变量可以通过存放对象地址来指向堆中的对象。
  5. 本地方法栈
    线程私有,存放各种native方法的局部变量表之类的信息。

3.垃圾回收

  1. 方法区内会不会进行垃圾回收?
    会进行垃圾回收。首先该类的所有实例对象都已经从Java堆内存里被回收,其次加载这个类的ClassLoader已经被回收,最后,对该类的Class对象没有任何引用。

  2. JVM内存的核心参数
    (1)-Xms:Java堆内存的大小
    (2)-Xmx:Java堆内存的最大大小
    (3)-Xmn:Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了
    (4)-XX:PermSize:永久代大小
    (5)-XX:MaxPermSize:永久代最大大小
    (6)-Xss:每个线程的栈内存大小
    JDK 1.8以后的版本,永久代俩参数被替换为了-XX:MetaspaceSize和-XX:MaxMetaspaceSize

  3. 如何评估一个系统的JVM堆内存设置大小?
    (1)假如一个核心业务处理需要1秒,单机情况下每秒并发处理30个
    (2)计算处理一个核心业务所需要创建对象的大小,int占4个字节,Long 8个字节,可能这个对象包含20字段,大概就计算500个字节,500字节*30=15000字节,15kb。
    (3)除了核心业务,还包括其他业务,将计算的扩大10-20倍,大概就算每秒产生1mb的新生代对象,1秒后就变成了垃圾。
    (4)假如我们机器是2核4g,则除去机器本身需要占用内存,则JVM只能分到2G左右,还得出去永久代,栈内存,老年代,则最后分给新生代只有几百MB。假设就500MB,500MB还得分交换区,剩下可用就400MB。
    (5)现在是每秒产生1MB,则进行7分钟左右就得进行Minor GC。
    (6)如果是4核8G得机器,可以分给新生代得空间就可以1G左右了,这样大大降低了GC得频率

  4. 垃圾回收是怎么判断哪些对象可以回收的?
    (1)可达性分心算法,查看谁引用它,看最后是否有一个GC Roots
    (2)局部变量,静态变量可以看作是一种GC Roots,只要被他们引用的对象不可以回收。
    (3)如果是软引用对象,即使被GC Roots引用着,在垃圾回收时,如果内存实在不够的时候,还是被回收的。
    (4)没有GC Roots的引用,但如果重写了Object的finialize()方法,finialize()方法中重新赋值给某个GC Roots变量,这个垃圾回收时,是会先调用finialize()方法,这样使这个对象有重新被GC Roots引用了,就不会被回收。

  5. 引用类型
    (1)强引用
    (2)软引用:正常情况下垃圾回收是不会回收软引用对象的,但是如果你进行垃圾回收之后,发现内存空间还是不够存放新的对象,内存都快溢出了此时就会把这些软引用对象给回收掉,哪怕他被变量引用了,但是因为他是软引用,所以还是要回收。
    (3)弱引用
    (4)虚引用

  6. 新生代空间为什么这么设计,Eden区和Survivor区,比例8:1:1?
    (1)大部分对象的存活时间都是很短的,所以垃圾回收后,只有一小部分存活,将其分配到Survivor区,这样还有90%的空间可以使用,可以提供新生代空间的利用率。

  7. 对象如何进入老年代的?
    (1)躲过15次GC后进入老年代,可以通过JVM参数“-XX:MaxTenuringThreshold”来设置,默认是15岁
    (2)动态对象年龄判断:假如说当前放对象的Survivor区域里,一批对象的总大小大于了这块Survivor区域的内存大小的 XX:SurvivorRatio=50%,那么此时大于等于这批对象年龄的对象,就可以直接进入老年代了。
    (3)大对象直接进入老年代:-XX:PretenureSizeThreshold设置对象的字节数,大于这个大小的对象直接进入老年代,不经过新生代。这样可以避免在survivor区复制来复制去。
    (4)Minor GC后的对象太多,无法放入Survivor区,直接移入老年代。(此时部分对象会进入Survivor区,部分进入老年代)
    (5)老年代空间担保规则。
    在这里插入图片描述

  8. 触发full GC的时机
    (1)Minor GC之后,需要放入老年代,老年代放不下,需要Full GC
    (2)通过参数检测后,发现需要提前进行Full GC,然后再进行Minor GC

4.频繁full GC场景及调优

  1. 场景:一个数据处理系统,每分钟执行100次计算,每次计算执行10秒钟,需要查询1万条数据,每条数据1kb,大概计算一次就需要10MB
  2. JVM配置:4核8G的机器,分给JVM4G,堆内存3G,新生代老年代各1.5G。新生代的Eden和survivor比例:8:1:1,所以Eden 1.3G,survivor区占100MB
  3. 大概运行80秒左右,Edge区就满了,需要进行Minor GC,此时会判断老年代是否大于Edge,大于则进行Minor GC,发现会有大概200MB的存活对象,大于Survivor区,则进入老年代。
  4. 每执行80秒左右,就会有200MB进入老年代。大概10分钟左右,就会进行一次Full GC。Full GC的熟读是很慢的,性能很差。
  5. 产生原因:没有利用到Survivor区,使存活时间短的对象进入了老年代。
  6. 解决方式:重新分配内存,新生代3G,老年代1G,调大Survivor区,200MB,这样每次Minor GC时候就能进入Survivor区。而且调整动态年龄判断的大小XX:SurvivorRatio=80%,减少直接进入老年代的数据

5.新生代垃圾回收算法和回收器

  1. 回收算法:复制算法
  2. 回收器:
    (1)Serial:单线程回收,无法利用多核CPU
    (2)ParNew:多线程回收,默认使用跟CPU核数一样的线程数。通过“-XX:+UseParNewGC”配置

6.使用ParNew还是Serial回收器好?

  • 作为服务器模式,一般部署到多核的linux服务器上,所以一般采用ParNew
  • 作为客户端模式,比如win上面的一些客户端软件,如果部署到单核CPU的服务器上,使用Serial单线程可以减少线程切换带来的消耗。

7.CMS垃圾回收器

  1. 采用标记清理算法,一般用于老年代
  2. CMS的工作流程:
    (1)初始标记:标记出来所有GC Roots直接引用的对象,进入“Stop the World”状态。他的速度很快,仅仅标记GC Roots直接引用的那些对象。
    (2)并发标记:是对老年代所有对象进行GC Roots追踪,其实是最耗时的。跟系统程序并发运行的,所以其实这个阶段不会对系统运行造成影响的。
    (3)重新标记:以第二阶段结束之后,绝对会有很多存活对象和垃圾对象,是之前第二阶段没标记出来的,所以此时进入第三阶段,要继续让系统程序停下来,再次进入“Stop the World”阶段。重新标记的阶段,是速度很快的,他其实就是对在第二阶段中被系统程序运行变动过的少数对象进行标记,所以运行速度很快。
    (4)并发清理:这个阶段其实是很耗时的,因为需要进行对象的清理,但是他也是跟系统程序并发运行的,所以其实也不影响系统程序的执行
  3. 并发标记和并发清理时候存在的问题:消耗CPU资源,垃圾回收线程默认线程数是(CPU核数+3)/4
  4. Concurrent Mode Failure问题
    (1)在并发清理的时候,也会有新的对象通过Minor GC进入老年代,这些对象要等下次Full GC的时候才能进行回收。
    (2)CMS垃圾回收的触发时机,有一个就是当老年代内存占用达到一定比例了,就自动执行GC。“-XX:CMSInitiatingOccupancyFaction”参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK 1.6里面默认的值是92%。预留剩下的8%空间给新对象进入老年代。
    (3)如果CMS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间,会发生Concurrent Mode Failure,就是说并发垃圾回收失败。此时就会自动用“Serial Old”垃圾回收器替代CMS,就是直接强行把系统程序“Stop the World”,重新进行长时间的GC Roots追踪,标记出来全部垃圾对象,不允许新的对象产生。
  5. 内存碎片问题:
    (1)CMS有一个参数是“-XX:+UseCMSCompactAtFullCollection”,默认就打开了,意思是在Full GC之后要再次进行“Stop the World”,停止工作线程,然后进行碎片整理,就是把存活对象挪到一起,空出来大片连续内存空间,避免内存碎片。
    (2)还有一个参数是“-XX:CMSFullGCsBeforeCompaction”,这个意思是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认是0,意思就是每次Full GC之后都会进行一次内存整理。

8.为啥老年代的Full GC要比新生代的Minor GC慢很多倍,一般在10倍以上?

  1. 新生代执行速度其实很快,因为直接从GC Roots出发就追踪哪些对象是活的就行了,新生代存活对象是很少的,这个速度是极快的,不需要追踪多少对象。
  2. 老年代垃圾回收慢的问题:
    (1)老年代在并发标记阶段,他需要去追踪所有存活对象,老年代存活对象很多,这个过程就会很慢;
    (2)其次并发清理阶段,他不是一次性回收一大片内存,而是找到零零散散在各个地方的垃圾对象,速度也很慢;
    (3)最后完事儿了,还得执行一次内存碎片整理,把大量的存活对象给挪在一起,空出来连续内存空间,这个过程还得“Stop the World”,那就更慢了。
    (4)万一并发清理期间,剩余内存空间不足以存放要进入老年代的对象了,引发了“Concurrent Mode Failure”问题,那更是麻烦,还得立马用“Serial Old”垃圾回收器,“Stop the World”之后慢慢重新来一遍回收的过程,这更是耗时了。

9.几个触发老年代GC的时机

  1. 老年代可用内存小于新生代全部对象的大小,如果没开启空间担保参数,会直接触发Full GC,所以一般空间担保参数都会打开;
  2. 老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC;
  3. 新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时老年代内存不足。
  4. -XX:CMSInitiatingOccupancyFaction”参数,刨除掉上述几种情况,如果老年代可用内存大于历次新生代GC后进入老年代的对象平均大小,但是老年代已经使用的内存空间超过了这个参数指定的比例,也会自动触发Full GC。

10.G1垃圾回收器

  1. 可以设定预期系统停顿时间
  2. 可以使用“-XX:+UseG1GC”来指定使用G1垃圾回收器,Region的数量是自动计算的,最多2048个,而且region的大小必须是2的倍数。比如说1MB、2MB、4MB之类的。如果通过手动方式来指定,则是“-XX:G1HeapRegionSize”
  3. 默认新生代对堆内存的占比是5%,可以通过“-XX:G1NewSizePercent”来设置新生代初始占比的;在系统运行中,JVM其实会不停的给新生代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”。而且一旦Region进行了垃圾回收,此时新生代的Region数量还会减少,这些其实都是动态的。
  4. 新生代里还是有Eden和Survivor的划分,“-XX:SurvivorRatio=8”设置eden和survivor区的比例。
  5. 当新生代数量达到“-XX:G1MaxNewSizePercent”时,就会触发GC回收,根据过“-XX:MaxGCPauseMills”参数设置的停顿时间,G1会尽可能多的回收一些对象。
  6. G1回收时,对象什么时候从新生代进入老年代
    (1)对象在新生代躲过了很多次的垃圾回收,达到了一定的年龄了,“-XX:MaxTenuringThreshold”参数可以设置这个年龄,他就会进入老年代
    (2)动态年龄判定规则,如果一旦发现某次新生代GC过后,存活对象超过了Survivor的50%
    (3)存活对象在Survivor放不下了,都会让对象进入老年代中。
  7. 大对象Region:在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,只要一个对象超过region的50%,就会放到专门存放大对象的region中,大对象既然不属于新生代和老年代,在新生代、老年代在回收的时候,会顺带带着大对象Region一起回收。
  8. G1新生代采用的是复制算法来进行回收。
  9. 混合回收:G1有一个参数,是“-XX:InitiatingHeapOccupancyPercent”,他的默认值是45%,果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。
  10. G1回收流程:
    (1)初始标记:标记GC Root直接引用的对象,会stop the world
    (2)并发标记: 这个阶段会允许系统程序的运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象
    (3)最终标记:这个阶段会进入“Stop the World”,系统程序是禁止运行的,但是会根据并发标记 阶段记录的那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象
    (4)混合回收:
    一、这个阶段会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回
    收的预期性能和效率。其实老年代对堆内存占比达到45%的时候,触发的是“混合回收”。此时垃圾回收不仅仅是回收老年代,还会回收新生代,还会回收大对象。
    二、接着会停止系统程序,然后全力以赴尽快进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我们指定的范围内。
    三、会从新生代、老年代、大对象里各自挑选一些Region,保证用指定的时间(比如200ms)回收尽可能多的垃圾,这就是所谓的混合回收。
    四、最后一个阶段混合回收的时候,其实会停止所有程序运行,所以说G1是允许执行多次混合回收。执行一次混合回收回收掉 一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收回收掉一些Region。“-XX:G1MixedGCCountTarget”参数,就是在一次混合回收的过程中,最后一个阶段执行几次混合回收,默认值是8次
    五、“-XX:G1HeapWastePercent”,默认值是5%,他的意思就是说,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉。一旦空闲出来的Region数量达到了堆内存的5%,此时就会 立即停止混合回收,意味着本次混合回收就结束了。
    六、来G1整体是基于复制算法进行Region垃圾回收的,不会出现内存碎片的问题,不需要像CMS那样标记-清理之后,再进行内存碎片的整理。
    七、“-XX:G1MixedGCLiveThresholdPercent”,他的默认值是85%,意思就是确定要回收的Region的时候,必须是存活对象低于85%的Region才可以进行回收。否则要是一个Region的存活对象多余85%,你还回收他干什么?这个时候要把85%的对象都拷贝到别的Region,这个成本是很高的。
  11. 回收失败时的Full GC:如果在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region里去,此时万一出现拷贝的过程中发现没有空闲Region可以承载自己的存活对象了,就会触发 一次失败;一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的。

11.G1优化策略

  1. 减少进入老年代的对象
    (1)“-XX:MaxGCPauseMills”参数设置的值很大,导致系统运行很久,新生代可能都占用了堆
    内存的60%了,此时才触发新生代gc。那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。
    (2)或者是你新生代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。
    (3)尽量让短命对象在新生代回收掉,长期存活对象早进入老年代,G1的优化思路亦是如此。首先是根据具体业务系统,合理分配老年代和新生代的大小、新生代Eden和Survivor区大小 其次是根据具体业务系统,合理设置G1的MaxGCPauseMills大小。太小容易造成回收频繁,影响系统的吞吐量。太大会增大系统的停顿时间,影响用户体验

12.G1这种垃圾回收器到底在什么场景下适用呢?

  1. G1压缩内存空间会比较有优势,适合会产生大量碎片的应用;
  2. G1能够可预期的GC停顿时间,对高并发应用更有优势
  3. 其他垃圾收集器对大内存回收耗时较长,G1对内存分成多块区域,能够根据预期停顿时间选择性的对垃圾多的区域进行回收,适用多核、jvm内存占用大的应用
  4. parNew+cms回收器比较适用内存小,对象能够在新生代中存活周期短的应用

13.jstat工具使用

  1. 新生代对象增长速率: jstat -gc PID 1000 10 表示每隔1秒执行一次,总共执行10次,可以知道每秒的内存变化,新生代对象增长率等等
  2. Young GC的触发频率和每次耗时:触发频率可以根据新生代增长速率和新生代大小进行估算;每次耗时可以根据Young GC总耗时除于Young GC总次数得到
  3. 每次Young GC后有多少对象是存活和进入老年代:可以根据Young GC的触发频率,比如是3分钟一次,那么可以jstat -gc PID 18000 10,每隔3分钟统计一次,观察时Eden、Survivor、老年代的对象变化。
  4. Full GC的触发时机和耗时:也是根据老年代的对象增长速率,计算得出。耗时则有总耗时除于总次数。

14.jmap,jhat,jstack

  1. jmap可以查看系统运行时的内存区域:jmap -heap PID
  2. jmap查看对象分布:jmap -histo PID
  3. 使用jmap生成堆内存转储快照:jmap -dump:live,format=b,file=dump.hprof PID
  4. jmap生成的快照是二进制文件,需要使用jhat进行查看:jhat dump.hprof -port 7000
  5. jstack PID: 查看线程快照,jstack PID -> 文件目录: 导出线程快照,可以通过https://fastthread.io/网站进行可视化分析

15.JVM优化思路

  1. 尽量使对象留在新生代:
    (1)调整survivor大小,使对象进入survivor区
    (2)合理设置新生代对象进入老年代的年龄,老年代担保策略,大对象进入老年代的大小(设置大对象可以防止大对象在新生代频繁复制)
  2. 老年代配置:
    (1)-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0:GC多少次后,进行碎片整理配置。次数配置小,可以防止由于碎片导致对象无法进入老年代而引发的频繁full GC。
    (2)-XX:+CMSParallelInitialMarkEnabled:这个参数会在CMS垃圾回收器的“初始标记”阶段开启多线程并发执行
    (3)-XX:+CMSScavengeBeforeRemark:这个参数会在CMS的重新标记阶段之前,先尽量执行一次Young GC。果在重新标记之前,先执行一次Young GC,就会回收掉一些年轻代里没有人引用的对象。所以如果先提前回收掉一些对象,那么在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升CMS的重新标记阶段的性能,减少他的耗时。
    (4)SoftRefLRUPolicyMSPerMB设置为0,根据clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB,软引用的对象会被回收掉。JVM通过反射创建的字节码对象,都是软引用的,所以会导致这些对象被不断的回收,创建。(这点不是很明白,元空间的GC时机?元空间被回收,创建,理应不会导致元空间塞满而触发Full GC的?)
  3. 不用使用System.gc()去触发GC。可以使用-XX:+DisableExplicitGC来禁止显示执行GC

16.频繁Full GC的几种常见原因

  1. 系统承载高并发请求,或者处理数据量过大,导致Young GC很频繁,而且每次Young GC过后存活对象太多,内存分配不合理,Survivor区域过小,导致对象频繁进入老年代,频繁触发Full GC。
  2. 系统一次性加载过多数据进内存,搞出来很多大对象,导致频繁有大对象进入老年代,必然频繁触发Full GC
  3. 系统发生了内存泄漏,莫名其妙创建大量的对象,始终无法回收,一直占用在老年代里,必然频繁触发Full GC
  4. Metaspace(永久代)因为加载类过多触发Full GC
  5. 误调用System.gc()触发Full GC

17.可能发生OOM的几块区域

1.元空间
(1)元空间太小,不够放类信息。
(2)写系统的时候会用cglib之类的技术动态生成一些类,一旦代码中没有控制好,导致你生成的类过于多的时候,就很容易把Metaspace给塞满,进而引发内存溢出
2. 虚拟机栈
(1)递归
3. 堆
(1)高并发下系统负载过高
(2)代码缺陷(一次查询大量数据)

18.发生OOM时自动dump快照配置

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/local/app/oom

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值