JVM调优实战

本文详细介绍了JVM的基础概念,包括JVM、JRE和JDK的关系,class文件结构,内存加载过程,垃圾回收机制(如引用计数法、可达性分析、GC算法和逃逸分析),以及Java内存管理和调优技巧,如CPU飙高处理方法。
摘要由CSDN通过智能技术生成

1、jvm入门

jvm、jre、jdk他们的关系如下图

2、class文件结构

2.1、class文件的格式,

类名.class文件是二进制字节码文件,用于保存 Java类的二进制编码以及Class对象,每一个 Java类都有一个解释该类特征的 Class对象。

格式大体如下图所示:

数据类型

u1

1个字节,无符号类型

u2

2个字节,无符号类型

u4

4个字节,无符号类型

u8

8个字节,无符号类型

_info

表类型, _info的来源是hotspot源码中的写法

classFile的构成

详细信息参考:ClassFileFormat_查看16进制文件的工具-CSDN博客

2.2、JVM的8个原子指令

lock(锁定)、read(读取)、load(载入)、use(使用)

assign(赋值)、store(存储)、write(写入)、unlock(解锁)

3、内存加载过程

编译后的class文件是怎么加载到内存的尼?

大概流程如下:

3.1、装载

        查找和导入class文件

3.2、链接

        执行检验、准备、解析;其中解析步骤可选

        3.2.1、检验:检查载入class文件数据的准确性

                检查文件开头格式等;

        3.2.2、准备:给类的静态变量分配存储空间,赋默认值

                如:static int i=8;这个时候给i赋默认值0;

        3.2.3、解析: 将符号引用转换成直接内存地址,可以直接访问到内容       

3.3、初始化

        将类的静态变量、静态代码块执行初始化工作

3.4、类加载器

3.4.1、Bootstrap

        最顶层的加载器,他是用来加载lib里jdk最核心的内容,如:rt.jar、charset.jar等核心类

3.4.2、Extension

        加载拓展包里的各种各样文件,这些在jdk安装目录jre/lib/ext下

3.4.3、Application

        加载classpath下所指定的内容

3.4.5、Custom

        自定义加载器

3.4.5、双亲委派机制

以上的加载机制逐层向parent提交,parent看是自己这层该加载的,就返回,如果没有返回就自己记载,这种加载机制就叫双亲委派,先给parent优先。

双亲委派能避免,如:自定义java.lang.String类就不行,保证核心代码的安全性;

4、运行时内存结构

  • JDK1.8前:

  • JDK1.8后:

5、GC

5.1、如何判断一个对象没有引用

5.1.1、引用计数法:

对象中添加一个引用计数器,每当有一个地方引用计数器就增加1,引用失效就减少1,计数器为0就不可用;缺点就在于无法处理对象直接相互引用的问题,因为相互引用以后无法使计数器为0,所以无法回收;

5.1.2、可达性分析算法

也就是我们常说的GC Root,,当一个对象没有与任何引用链相连的时候,就可以对该对象进行回收,下面是Java中GC Root对象使用的几个地方

5.2、什么时候触发GC

当内存空间不足的时候就需要触发GC,GC回收的时候采用的是分代收集的算法,

主要分为年轻代和老年代,接下来我们简单介绍一下这2种方式:

   年轻代:当一个对象被创建的时候,内存分配首先分配在年轻代,大部分对象创建以后都不再使用,对象很快变得不可达,就是对象无用,由于垃圾是被年轻代清理掉的,所以被叫做Minor GC或者Young GC。

   老年代:对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。

 1.当一个对象被创建的时候(new)首先会在年轻代的Eden区被创建,直到当GC的时候,根据可达性算法,看一个对象是否消亡,没有消亡的对象会被放入年轻带的Survivor区,消亡的直接被Minor GC Kill掉;

   2.进入到Survivor区的对象也不是安全的,当下一次Minor GC来的时候还是会检查Enden和Survivor存放对象区域中对象是否存活,存活放入另外一块Survivor区域;

   3.当2个Survivor区切换几次以后,会直接进入老年代,当然进入到老年代也不是安全的,当老年代内存空间不足的时候,会触发Major GC,已经消亡的依然还是被Kill掉

5.3、GC算法

5.3.1、标记清除法

两次扫描第一次找到有用的,第二次找到没用的进行清理、容易产生碎片

存活对象比较多时效率较高

5.3.2、复制算法

适用于存活对象少的情况,扫描一次,效率提高,没有碎片空间浪费

5.3.3、标记压缩算法

扫描2次、需要移动对象、效率较低

不会产生碎片,方便对象分配、不会产生内存减半

5.4、垃圾回收器

​​​​​​​

  • 并发(concurrent) vs 并行(parallel)
    1. 并行是同时进行(多 CPU)
    2. 并发可交替
  • Minor GC vs Major GC vs Full GC
    • Minor GC:只回收新生代
    • Major GC:只回收永久代
    • Full GC: 回收整个堆。相当于 Minor GC + Major GC

     jvm默认是Parallel Scavenge和Parallel old(PS+PO)

  1. serial。单线程,简单高效。复制算法
  2. serial old。serial 的永久代版本。采用标记整理算法。
  3. parallel Scavenge。 复制算法、多线程、并行。但侧重吞吐量,拥有自适应调节的能力。适合用在后台不需要太多用户交互的地方。
  4. parallel old。parallel Scavenge 的老年代版本,采用标记整理算法。与 parallel scavenge 搭配可以用在注重吞吐量及 CPU 资源敏感的地方。
  5. PerNew。3的新版本做了些增强与CMS适配。
  6. CMS(concurrent mark sweep)。并发低停顿,使用标记清理算法。非常优秀的一款收集器,但还是有几个缺点:
    1. ​​​​​​​对 CPU 资源敏感,当其小于数量小于 4 个是可能会对用户程序有较大影响。默认启动回收线程数 = (CPU 数 + 3)/ 4
    2. 无法处理浮动垃圾。浮动垃圾:在垃圾回收期间生成的垃圾
    3. 回收后会留有大量的空间碎片
  7. G1.逻辑分区,物理不分区
  8.  ZGC
  9. Java 8及之前版本:Parallel GC是默认的垃圾回收器。它通过多线程并行地进行垃圾收集,适用于多核处理器,并且通常用于处理大型堆内存。
  10. Java 9及以后版本:G1垃圾回收器成为了默认的垃圾回收器。G1垃圾回收器是一种面向服务端应用的垃圾回收器,它采用了分代的垃圾回收策略,可以更加灵活地管理堆内存,并且能够在不牺牲太多吞吐量的情况下实现更加可预测的垃圾回收。

5.5、GC配置参数

java启动参数共分为三类;
其一是标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
其二是非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;
其三是非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;

  1. -Xms:设置JVM启动时的初始堆内存大小。例如,-Xms512m

  2. -Xmx:设置JVM最大可用堆内存大小。例如,-Xmx1024m

  3. -Xmn:设置JVM新生代内存大小。例如,-Xmn1024m

  4. -Xss:设置每个线程的堆栈大小。例如,-Xss256k

  5. -XX:NewSize:设置新生代的默认初始大小。例如,-XX:NewSize=256m

  6. -XX:MaxNewSize:设置新生代的最大大小。例如,-XX:MaxNewSize=512m

  7. -XX:PermSize:设置永久代(PermGen space)的初始大小。例如,-XX:PermSize=128m

  8. -XX:MaxPermSize:设置永久代的最大大小。例如,-XX:MaxPermSize=256m

  9. -XX:+UseG1GC:启用G1垃圾收集器。

  10. -XX:MaxGCPauseMillis:设置G1收集器的最大暂停时间。例如,-XX:MaxGCPauseMillis=200

  11. -XX:+PrintGCDetails:打印详细的GC日志信息。

    1. PrintGCTimeStamps打印GC时间

    2. PrintGCCauses打印GC产生的原因

  12. -XX:-DoEscapeAnalysis 去掉逃逸分析

  13. -XX:-UseTLAB 去掉TLAB

  14. -XX:MaxTenuringThreshold 指定次数YGC,达到这个次数进入老年代,

    1. Parallel Scavenge 默认15 ;CMS默认6 ;G1默认15

X是分表参数;m是meomory;s是最小值;x是最大值;n是new;s是stack;

:后面+是用;后面-禁用

5.6、TLAB

线程本地分配(Thread Local Allocation Buffer):占用eden 默认1%,这块空间线程独有;

优先栈上分配,分配不了在进行本地分配;

5.7、逃逸分析

在Java中每一个对象都有一定的作用域,理论上,一个对象在一块代码中构造,那么也应该在这块代码中被回收,但是实际上,我们经常会让一个对象存活更长的时间,超过定义它的代码块,这就好比一个人逃出了生他养他的地方,我们将这种现象称为逃逸。

三种逃逸现象

栈上分配:

众所周知,Java中对象时分配在堆上的,在初始化时,会在堆上分配一块空间,当这个对象不再使用时,会在之后发生垃圾回收时被回收,这是一个Java对象正常的生命周期。但是当能够明确对象不会发生逃逸时,就可以对这个对象做一个优化,不将其分配到堆上,而是直接分配到栈上,这样在方法结束时,这个对象就会随着方法的出栈而销毁,这样就可以将少垃圾回收的压力。

同步消除

在多线程中,对于一个变量操作进行同步操作是效率很低的,当我们确定一个对象不会发生逃逸时,那么就没有必要对这个对象进行同步操作,所以如果代码中有对这种变量操作的同步操作,JVM将会取消同步,从而提升性能。

标量替换

标量指的是没有办法再分解为更小的数据的类型,即Java中的基本类型,我们平时定义的类都属于聚合量。标量替换即是将一个聚合量拆成多个标量来替换,即用一些基本类型来代替一个对象。如果明确对象不会发生逃逸,并且可以进行标量替换的话,那么就可以不创建这个对象,而是直接使用基本类型来代替,这样也就可以节省创建和销毁对象的开销。

虽然基于逃逸技术的优化能够提升程序运行时的性能,但是在实际生产中,对象逃逸的分析默认是不开启的。这是因为分析一个对象是否会发生逃逸消耗比较大,所以,开启逃逸分析并进行这些优化之后得到的效果,并不一定就比不进行优化更好。如果确定开启逃逸分析效率更好,那么可以使用参数-XX:+DoEscapeAnalysis来开启逃逸分析

6.0、引用类型-强、软、弱、虚

强引用

强引用 是最常见的引用, 首先用 new 关键字创建对象的时候。这个对象就是一个强引用也就是默认的引用类型。 只要强引用的对象是可触及的, 那么他就不会被回收!如果强引用对象超过了他的作用范围或者被设置为 null 那就可以被回收了。只要有强引用在, 当内存不足的时候jvm就算抛出OOM也不会回收掉它!

如果对应的引用被使用了,且没有被设置为null,不会回收;

public class NormalReferenceTest {

    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize");
    }

    public static void main(String[] args) throws IOException {
        NormalReferenceTest n = new NormalReferenceTest();
        System.out.println(n);
        n=null;
        System.out.println(n);
        System.gc(); //垃圾回收
        System.in.read(); //模拟堵塞,因为回收异步的,不然看不到效果
    }
}


执行结果:
reference.M@1540e19d
null
finalize

软引用

当一个对象被一个软引用所指向的时候,只有系统内存不够用的时候,才会回收它。

应用场景:缓存

public class SoftReferenceTest {

    public static void main(String[] args) throws InterruptedException {
        SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*30]);
        System.out.println(m.get());
        System.gc();
        Thread.sleep(500);

        System.out.println(m.get());

        //启动参数中固定heap参数最大值为50m,这里模拟内存不够时 垃圾回收软引用的空间
        byte[] b = new byte[1024*1024*30];
        System.out.println(b);

        System.out.println(m.get());
    }
}

打印结果:
[B@1540e19d
[GC (System.gc()) [PSYoungGen: 2048K->592K(14848K)] 32768K->31320K(49152K), 0.0013733 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 592K->0K(14848K)] [ParOldGen: 30728K->31107K(34304K)] 31320K->31107K(49152K), [Metaspace: 3151K->3151K(1056768K)], 0.0048637 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[B@1540e19d
[GC (Allocation Failure) [PSYoungGen: 256K->0K(14848K)] 31363K->31107K(49152K), 0.0028011 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 0K->0K(14848K)] [ParOldGen: 31107K->31101K(34304K)] 31107K->31101K(49152K), [Metaspace: 3152K->3152K(1056768K)], 0.0086161 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(14848K)] 31101K->31101K(49152K), 0.0016592 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(14848K)] [ParOldGen: 31101K->363K(34304K)] 31101K->363K(49152K), [Metaspace: 3152K->3152K(1056768K)], 0.0060233 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[B@677327b6
null
Heap
 PSYoungGen      total 14848K, used 640K [0x00000007bef80000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 12800K, 5% used [0x00000007bef80000,0x00000007bf0201d8,0x00000007bfc00000)
  from space 2048K, 0% used [0x00000007bfc00000,0x00000007bfc00000,0x00000007bfe00000)
  to   space 2048K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007c0000000)
 ParOldGen       total 34304K, used 31083K [0x00000007bce00000, 0x00000007bef80000, 0x00000007bef80000)
  object space 34304K, 90% used [0x00000007bce00000,0x00000007bec5ae88,0x00000007bef80000)
 Metaspace       used 3158K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 348K, capacity 388K, committed 512K, reserved 1048576K

对应的配置:

弱引用

弱引用,只要遭到gc就会被回收,如果有强引用指向了弱引用,只要强引用消失,弱引用就会被回收

public class WeakReferenceTest {

    public static void main(String[] args) {
        WeakReferenceTest test = new WeakReferenceTest();
        WeakReference<WeakReferenceTest> m = new WeakReference<>(test);
        System.out.println(m.get());
        test = null; //只要这个强引用消失,弱引用就会被回收
        System.gc();
        System.out.println(m.get());
    }
}

打印结果:
reference.WeakReferenceTest@1540e19d
null

这里ThreadLocal就是基于弱引用实现的:

  可以看出ThreadLocal是由当前线程的thread和Map(threadLocals)实现的

当thread强引用消失了,map中的key的指向也被回收了,但有可能key指向了null,这个时候Map就会用用存在,造成内存泄漏。所以使用ThreadLocal时务必使用remove不然容易内存泄漏;

虚引用

管理堆外内存,构造方法有两个参数第二个参数必须是队列,给写JVM的人用的;如堆外内存的回收处理。

队列就是用来处理回收堆外内存逻辑的

public class PhantomReferenceTest {

    private static final List<Object> list = new LinkedList<>();

    private static final ReferenceQueue<PhantomReferenceTest> queue = new ReferenceQueue<>();

    public static void main(String[] args) {
        PhantomReference<PhantomReferenceTest> m = new PhantomReference<>(new PhantomReferenceTest(),queue);

        new Thread(()->{
           while (true){
               list.add(new byte[1024*1024]);

               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println(m.get());
           }
        }).start();

        new Thread(()->{
            while (true){
                Reference<? extends  PhantomReferenceTest> poll = queue.poll();
                if(null != poll){
                    System.out.println("虚引用对象被jvm回收了 " + poll);
                }
            }
        }).start();


    }
}

7.0、调优

cpu飙高处理

1、找出哪个进程cpu飙高

        top

2、该进程哪个线程cpu高

       top -Hp ‘pid’

3、导出该线程的堆栈

        jstack

4、对dump日志进行分析

        jconsole、jvisualvm、jprofiler

       阿里Arthas在线分析

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值