首页 后端 Java 正文

深入理解JVM虚拟机

类加载器

类加载器:

  • 引导类加载器 BootstrapLoader:负责加载支撑JVM运行的核心类库,例如rt.jar/charset.jar

  • 扩展类加载器ExtClassLoader:负责加载支撑JVM运行的扩展类库,jre/lib/ext目录下的jar

  • 应用类加载器AppClassLoader:负责加载classpath目录下的类,就是加载你自己写的类。

  • 自定义类加载器:加载注定路径下的类

类加载流程: 深入理解JVM虚拟机  第1张 双亲委派机制:在JVM启动时,会由扩展类加载器加载一个单例的sun.misc.Launcher类,在Launcher的构造方法中会创建ExtClassLoader和AppClassLoader两个类加载器,其中ExtClassLoader的parent是BootstrapLoader。AppClassLoader是ExtClassLoader。JVM默认就是使用AppClassLoader来加载类。在双亲委派模型下,AppClassLoader加载类时,会先将该类交给ExtClassLoader加载,ExtClassLoader会将该类交给BootstrapLoader加载。如果BootstrapLoader加载成功则返回,失败则交给子加载器ExtClassLoader加载。以此类推,完成加载。

通过源码看双亲委派: AppClassLoader.loadClass方法,会调用父类的loadClass方法: 父类ClassLoader.loadClass方法:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
           //检查是否加载过此类
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //交给父类加载
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }


为什么要设计双亲委派机制?

  1. 沙箱安全机制,如自己写的java.lang.String.class不会被加载,这样便防止了核心API被随意篡改。

  2. 避免类的重复加载:当父加载器加载了该类,子加载器就不用再次加载了。

全盘负责委托机制:

“全盘负责”是指当一个classLoader装载一个类时,除非显式的制定另一个类加载器,否则该类的所依赖及其引用的类都由该类加载器加载。

自定义类加载器

自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要是重写findClass方法。

如何打破双亲委派模型?

打破双亲委派,只需要重写loadClass(String,Boolean)方法即可,不将类加载先交给父类即可。

打破双亲委派的例子:

tomcat打破双亲委派

  1. tomcat是一个web容器,一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。

  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。

  3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。

tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。

JDBC打破双亲委派

JDBC的连接类Driver通常由第三方实现,而Driver的加载由rt.jar中使用,这里的类加载器时BootstrapLoader,无法加载指定三方库中的类,所以JDBC使用了Java spi机制,逻辑上打破了双亲委派模型。

JVM模型

深入理解JVM虚拟机  第2张 内存参数: 深入理解JVM虚拟机  第3张 关于元空间的JVM参数有两个:-XX:MetaspaceSize=N和 -XX:MaxMetaspaceSize=N -XX:MaxMetaspaceSize: 设置元空间最大值, 默认是-1, 即不限制, 或者说只受限于本地内存大小。 -XX:MetaspaceSize: 指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M,达到该值就会触发 full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超 过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。 由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于元空间发生 了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大, 对于8G物理内存的机器来说,一般我会将这两个值都设置为256M。

栈参数设置: -Xss:设置线程栈的内存,默认1M,当深度递归时可能触发栈内存溢出(StackOverflowError)。-Xss设置越小,JVM整体能开的线程数越多。

对象的内存分配方式 在类加载检查过后,新生对象需要的内存大小就确定下来了。这时就要在堆中为其分配大小。

  • 指针碰撞(默认用指针碰撞):如果JVM内存都是规整的,则可以使用一个指针指在边界处,每次移动一个对象大小的内存长度。

  • 空闲列表:如果内存时零散的,空闲与非空闲交错,就无法使用指针碰撞了,这时就要维护一个内存空闲列表,记录哪个内存块是可用的。每次为对象分配内存的时候,从列表中找出一个足够的内存块进行分配。

如何解决内存分配并发问题?

  1. CAS(compare and swap):虚拟机采用CAS分配失败重试的方式进行解决。

  2. 本地线程缓冲(Thread Local Allocation Buffer,  TLAB):虚拟机会预先给每个线程分配一小块内存,该线程创建的对象会现在改内存中分配。通过XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启XX:+UseTLAB),XX:TLABSize 指定TLAB大小。 如果TLAB失败会继续采用CAS分配。

对象头: 深入理解JVM虚拟机  第4张什么是java对象的指针压缩?

  1. jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩

  2. jvm配置参数:UseCompressedOops,compressed压缩、oop(ordinary object pointer)对象指针

  3. 启用指针压缩:XX:+UseCompressedOops(默认开启),禁止指针压缩:XX:UseCompressedOops 为什么要进行指针压缩?

  4. 在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力

  5. 为了减少64位平台下内存的消耗,启用指针压缩功能

  6. 在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G)

  7. 堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间

  8. 堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内 存不要大于32G为好

对象内存分配流程 深入理解JVM虚拟机  第5张栈上分配 通过逃逸分析,确定对象不会被方法外部访问,就会将临时对象分配在栈上,随着栈帧的出栈而销毁,减轻了GC的压力。 逃逸分析:分析对象的动态作用域,当一个对象在方法中被定义后,不会在返回值或者参数中逃逸出本方法,那么该对象就能进行栈上分配。 JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优 先分配在栈上(栈上分配),JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)。 标量替换:通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该 对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就 不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认 开启。 标量与聚合量:标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及 reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一 步分解的聚合量。

对象在Eden区分配: 大多数情况下,对象会在新生代的Eden区分配(大对象会直接分配在老年代)。当Eden区内存不足时,会触发一次young GC来释放空间。

young GC:回收新生代垃圾,比较频繁,速度较快。 full GC:回收新生代、老年代、元空间垃圾,full GC一般会比young GC慢10倍以上,所以要尽量避免full gc。

Eden与Survivor区默认8:1:1

对象会先分配在Eden区,当Eden区空间不足时,会触发young GC,99%得对象会被回收掉,存活的对象会被挪到为空的Survivor区。因为新生代的对象大部分时朝生夕死的,存活时间很短,所以JVM默认Eden区与Survivor区8:1:1是很合适的,让Eden区足够大,Survivor够用即可。 JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变 化可以设置参数-XX:-UseAdaptiveSizePolicy

大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大 对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下 有效。 比如设置JVM参数:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC ,再执行下上面的第一 个程序会发现大对象直接进了老年代 为什么要这样呢? 为了避免为大对象分配内存时的复制操作而降低效率。

长期存活的对象进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在 老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度 (默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代 的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

对象动态年龄判断

当Eden区内存不足时,会触发young GC,这时所有存活的对象都会挪到空白的Survivor区,如果存活对象的总大小>Survivor的50%(可配置阈值),则会触发动态年龄判断机制,会将年龄由小到大存入Survivor,当存到50%时,会将大于Survivor中最大年龄的对象直接放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在young gc之后触发的。

老年代空间担保机制

新生代每次发生young GC之前JVM都会计算老年代剩余的可用空间。如果该空间<新生代已用内存大小(包括垃圾对现象),就会看老年代空间担保机制配置:XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了。 如果设置了担保机制,JVM就会判断老年代可用空间是否大于之前历次young GC后进入老年代对象平均大小,如果小于则直接发起full GC。如果大于,则继续young GC,如果GC后老年代空间不够,继续full GC,还不够oom。 如果未设置,则直接发起full GC。

该配置的目的是,根据历代GC平均值判断是否需要full gc。达到减少full gc的目的。

对象的内存回收

引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决 对象之间相互循环引用的问题

可达性分析算法

将“GC Roots” 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的 对象都是垃圾对象。 GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等。 深入理解JVM虚拟机  第6张

常见的引用类型

强引用、软引用、弱引用、虚引用

  1. 强引用:普通的对象引用。

  2. 软引用:将对象用SoftReference软引用类型包裹,正常情况下不会被GC回收掉,但是当GC后还没有足够的内存,这时jvm会回收软引用对象。软引用可以用来实现内存敏感的高速缓存。

    public static SoftReference<User> user = new SoftReference<User>(new User());


  3. 弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉。在ThreadLocal中有使用。

  4. 虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,可以用来直接内存回收。

如何判断一个类是无用的类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢? 类需要同时满足下面3个条件才能算是 “无用的类” :

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。

  2. 加载该类的 ClassLoader 已经被回收。

  3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

垃圾收集

深入理解JVM虚拟机  第7张分代收集理论 JVM使用分代收集的思想,将堆分为新生代和老年代,根据各个年代的特点进行垃圾收集。比如年轻代,大部分对象是朝生夕死的,每次gc都要回收大量的垃圾对象,只有少量的对象存活,所以适合使用标记-复制算法进行垃圾收集。而老年代,大部分是长期存活的对象,每次gc只回收少量的垃圾对象,所以适合使用标记清除/标记整理算法。注意,“标记-清除”或“标记-整理”算法会比复制算法慢10倍以上。

标记复制算法

复制算法将内存分为两块大小相同的空间,每次使用其中的一块。当这一块内存使用完后,会将存活的对象复制到另一块内存中区,并清空原有使用的内存块。这样就使每次的内存回收都是对内存区间的一半进行回收。自带内存整理功能。 深入理解JVM虚拟机  第8张

标记清除算法

标记清除算法,是对存活的对象进行标记,回收未标记的对象。 有两个明显的问题:

  1. 效率问题:如果需要标记的对象过多,效率不高。

  2. 会产生空间碎片:标记清除后会产生大量不连续的空间碎片。 3.深入理解JVM虚拟机  第9张

标记整理算法

根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回 收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。 深入理解JVM虚拟机  第10张

垃圾收集器

深入理解JVM虚拟机  第11张如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。 虽然我们对各个收集器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出 现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一 种四海之内、任何场景下都适用的完美收集器存在,那么我们的Java虚拟机就不会实现那么多不同的垃圾收集器了。

Serial和Serial old收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

Serial(串行)收集器是最基本的、历史最悠久的垃圾收集器。是一个单线程收集器。该收集器会采用单线程进行垃圾收集,在垃圾收集期间会停止所有应用线程,就是STW(stop the world),直到垃圾收集完成。 新生代采用标记-复制算法,老年代采用标记-整理算法。 深入理解JVM虚拟机  第12张 虚拟机的设计者们当然知道Stop The World带来的不良用户体验,所以在后续的垃圾收集器设计中停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。 但是Serial收集器有没有优于其他垃圾收集器的地方呢?当然有,它简单而高效(与其他收集器的单线程相比)。Serial收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。 Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在JDK1.5 以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。

Parallel Scavenge(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))

Parallel垃圾收集器其实就是Serial的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)均和Serial收集器类似。默认收集线程数和CPU核数相同,,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。 Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停 顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。 Parallel Scavenge收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以 选择把内存管理优化交给虚拟机去完成也是一个不错的选择。 新生代采用复制算法,老年代采用标记-整理算法。 深入理解JVM虚拟机  第13张 Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集器)。

ParNew垃圾收集器

ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。 深入理解JVM虚拟机  第14张 它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收 集器,后面会介绍到)配合工作。

CMS(Concurrent Mark Sweep)收集器

CMS是使用于老年代的垃圾收集器,使用的是标记-清除算法。是一种以获取最短停顿时间为目标的收集器,注重用户体验,降低一次垃圾回收时连续STW的时间。 垃圾收集整体分为五个步骤:

  1. 初始标记:暂停所有用户线程(STW),从GC roots标记直接引用的对象,速度很快。

  2. 并发标记用户线程不停顿,回收线程从初始标记的对象往下遍历对象图,本次过程较长,耗时较久,但是不停止用户线程。因为程序在继续运行,可能已经标记过的对象状态会改变。

  3. 重新标记STW,对已经标记的对象重新遍历标记,速度较快。用到了三色标记法。

  4. 并发清除:开启用户线程,不STW。对未标记的对象进行清除,此阶段产生的对象标记黑色,不做清除。

  5. 并发重置:重置本次GC标记数据。

从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面几个明显的缺点:

  • 对CPU资源敏感(会和服务抢CPU资源)

  • 会产生浮动垃圾,在并发标记和并发清除阶段因为应用程序线程会继续运行,会参数浮动垃圾,只能等到下次gc回收。

  • 使用标记-清除算法,会产生连续的空间碎片。可以通过参数(-XX:+UseCMSCompactAtFullCollection)配置JVM执行标记-清除完成后进行整理。

  • 会产生并发模型失败问题。在并发标记和并发清理阶段,应用程序继续运行,会不断的参数垃圾,这时会出现新的gc,这时就出现了“concurrent mode failure”,此时会使用serial old收集器回收,进入STW。

CMS核心参数:

  1. -XX:+UseConcMarkSweepGC:启用cms

  2. -XX:ConcGCThreads:并发的GC线程数

  3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)

  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一 次

  5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)

  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设 定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整

  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引 用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段

  8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW

  9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;

垃圾收集底层算法实现

三色标记

  • 黑色:表示对象已经被垃圾收集器扫描过,它是存活的,并且该对象的所有引用对象也都被扫描过。CMS并发阶段产生的对象就会被标记为黑色。

  • 灰色:表示对象已经被垃圾收集器扫描过,是存活的,但是该对象的引用对象未被完全扫面。

  • 白色: 表示未被垃圾收集器扫描过的对象。初始都是白色标记。在标记阶段结束后仍为白色的对象就是不可达对象,即为垃圾对象。深入理解JVM虚拟机  第15张

多标-浮动垃圾 在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过 (被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动 垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。 另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分 对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。

漏标-读写屏障 漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案: 增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB) 。 增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之 后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向 白色对象的引用之后, 它就变回灰色对象了。 原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑 色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾) 以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。

现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同:比如白色/黑色 集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可 以是广度/深度遍历等等。 对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下: CMS:写屏障 + 增量更新 G1,Shenandoah:写屏障 + SATB ZGC:读屏障

为什么G1用SATB?CMS用增量更新? 我的理解:SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描 被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代 区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC 再深度扫描。

跨代引用

记忆集与卡表 在新生代做GCRoots可达性扫描过程中可能会碰到跨代引用的对象,这种如果又去对老年代再去扫描效率太低了。 为此,在新生代可以引入记录集(Remember Set)的数据结构(记录从非收集区到收集区的指针集合),避免把整个 老年代加入GCRoots扫描范围。事实上并不只是新生代、 老年代之间才有跨代引用的问题, 所有涉及部分区域收集 (Partial GC) 行为的垃圾收集器, 典型的如G1、 ZGC和Shenandoah收集器, 都会面临相同的问题。 垃圾收集场景中,收集器只需通过记忆集判断出某一块非收集区域是否存在指向收集区域的指针即可,无需了解跨代引 用指针的全部细节。 hotspot使用一种叫做“卡表”(cardtable)的方式实现记忆集,也是目前最常用的一种方式。关于卡表与记忆集的关系, 可以类比为Java语言中HashMap与Map的关系。 卡表是使用一个字节数组实现:CARD_TABLE[ ],每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡 页”。 hotSpot使用的卡页是2^9大小,即512字节 深入理解JVM虚拟机  第16张 一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0. GC时,只要筛选本收集区的卡表中变脏的元素加入GCRoots里。 卡表的维护 卡表变脏上面已经说了,但是需要知道如何让卡表变脏,即发生引用字段赋值时,如何更新卡表对应的标识为1。Hotspot使用写屏障维护卡表状态。

G1垃圾收集器

G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC 停顿时间要求的同时,还具备高吞吐量性能特征. 深入理解JVM虚拟机  第17张深入理解JVM虚拟机  第18张 G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数"-XX:G1HeapRegionSize"手动指定Region大小,但是推荐默认的计算方式。 G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200MB左右的内存,对应大概是100个Region,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent”调整。年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个。一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能 可能会动态变化。

G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。

G1收集器一次GC的运作过程大致分为以下几个步骤:

  • 初始标记:STW,标记GC roots直接引用的对象,速度很快。

  • 并发标记:不STW,从初始标记的对象望后遍历。同CMS并发标记

  • 重新标记:STW,对标记的对象重新遍历。同CMS重新标记

  • 筛选回收筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。

G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字 Garbage-First的由来),比如一个Region花200ms能回收10M垃圾,另外一个Region花50ms能回收20M垃圾,在回 收时间有限情况下,G1当然会优先选择后面这个Region回收。这种使用Region划分内存空间以及有优先级的区域回收 方式,保证了G1收集器在有限时间内可以尽可能高的收集效率。

G1被视为JDK1.7以上版本Java虚拟机的一个重要进化特征。它具备以下特点:

  • 并行与并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

  • 分代收集:G1可以不需要其他收集器配合就能独立管理整个GC堆,G1逻辑上保留了分代收集理论。

  • 空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器(基于region标记清除);从局部上来看是基于“复制”算法实现的(对region的清除是复制算法)。

  • 可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1 和 CMS 共同的关注点,但G1 除了追求低停顿外,还能建立可预测的停顿时间模型能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。

毫无疑问, 可以由用户指定期望的停顿时间是G1收集器很强大的一个功能, 设置不同的期望停顿时间, 可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。 不过, 这里设置的“期望值”必须是符合实际的, 不能异想天开, 毕竟G1是要冻结用户线程来复制对象的, 这个停顿时间再怎么低也得有个限度。 它默认的停顿目标为两百毫秒, 一般来说, 回收阶段占到几十到一百甚至接近两百毫秒都很正常, 但如果我们把停顿时间调得非常低, 譬如设置为二十毫秒, 很可能出现的结果就是由于停顿目标时间太短, 导致每次选出来的回收集只占堆内存很小的一部分, 收集器收集的速度逐渐跟不上分配器分配的速度, 导致垃圾慢慢堆积。 很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间, 但应用运行时间一长就不行了, 最终占满堆引发Full GC反而降低性能, 所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。

G1垃圾收集分类

  • Young GC:YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC。

  • Mixed GC不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC。

  • Full GC:停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这 个过程是非常耗时的。

G1收集器参数设置

  • -XX:+UseG1GC:使用G1收集器

  • -XX:ParallelGCThreads:指定GC工作的线程数量

  • -XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区

  • -XX:MaxGCPauseMillis:目标暂停时间(默认200ms)

  • -XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)

  • -XX:G1MaxNewSizePercent:新生代内存最大空间

  • -XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个 年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代

  • -XX:MaxTenuringThreshold:最大年龄阈值(默认15)

  • -XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了

  • -XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。

  • -XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。

  • -XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。

G1垃圾收集器优化建议 &ensp;&ensp;假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc。 &ensp;&ensp;那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。 &ensp;&ensp;或者是你年轻代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。 &ensp;&ensp;所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc。

什么场景适合使用G1

  1. 50%以上的堆被存活对象占用

  2. 对象分配和晋升的速度变化非常大

  3. 垃圾回收时间特别长,超过1秒

  4. 8GB以上的堆内存(建议值)

  5. 停顿时间要求在500ms以内

每秒几十万并发的系统如何优化JVM &ensp;&ensp;Kafka类似的支撑高并发消息系统大家肯定不陌生,对于kafka来说,每秒处理几万甚至几十万消息时很正常的,一般 来说部署kafka需要用大内存机器(比如64G),也就是说可以给年轻代分配个三四十G的内存用来支撑高并发处理,这里就 涉及到一个问题了,我们以前常说的对于eden区的young gc是很快的,这种情况下它的执行还会很快吗?很显然,不可 能,因为内存太大,处理还是要花不少时间的,假设三四十G内存回收可能最快也要几秒钟,按kafka这个并发量放满三 四十G的eden区可能也就一两分钟吧,那么意味着整个系统每运行一两分钟就会因为young gc卡顿几秒钟没法处理新消 息,显然是不行的。那么对于这种情况如何优化了,我们可以使用G1收集器,设置 -XX:MaxGCPauseMills 为50ms,假 设50ms能够回收三到四个G内存,然后50ms的卡顿其实完全能够接受,用户几乎无感知,那么整个系统就可以在卡顿几 乎无感知的情况下一边处理业务一边收集垃圾。 &ensp;&ensp;    G1天生就适合这种大内存机器的JVM运行,可以比较完美的解决大内存垃圾回收时间过长的问题。

ZGC收集器(-XX:+UseZGC)

ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器,ZGC可以说源自于是Azul System公司开发的C4(Concurrent Continuously Compacting Collector) 收集器。

ZGC特性

如下图所示,ZGC的目标主要有4个:

  • 支持TB量级的堆。我们生产环境的硬盘还没有上TB呢,这应该可以满足未来十年内,所有JAVA应用的需求了 吧。

  • 最大GC停顿时间不超10ms。目前一般线上环境运行良好的JAVA应用Minor GC停顿时间在10ms左右, Major GC一般都需要100ms以上(G1可以调节停顿时间,但是如果调的过低的话,反而会适得其反),之所以能 做到这一点是因为它的停顿时间主要跟Root扫描有关,而Root数量和堆大小是没有任何关系的。

  • 奠定未来GC特性的基础

  • 最糟糕的情况下吞吐量会降低15%。这都不是事,停顿时间足够优秀。至于吞吐量,通过扩容分分钟解决。 另外,Oracle官方提到了它最大的优点是:它的停顿时间不会随着堆的增大而增长!也就是说,几十G堆的停顿时间是 10ms以下,几百G甚至上T堆的停顿时间也是10ms以下。

ZGC不分代 单代,即ZGC「没有分代」。我们知道以前的垃圾回收器之所以分代,是因为源于“「大部分对象朝生夕死」”的假设,事实上大部分系统的对象分配行为也确实符合这个假设。 那么为什么ZGC就不分代呢? 因为分代实现起来麻烦,作者就先实现出一个比较简单可用的单代版本,后续会优化。

ZGC内存布局

ZGC收集器是一款基于Region内存布局的, 暂时不设分代的, 使用了读屏障、 颜色指针等技术来实现可并发的标记-整 理算法的, 以低延迟为首要目标的一款垃圾收集器。 ZGC的Region可以具有如图3-19所示的大、 中、 小三类容量:

  • 小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象。

  • 中型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。

  • 大型Region(Large Region) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或以上的大对象。 每个大型Region中只会存放一个大对象, 这也预示着虽然名字叫作“大型Region”, 但它的实际容量完全有可能小于中型Region, 最小容量可低至4MB。 大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段, 稍后会介绍到)的, 因为复制一个大对象的代价非常高昂。 深入理解JVM虚拟机  第19张NUMA-aware NUMA对应的有UMA,UMA即Uniform Memory Access Architecture,NUMA就是Non Uniform Memory Access Architecture。UMA表示内存只有一块,所有CPU都去访问这一块内存,那么就会存在竞争问题(争夺内存总线访问权),有竞争就会有锁,有锁效率就会受到影响,而且CPU核心数越多,竞争就越激烈。NUMA的话每个CPU对应有一块内存,且这块内存在主板上离这个CPU是最近的,每个CPU优先访问这块内存,那效率自然就提高了: 深入理解JVM虚拟机  第20张 服务器的NUMA架构在中大型系统上一直非常盛行,也是高性能的解决方案,尤其在系统延迟方面表现都很优秀。ZGC是能自动感知NUMA架构并充分利用NUMA架构特性的。

颜色指针 Colored Pointers,即颜色指针,如下图所示,ZGC的核心设计之一。以前的垃圾回收器的GC信息都保存在对象头中,而ZGC的GC信息保存在指针中。 深入理解JVM虚拟机  第21张 每个对象有一个64位指针,这64位被分为:

  • 18位:预留给以后使用;

  • 1位:Finalizable标识,此位与并发引用处理有关,它表示这个对象只能通过finalizer才能访问;

  • 1位:Remapped标识,设置此位的值后,对象未指向relocation set中(relocation set表示需要GC的Region集合);

  • 1位:Marked1标识;

  • 1位:Marked0标识,和上面的Marked1都是标记对象用于辅助GC;

  • 42位:对象的地址(所以它可以支持2^42=4T内存)。

为什么有2个mark标记? 每一个GC周期开始时,会交换使用的标记位,使上次GC周期中修正的已标记状态失效,所有引用都变成未标记。 GC周期1:使用mark0, 则周期结束所有引用mark标记都会成为01。 GC周期2:使用mark1, 则期待的mark标记10,所有引用都能被重新标记。 通过对配置ZGC后对象指针分析我们可知,对象指针必须是64位,那么ZGC就无法支持32位操作系统,同样的也就无法支持压缩指针了(CompressedOops,压缩指针也是32位)。

颜色指针的三大优势:

  1. 一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理,这使得理论上只要还有一个空闲Region,ZGC就能完成收集。

  2. 颜色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,ZGC只使用了读屏障。

  3. 颜色指针具备强大的扩展性,它可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。

ZGC运作过程

ZGC的运作过程大致可划分为以下四个大的阶段: 深入理解JVM虚拟机  第22张

  • 并发标记(Concurrent Mark):与G1一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记(Mark Start)和最终标记(Mark End)也会出现短暂的停顿,与G1不同的是, ZGC的标记是在指针上而不是在对象上进行的, 标记阶段会更新染色指针中的Marked 0、 Marked 1标志位。

  • 并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。

  • 并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障)所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。

  • 并发重映射(Concurrent Remap):Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。

ZGC存在的问题: &ensp;&ensp;ZGC最大的问题是浮动垃圾。ZGC的停顿时间是在10ms以下,但是ZGC的执行时间还是远远大于这个时间的。假如ZGC全过程需要执行10分钟,在这个期间由于对象分配速率很高,将创建大量的新对象,这些对象很难进入当次GC,所以只能在下次GC的时候进行回收,这些只能等到下次GC才能回收的对象就是浮动垃圾。 ZGC没有分代概念,每次都需要进行全堆扫描,导致一些“朝生夕死”的对象没能及时的被回收。

解决方案: 目前唯一的办法是增大堆的容量,使得程序得到更多的喘息时间,但是这个也是一个治标不治本的方案。如果需要从根本上解决这个问题,还是需要引入分代收集,让新生对象都在一个专门的区域中创建,然后专门针对这个区域进行更频繁、更快的收集。

如何选择垃圾收集器

  1. 优先调整堆的大小让服务器自己来选择

  2. 如果内存小于100M,使用串行收集器

  3. 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择

  4. 如果允许停顿时间超过1秒,选择并行或者JVM自己选

  5. 如果响应时间最重要,并且不能超过1秒,使用并发收集器

  6. 4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC 深入理解JVM虚拟机  第23张 JDK 1.8默认使用 Parallel(年轻代和老年代都是) JDK 1.9默认使用 G1

安全点与安全区域

安全点: 就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是确定的,这样JVM就可以安全的进行一些操作,比如GC等,所以GC不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。这些特定的安全点位置主要有以下几种:

  1. 方法返回之前

  2. 调用某个方法之后

  3. 抛出异常的位置

  4. 循环的末尾 大体实现思想是当垃圾收集需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标志位, 各个线程执行过程 时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。 轮询标志的地方和 安全点是重合的。 安全区域又是什么? Safe Point 是对正在执行的线程设定的。 如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。 因此 JVM 引入了 Safe Region。 Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。

JVM调优工具

jdk自带命令

使用jps查看应用进程。

jmap

此命令可以查看内存信息,实例个数以及内存大小

  1. 查看实例信息

    jmap -histo [进程id] > ./log.txt


    查看log.txt 深入理解JVM虚拟机  第24张

  2. 查看堆信息

    jmap -heap [进程id]


    深入理解JVM虚拟机  第25张

  3. 堆内存dump

    jmap ‐dump:format=b,file=eureka.hprof 14660


    深入理解JVM虚拟机  第26张 也可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)

  4. -XX:+HeapDumpOnOutOfMemoryError

  5. -XX:HeapDumpPath=./ (路径)

可以用jvisualvm命令工具导入该dump文件分析 深入理解JVM虚拟机  第27张

jstack

  1. 查找死锁线程

    jstack [进程id]


    深入理解JVM虚拟机  第28张

  2. 找出占用cpu最高的线程堆栈信息 (1)使用命令top -p

    ,显示你的java进程的内存情况,pid是你的java进程号,比如19663 深入理解JVM虚拟机  第29张 (2)按H,获取每个线程的内存情况 深入理解JVM虚拟机  第30张 (3)找到内存和cpu占用最高的线程tid,比如19664,转为十六进制得到 0x4cd0,此为线程id的十六进制表示。 (5)执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调用方法 深入理解JVM虚拟机  第31张 (6)查看对应的堆栈信息找出可能存在问题的代码

jinfo

查看正在运行的Java应用程序的扩展参数。 查看jvm的参数: 深入理解JVM虚拟机  第32张查看java系统参数: 深入理解JVM虚拟机  第33张

jstat

jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。命令的格式如下:

jstat [-命令选项] [vmid] [间隔时间(毫秒)] [查询次数]


注意:使用的jdk版本是jdk8

  1. 垃圾回收统计 jstat -gc pid 最常用,可以评估程序内存使用及GC压力整体情况 深入理解JVM虚拟机  第34张

  2. 查看堆内存统计

    jstat -gccapacity 13998


    深入理解JVM虚拟机  第35张 其它: 深入理解JVM虚拟机  第36张

    JVM运行情况预估

    用 jstat gc -pid 命令可以计算出如下一些关键数据,有了这些数据就可以采用之前介绍过的优化思路,先给自己的系统设置一些初始性的 JVM参数,比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。 年轻代对象增长的速率 可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。

Young GC的触发频率和每次耗时 知道年轻代对象增长速率我们就能推根据eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC 公式算出,根据结果我们大概就能知道系统大概多久会因为Young GC的执行而卡顿多久。

每次Young GC后有多少对象存活和进入老年代 这个因为之前已经大概知道Young GC的频率,假设是每5分钟一次,那么可以执行命令 jstat -gc pid 300000 10 ,观察每次结果eden, survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次 Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率。

Full GC的触发频率和每次耗时 知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。

优化思路其实简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年 代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。

使用Arthas调优

Arthas 是 Alibaba 在 2018 年 9 月开源的 Java 诊断工具。支持 JDK6+, 采用命令行交互模式,可以方便的定位和诊断 线上程序运行问题。
Arthas使用:

# github下载arthas
wget https://alibaba.github.io/arthas/arthas‐boot.jar
# 或者 Gitee 下载
wget https://arthas.gitee.io/arthas‐boot.jar


用java -jar运行即可,可以识别机器上所有Java进程(我们这里之前已经运行了一个Arthas测试程序,代码见下方) 输入进程号,进入进程操作: 深入理解JVM虚拟机  第37张输入dashboard可以查看整个进程的运行情况,线程、内存、GC、运行环境信息: 深入理解JVM虚拟机  第38张输入thread可以查看线程详细情况 深入理解JVM虚拟机  第39张输入 thread加上线程ID 可以查看线程堆栈深入理解JVM虚拟机  第40张输入 thread -b 可以查看线程死锁 深入理解JVM虚拟机  第41张 输入 jad加类的全名 可以反编译,这样可以方便我们查看线上代码是否是正确的版本 深入理解JVM虚拟机  第42张 使用 ognl 命令可以查看线上系统变量的值,甚至可以修改变量的值 深入理解JVM虚拟机  第43张深入理解JVM虚拟机  第44张

原文:https://juejin.cn/post/7101970284957138975
打赏
海报

本文转载自互联网,旨在分享有价值的内容,文章如有侵权请联系删除,部分文章如未署名作者来源请联系我们及时备注,感谢您的支持。

转载请注明本文地址:https://www.shouxicto.com/article/5539.html

相关推荐

发布评论

ainiaobaibaibaibaobaobeishangbishibizuichiguachijingchongjingdahaqiandaliandangaodw_dogedw_erhadw_miaodw_tuzidw_xiongmaodw_zhutouganbeigeiliguiguolaiguzhanghahahahashoushihaixiuhanheixianhenghorse2huaixiaohuatonghuaxinhufenjiayoujiyankeaikeliankouzhaokukuloukunkuxiaolandelinileimuliwulxhainiolxhlikelxhqiuguanzhulxhtouxiaolxhwahahalxhzanningwennonuokpinganqianqiaoqinqinquantouruoshayanshengbingshiwangshuaishuijiaosikaostar0star2star3taikaixintanshoutianpingtouxiaotuwabiweifengweiquweiwuweixiaowenhaowoshouwuxiangjixianhuaxiaoerbuyuxiaokuxiaoxinxinxinxinsuixixixuyeyinxianyinyueyouhenghengyuebingyueliangyunzanzhajizhongguozanzhoumazhuakuangzuohenghengzuoyi
支付宝
微信
赞助本站