4

JVM性能调优实战笔记2

 2 years ago
source link: https://codeshellme.github.io/2022/03/jvm2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

公号:码农充电站pro

主页:https://codeshellme.github.io

6,线程私有空间-TLAB

7,逃逸分析

对于没有发生逃逸的对象,可将其内存分配在栈上,从而减少堆的使用

代码分析:

结论:开发中能使用局部变量的,就不要在方法外定义。

9,方法区

方法区(又叫非堆)是一块独立于堆的内存空间。

  • 其大小可设置
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,JVM 会抛出内存溢出错误:
    • OutOfMemoryError:PermGen space(Jdk7 及之前)
    • OutOfMemoryError:Metaspace (Jdk8 及之后)

结合代码看:

1,设置方法区的大小

方法区的大小不必是固定的,jvm 可以根据应用的需要动态调整。

JDK7 及以前:

JDK8 及以后:

2,方法区的内部结构

方法区主要存储的内容有:

  • 类型信息(class,interface,enum,annotation)
    • 域信息:域名称、域类型、域修饰符、域的声明顺序
    • 方法信息:方法名称、返回类型、参数信息、方法修饰符、方法声明顺序等
  • 即时编译器编译后的代码缓存

3,运行时常量池

常量池中的数据类型包括:

关于运行时常量池:

  • 运行时常量池是方法区的一部分
  • 常量池表是 Class 文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池
  • JVM 为每个已加载的类型都维护一个常量池,池中的数据项如数组项一样,通过索引访问
  • 运行时常量池类似传统编程语言中的符号表,但是它所包含的数据比符号表更加丰富

10,String 的基本特性

  • String 是 final 的,不可被继承
  • String 内部:
    • jdk8 及之前内部是 char[],字符数组
    • jdk9 及之后内部是 byte[],字节数组,更加节省空间
  • String 的两种声明方式:
    • String a = "123"; 存储在字符串常量池
      • 常量池是堆的一部分
      • 常量池中不会有重复的字符串
      • 常量池中的字符串是不可变
      • 常量池是一个固定大小的 Hashtable
        • jdk6 中池的默认大小是 1009
        • jdk7 中池的默认大小是 60013,最小值是 1009
        • 可通过 -XX:StringTableSize 设置池的大小
      • String s = new String("xxx"); s.intern(); 方法:
        • 如果常量池中有 s 对应的字符串,则将 s 指向池中的串
        • 如果常量池中没有 s 对应的字符串,则先在池中生成串,再将 s 指向池中的串
    • String b = new String("456"); 存储在
String s1 = "Runoob"; // String 直接创建
String s2 = "Runoob"; // String 直接创建
String s3 = s1; // 相同引用
String s4 = new String("Runoob"); // String 对象创建
String s5 = new String("Runoob"); // String 对象创建

1,常量池

常量池不只有字符串常量池,也有其它基本数据类型的常量池。

Java 语言中有 8 种基本数据类型,和比较特殊的类型 String。为了使这些内容在运行时更快,更节省内存,都提供了常量池的概念。

运行时常量池的空间比较小,所以 StringTable 移到了堆中。

2,字符串的拼接

  • 常量与常量的拼接,结果在常量池,原理是编译期优化
  • 只要其中有一个是变量,结果就在堆中。变量拼接的原理是 StringBuilder

11,垃圾回收

什么是垃圾?

  • 没有任何指针指向的对象

Java 垃圾回收的区域:

Java 堆(Heap)是垃圾回收的重点区域,从回收频率上讲:

  • 年轻代,频繁收集
  • 老年代,较少收集
  • 永久代 / 元空间,基本不收集

12,垃圾回收算法

垃圾回收有两个阶段:

  • 标记阶段(确认哪些是垃圾),有两种算法:
    • 引用计数算法:对每个对象保存一个整型的引用计数属性,用于记录对象被引用的次数(Python 使用)
      • 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性
      • 缺点:无法处理循环引用的情况(致命缺点,导致该算法无法使用)
    • 可达性分析算法:也叫追踪性垃圾收集(Java,C# 使用)
      • 思路:以根对象(GC Roots)为起始点,从上到下搜索每个对象是否可达
      • 内存中的存活对象都会被根对象直接或间接连接着,搜索所走过的路径称为引用链,如果目标对象没有与任何引用链相连,则是不可达的
      • 优点:实现简单,执行高效,能处理循环引用的情况
      • Java 语音中可以作为 GC Roots 的对象包括以下几类
        • 虚拟机栈中引用的对象
        • 本地方法栈内 JNI 引用的对象
        • 方法区中静态属性引用的对象
        • 方法区中常量引用的对象
        • 所有被同步锁 synchronized 持有的对象
        • Java 虚拟机内部的引用
  • 清除阶段(清理垃圾),有三种算法:
    • 标记-清除算法:该算法在 1960 年提出并应用于 Lisp 语言
      • 标记:从引用根节点开始遍历,标记所有被引用的对象(可达对象)
      • 清除:对所有不可达的对象进行回收
      • 缺点:效率不高,需要遍历两次,标记一次,清除一次
    • 复制算法:将内存空间分为两块,每次只使用其中一块;在垃圾回收时,将正在使用的内存中的存活对象复制到未被使用的内存块中,然后清除正在使用的内存块中的所有对象,然后交换两个内存的角色
      • 优点:没有标记和清除过程,实现简单,运行高效,不会出现内存碎片
      • 缺点:需要两倍的内存空间
      • 比较适用于垃圾比较多的情况(新生代中的幸存者区使用的就是该算法)
    • 标记-压缩(整理)算法:其最终效果等同于标记-清除算法执行后,再进行一次内存碎片整理
      • 标记-清除算法多了一个整理内存碎片的阶段
      • 复制算法多了一个标记的阶段
    • 分代收集算法:不同的对象的生命周期是不一样的,不同生命周期的对象可以使用不同的收集方式,以便可以提高效率
      • 比如 Java 堆分为新生代和老年代
      • 分代的思想被现在的虚拟机广泛使用,几乎所有的垃圾收集器都区分新生代和老年代
    • 增量收集算法:如果一次性将所有的垃圾进行处理,需要造成系统长时间停顿,那就让垃圾收集线程和应用程序线程交替执行。每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成
      • 其基础仍是传统的标记-清除和复制算法
      • 缺点:线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量下降
    • 分区算法
      • 分代算法按照对象的生命周期长短划分为两部分,分区算法将整个堆空间划分为不同的小区间
      • 每个小区间都独立使用,独立回收

使用 MAT 查看 GC Roots:

GC Roots 如下:

三种清除阶段算法的对比:

标记清除算法 标记压缩算法 复制算法
速度 中等 最慢 最快
空间开销 2倍空间
移动对象

没有最好的算法,只有最合适的算法。 实际的 GC 要复杂的多,大部分都是复合算法。

13,Java 中的几种引用

我们希望能描述这样一类对象:

  • 当内存空间足够时,则能留在内存中
  • 当内存空间短缺是,则可回收它们

在 JDK1.2 后,Java 将引用分为四种:

1,强引用-永远不回收

2,软引用-内存不够即回收

发生 OOM 的时候会回收软引用

3,弱引用-GC 即回收

4,虚引用-对象回收跟踪

14,垃圾回收器

1,垃圾回收器的分类

7 款经典的垃圾回收器:

  • 串行回收器:只使用一个CPU,且在进行垃圾回收时,必须暂停其它所有工作线程(Stop-The-World)

    • Serial:最基本、最悠久的GC
      • JDK1.3 之前新生代唯一选择;HotSpot 中 Client 模式下默认新生代 GC
      • 采用复制算法,串行回收
      • -XX:+UseSerialGC 参数可指定串行收集器
    • Serial Old
      • HotSpot 中 Client 模式下默认老年代 GC
      • 采用标记-压缩算法,串行回收
      • 在 Server 模式下有两种用途:
        • 与新生代 Parallel Scavenge 配合使用
        • 作为老年代 CMS 收集器的后备方案
  • 并行回收器:

    • ParNew:Serial 的多线程版本,只处理新生代
      • 采用复制算法,串行回收
      • 参数 -XX:+UseParNewGC 年轻代使用并行收集器,不影响老年代
        • -XX:ParallelGCThreads 设置并行线程数,默认与CPU 数相同
    • Parallel Scavenge(JDK8 默认)主打吞吐量,只处理新生代
      • 采用复制算法,并行回收
      • 自适应调节策略(自动调整) 也是与 ParNew 的一个重要区别
        • 参数 -XX:+UseAdaptiveSizePolicy 默认开启
        • 在这种模式下,年轻代的大小,Eden 和 Survivor 的比例、晋升老年代的年龄等参数会被自动调整
      • 相关参数:
        • -XX:+UseParallelGC:与下面的一个参数互相激活
        • -XX:+UseParallelOldGC
        • -XX:+ParallelGCThreads:设置并行线程数;当 CPU 数小于 8 时,默认为 CPU 数,当CPU 数大于 8 时,默认为 3+[5*CPU_Count]/8
    • Parallel Old(JDK8 默认)
      • 采用标记-压缩算法,并行回收
  • 并发回收器:

    • CMS(Concurrent-Mark-Sweep):老年代GC,主打低延迟
      • 在 JDK1.5 时,HotSpot 推出的,强交互应用中,认为是划时代意义的 GC
      • 是 HotSpot 中第一款并发 GC,第一次实现了让 GC线程与用户线程同时工作
      • 采用标记-清除算法
      • 参数 -XX:+UseConcMarkSweepGC (老年代)指定 CMS GC,该参数会将 -XX:+UseParNewGC (新生代)打开
      • 已被 JDK9 废弃,已被 JDK14 删除
    • G1(JDK9 默认)区域分代化 GC,其目标是在延迟可控的情况下获得尽可能高的吞吐量
      • G1 把堆内存分割为很多不同的区域,避免在整个堆中进行全区域的垃圾收集,优先回收垃圾最大量的区域
      • 参数 -XX:+UseG1GC
      • -XX:G1HeapRegionSize 设置每个 Region 的大小,值是 2 的幂,范围是 1MB ~ 32MB 之间,默认是堆内存的 1 /2000
      • -XX:MaxGCPauseMillis 设置期望达到的最大 GC 停顿时间指标,默认 200ms
    • ZGC:未来 GC,目前处于实验阶段

7 款垃圾回收器总结:

2,查看正在使用的 GC

两种方式:

  • -XX:+PrintCommandLineFlags
  • jinfo -flag 相关垃圾回收器参数 进程ID

JDK8UseParallelGC、UseParallelOldGC JDK9UseG1GC

15,Class 文件结构

任何一个 Class 文件都对应着唯一一个类或接口的定义信息。Class 文件的结构并不是一成不变的,随着 Java 虚拟机的不断发展,总是不可避免的会对 Class 文件结构做出调整,但其基本结构和框架是稳定的。

Class 文件的总体结构如下:

  • 魔数:cafebabe
  • Class 文件版本
  • 类索引、父类索引、接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

16,类的加载

Java 中的数据类型分为基本数据类型和引用数据类型,基本数据类型由虚拟机预先定义,引用数据类型则需要类的加载。

按照 Java 虚拟机规范,从 class 文件到加载到内存中的类,再到类卸载出内存为止,它的生命周期包括 7 个阶段:

初始化之后的类会放在方法区

17,类的加载器

ClassLoader 是 Java 的核心最贱,所有的 Class 都是由 ClassLoader 进行加载的,ClassLoader 通过各种方式将 Class 信息的二进制数据流读入 JVM 内部。

类的唯一性:

  • 任意一个类,都需要由加载它的类加载器和这个类本身一起确认其在 Java 虚拟机中的唯一性。
  • 每一个类加载器,拥有一个独立的类名称空间,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。
  • 否则,即使这两个类源自同一个 Class 文件,被同一个虚拟机加载,只要加载它的类加载器不同,那这两个类就必定是不同的。

18,双亲委派机制

类加载器把类加载到 Java 虚拟机中,从 JDK1.2 开始,类的加载过程采用双亲委派机制,更好地保证 Java 平台的安全。

  • 确保一个类的全局唯一性,避免类的重复加载
    • Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类时,就不需子类加载器再加载一次
  • 保护程序安全,防止核心 API 被篡改

定义:当一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,如果父类加载器可以完成类加载任务,就返回成功。只有父类加载器无法完成此加载任务时,才自己去加载。

该机制规定了类加载的顺序:

  • 引导类加载器先加载,若加载不到,由扩展类加载器加载
  • 若还加载不到,才会由系统类加载器自定义类加载器进行加载

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK