JVM调优

一、 JVM运行时内存模型、1.7和1.8的区别

  1. JVM有本地方法栈、虚拟机栈、程序计数器、堆、方法区。根据线程是否共享,将JVM内存分为共享区和私有区。
    共享区有:堆和方法区,私有区有:程序计数器、虚拟机栈(栈)和本地方法栈。
    a.堆:保存着Java程序运行时的变量,比如new的对象、(字符串常量池、基本数据类型的包装类的对象池)。
    b.方法区(元空间):保存着静态的东西,存储类结构信息。(比如类信息、静态变量、常量等。注:如果静态变量的值是个对象,那么存放的是对象在堆中的内存地址。)
    c.程序计数器(PC寄存器):是一个程序执行的行号,程序在进行跳转时,我们要记住跳转的行号,它方便我们的程序进行还原。
    d.虚拟机栈:包含了Java方法执行时的状态,每一个Java方法都会在虚拟机栈里面创建一个栈帧,里面存放局部变量表、操作数栈、动态链接、方法出口等。
    e.本地方法栈:跟虚拟机栈类似,在用于调用操作系统的底层方法时才会创建栈帧。
    虚拟机栈的具体存放细节:
    局部变量表:存放局部变量的空间,存放对象在堆中的内存地址。
    操作数栈:程序在运行过程中,进行运算操作的临时存储空间。
    动态链接(符号引用):存放着方法在方法区的入口地址。
    方法出口:记录着方法执行的线程位置,为了执行完方法后可以回到原来的位置 ,还存着返回值。

  2. 1.7和1.8的区别和原因:
    jdk1.7:存在永久代,运行时常量池在永久代,字符串常量池从永久代的运行时常量池分离到堆里
    jdk1.8:没有永久代,替换它的是元空间,元空间所占的内存不是在虚拟机内部,⽽是本地内存空间
    1、字符串存在永久代中,容易出现性能问题和内存溢出。
    2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难。(太小容易出现永久代溢出,太大则容易导致老年代溢)
    防止方法区太小造成内存溢出,太大占用JVM太多内存空间
    3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

二、 一个对象从加载到JVM,再到被GC清除,都经历了什么过程?

1、用户创建一个对象,JVM首先需要到方法区去找对象的类型信息。然后再创建对象。
2、JVM要实例化一个对象,首先要在堆当中先创建一个对象。->半初始化状态
3、对象首先会分配在堆内存中新生代的Eden。然后经过一次Minor GC,对象如果存活,就会进入S区。在后续的每次GC中,如果对象一直存活,就会在S区来回拷贝,每移动一次,年龄加1。->多大年龄才会移入老年代? 年龄最大15,超过一定年龄后,对象转入老年代。
4、当方法执行结束后,栈中的指针会先移除掉。
5、堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉。

三、JAVA类加载的全过程是怎么样的?什么是双亲委派机制?有什么作用?怎么打破?

  1. 类加载运行全过程:从java.exe创建JVM,到类加载器初始化,到加载类的过程,main方法执行,程序结束,最后,JVM销毁。
    类加载全过程大致分为三个部分:类加载运行全过程
    1.JVM创建:java.exe(C++启动程序)创建了JVM和引导类加载器
    2.加载类:JVM启动之后,C++调用Java启动程序,Launcher创建(getLauncher())Java级别的一些类加载器,然后通过这些类加载器去加载(loadClass())字节码文件。
    3.程序运行:C++底层调用main方法,程序运行,运行结束,JVM销毁。
    主要过程:
    ①加载:在硬盘上查找并通过I0读入字节码文件,在堆内存中生成一个对象,作为方法区数据的访问入口
    ②验证: 检查加载到的字节信息是否符合JVM规范
    ③准备:给类的静态变量分配内存,并赋予初始值,半初始化状态
    ④解析(动态链接): 将静态方法的符号引用替换为直接引用(有对应的内存地址信息)
    ⑤初始化: 给类的静态变量初始化为指定的值,执行静态代码块,使⽤,卸载

  2. 双亲委派机制:
    向上委派查找缓存,如果父类有加载这个类就直接返回,到顶层加载器为止;向下委派查找加载路径,有就加载这个类。
    当⼀个类收到了类加载请求,他首先不会尝试自己去加载这个类,⽽是把这个请求委派给父类去完成,每⼀个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当⽗类加载器反馈自己无法完成这个请求的时候,子类加载器才会尝试自己去加载。
    类加载器:根据指定全限定名称将Class文件加载到JVM内存,转为Class对象。
    ①启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
    ②扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
    ③应用程序类加载器(Application ClassLoader):负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

  3. 双亲委派模型的好处:
    ●主要是为了安全性,避免用户自己编写的类动态替换Java的一些核心类,比如String。
    ●同时也避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是不同的两个类。

  4. 打破双亲委派机制:
    不仅要继承ClassLoader类,还要重写loadClass和findClass方法。

四、垃圾回收!定位垃圾的方式?GCRoot?垃圾回收算法有哪些?

  1. 定位垃圾的两种方式:引用计数算法、根可达算法(存在内存中,从引用根对象向下一直找引用,找不到的对象就是垃圾)。

  2. GC Root:栈 jvm栈、本地栈、class类、常量池、静态变量。

  3. 垃圾回收算法有哪些?
    1、引⽤计数法:
    原理:给堆内存中的每个对象记录一个引用个数,引用个数为0就是垃圾。对于⼀个对象A,只要有任何⼀个对象引⽤了A,则A的引⽤计数器就加1,当引⽤失效时,引⽤计数器就减1。
    问题:i. 性能比较低,引⽤和去引⽤伴随加法和减法;ii. 无法解决循环引⽤。
    2、MarkSweep标记清除法:
    原理:标记阶段和清除阶段。在标记阶段讲垃圾内存标记出来,清除阶段:直接将垃圾内存回收。
    问题:i. 标记和清除两个过程效率不⾼,而且会产⽣大量的内存碎⽚。导致需要分配较⼤对象时⽆法找到⾜够的连续内存⽽需要触发⼀次GC操作。
    3、Copying复制算法:
    原理:将内存空间分为两块,每次只使⽤其中⼀块。在垃圾回收时,将当前这一块存活对象拷贝到另一块,之后清除当前的内存块。
    问题:i. 浪费空间;ii.效率跟存活对象的个数有关,所以不适⽤于存活对象⽐较多的场合,如⽼年代。
    4、MarkCompack标记压缩法:
    原理:适合⽤于存活对象较多的场合,如⽼年代。它在标记-清除算法的基础上做了⼀些优化。标记阶段⼀样,但在完成标记之后,不是直接清理垃圾内存,而是将所有存活对象压缩到内存的⼀端,然后 清除边界外所有内存。
    优点:i. 解决了标记- 清除算法导致的内存碎⽚问题和在存活率较⾼时复制算法效率低的问题。
    5、分代回收法:
    原理:根据对象存活周期的不同将内存划分为⼏块,⼀般是新⽣代和⽼年代,新⽣代基本采⽤复制算法,⽼年代采⽤标记整理算法。
    总结:这些算法各有利弊,各有各自适合的场景

五、什么是STW?JVM有哪些垃圾回收器?他们都是怎么工作的?STW都发生在哪些阶段?什么是三色标记?如何解决错标记和漏标记的问题?为什么要设计这么多的垃圾回收器?

1.什么是STW?
Stop-The-World。是在垃圾回收算法执行过程当中,需要将)VM内存冻结的一种状态。在STW状态下,JAVA的所有线程都是停止执行的-GC线程除外,native方法可以执行,但是,不能与 M交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。
2.JVM有哪些垃圾回收器?
分代算法:Serial、Parallel 、ParNew、CMS;不分代算法:G1、ZGC;
3.垃圾回收器的底层原理?以及STW都发生在哪些阶段?

  1. Serial串行:整体过程比较简单,就像踢足球一样,需要GC时,直接暂停,GC完了再继续。
    这个垃圾回收器,是早期垃圾回收器,只有一个线程执行GC。在多CPU架构下,性能就会下降严重。只适用于几十兆的内存
    空间。
  2. Parallel 并行:在串行基础上,增加多线程GC。PS+PO这种组合是JDK1.8默认的垃圾回收器。在多CPU的架构下,性能会比Serial高很多。
  3. CMS Concurrent Mark Sweep:核心思想,就是将STW打散,让一部分GC线程与用户线程并发执行。整个GC过程分为四个阶段
    1、初始标记阶段:STW 只标记出根对象直接引l用的对象。
    2、并发标记:继续标记其他对象,与应用程序是并发执行。
    3、重新标记:STW 对并发执行阶段的对象进行重新标记。
    4、并发清除:并行。将产生的垃圾清除。清除过程中,应用程序又会不断的产生新的垃圾,叫做浮动垃圾。这些垃圾就要留到下一次GC过程中清除。
  4. G1 Garbage First 垃圾优先:他的内存模型是实际不分代,但是逻辑上是分代的。在内存模型中,对于堆内存就不再分老年代和新生代,而是划分成一个-
    个的小内存块,叫做Region。每个Region可以隶属于不同的年代。
    GC分为四个阶段:
    第一:初始标记 标记出GCRoot直接引l用的对象。STW
    第二:标记Region,通过RSet标记出上一个阶段标记的Region引l用到的Old区Region。
    第三:并发标记阶段:跟CMS的步骤是差不多的。只是遍历的范围不再是整个Old区,而只需要遍历第二步标记出来的
    Region。
    第四:重新标记:跟CMS中的重新标记过程是差不多的。
    第五:垃圾清理:与CMS不同的是,G1可以采用拷贝算法,直接将整个Region中的对象拷贝到另一个Region。而这个阶
    段,G1只选择垃圾较多的Region来清理,并不是完全清理。

ZGC 和shengnandaoh是未来的垃圾回收器。shengnandaoh是G1的升级版。
ZGC效率很高,跟内存大小无关,采用颜色指针的机制,是完全不分代,没有老年代,新生代。
G1内存模型是实际不分代,但是逻辑上是分代的。
为什么要设计这么多的垃圾回收器?内存逐渐变大,服务器上t。

4.什么是三色标记?
三色标记是在cms和g1中使用的垃圾追踪算法,是一种逻辑上的抽象,它将每个内存对象分成三种颜色。

黑色:从GCRoots开始,已扫描过它全部引用的对象,标记为黑色。
灰色:扫描过对象本身,还没完全扫描过它全部引用的对象,标记为灰色。
白色:还没扫描过的对象,标记为白色。

所以,从GCRoots开始,顺着一直向下扫描,用可达性分析算法,最后所有的白色对象,都是垃圾对象,可以回收。

5.三色标记的漏标问题:
①我们采用一个最简单的模型,只有三个对象:某个状态下,黑色->灰色->白色。
②如果一切顺利,不发生任何引用变化,gc线程顺着灰色的引用向下扫描,最后都变成黑色,都是存活对象。
③但是如果出现了这样一个状况,在扫描到灰色的时候,还没有扫描到这个白色对象,此时,黑色对象引用了这个白色对象,而灰色对象指向了别人,或者干脆指向了null,也就是取消了对白色对象的引用。
④那么我们会发现一个问题,根据三色标记规则,gc会认为,黑色对象是本身已经被扫描过,并且它所有指向的引用都已经被扫描过,所以不会再去扫描它有哪些引用指向了哪些对象。
⑤然后,灰色对象因为取消了对白色对象的引用,所以后面gc开始扫描所有灰色对象的引用时候,也不会再扫描到白色对象。最后结果就是,白色对象直到本次标记扫描结束,也是白色,根据三色标记规则,认为它是垃圾,被清理掉。但是实际情况,它明显是被引用的对象,是绝对不能当做垃圾来清除的,因为漏标,最后被当垃圾清理掉了。

漏标的两个充要条件:①有至少一个黑色对象在自己被标记之后指向了这个白色对象;②所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用。这两个条件,必须全满足,才会造成漏标问题。换言之,我们破坏任何一个条件.这个白色对象,就不会再被漏标。

6.如何解决漏标记的问题

①CMS采用的是 增量更新
​增量更新破坏的是第一个条件,我们在这个黑色对象增加了对白色对象的引用之后,将它的这个引用,记录下来,在最后标记的时候,再以这个黑色对象为根,对它的引用进行重新扫描。
​ 可以简单理解为,当一个黑色对象增加了对白色对象的引用,那么这个黑色对象就被变灰。
​ 这样有一个缺点,就是会重新扫描这个黑色对象的所有引用,比较浪费时间。

②G1采用的是 原始快照
​原始快照破坏的是第二个条件,我们在这个灰色对象取消对白色对象的引用之前,将这个引用记录下来,在最后标记的时候,再以这个引用指向的白色对象为根,对它的引用进行扫描。
可以简单理解为,当一个灰色对象取消了对白色对象的引用,那么这个白色对象被变灰。
这样做的缺点就是,这个白色对象有可能并没有黑色对象去引用它,但是它还是被变灰了,就会导致它和它的引用,本来应该被垃圾回收掉,但是此次GC存活了下来,就是所谓的浮动垃圾.其实这样是比较可以忍受的,只是让它多存活了一次GC而已,浪费一点点空间,但是会比增量更新更省时间。
垃圾收集底层三色标记算法实现原理

六、你们项目如何排查JVM问题 ?

分两种情况
①对于还在正常运⾏的系统:
1.可以使⽤jmap来查看JVM中各个区域的使⽤情况;
2.可以通过jstack来查看线程的运⾏情况,⽐如哪些线程阻塞、是否出现了死锁;
3.可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc⽐较频繁,那么就得进⾏调优了 ;
4.通过各个命令的结果,或者jvisualvm等⼯具来进⾏分析 ;
⾸先,初步猜测频繁发送fullgc的原因,如果频繁发⽣fullgc但是⼜⼀直没有出现内存溢出,那么表示 fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进⼊到⽼年代,对于这种情况,就要考虑这些存活时间不⻓的对象是不是⽐较大,导致年轻代放不下,直接进⼊到了⽼年代,尝试加⼤年轻代的⼤⼩,如果改完之后,fullgc减少,则证明修改有效;
同时,还可以找到占⽤CPU最多的线程,定位到具体的⽅法,优化这个⽅法的执⾏,看是否能避免某些对象的创建,从⽽节省内存 。
② 对于已经发⽣了OOM的系统:
1.⼀般⽣产系统中都会设置当系统发⽣了OOM时,⽣成当时的dump⽂件(- XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
2. 我们可以利⽤jsisualvm等⼯具来分析dump⽂件
3. 根据dump⽂件找到异常的实例对象,和异常的线程(占⽤CPU⾼),定位到具体的代码
4. 然后再进⾏详细的分析和调试

七、如何进行JVM调优?JVM参数有哪些?怎么查看一个JAVA进程的JVM参数?谈谈你了解的JVM参数。如果一个java程序每次运行一段时间后,就变得非常卡顿,你准备如何对他进行优化?

JVM调优主要就是通过定制JVM运行参数来提高JAVA应用程度的运行数据
JM参数大致可以分为三类:
1、标注指令:-开头,这些是所有的HotSpot都支持的参数。可以用java -help 打印出来。
2、非标准指令:-X开头,这些指令通常是跟特定的HotSpot版本对应的。可以用java-X 打印出来。
-XSS 设置java线程堆栈大小
-Xms:启动JVM时的堆内存空间。
-Xmx:堆内存最⼤限制。
设定新⽣代⼤⼩。
新⽣代不宜太⼩,否则会有⼤量对象涌⼊⽼年代。
-XX:NewRatio:新⽣代和⽼年代的占⽐。
-XX:NewSize:新⽣代空间。
-XX:SurvivorRatio:伊甸园空间和幸存者空间的占⽐。
-XX:MaxTenuringThreshold:对象进⼊⽼年代的年龄阈值。
设定垃圾回收器
年轻代:-XX:+UseParNewGC。
⽼年代:-XX:+UseConcMarkSweepGC。
3、不稳定参数:-xX 开头,这一类参数是跟特定HotSpot)反本对应的,并且变化非常大。详细的文档资料非常少。在JDK1.8版本下,有几个常用的不稳定指令:
java -XX:+PrintCommandLineFlags:查看当前命令的不稳定指令。
java -xX:t PrintFlagslnitial:查看所有不稳定指令的默认值。
java-xx:+PrintFlagsFinal: 查看所有不稳定指令最终生效的实际值。
调优工具:jvisualvm、阿里巴巴Arthas
学习调优的方式:看一些开源项目的jvm参数,比如rocketmq。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱喝皮蛋瘦肉粥的小饶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值