JavaJVM学习笔记

2次阅读

共计 4042 个字符,预计需要花费 11 分钟才能阅读完成。

最美的不是下雨天,是曾与你躲过雨的屋檐————《不能说的秘密》

JVM 双亲委派机制


好处:防止内存中出现了多份相同的字节码
当一个.class 文件要被加载进 JVM 的时候

  1. AppClassLoader 首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器 ExtClassLoader 去完成
  2. 当 ExtClassLoader 加载一个 class 时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给 BootStrapClassLoader 去完成
  3. 如果 BootStrapClassLoader 加载失败,会使用 ExtClassLoader 来尝试加载;若 ExtClassLoader 也加载失败,则会使用 AppClassLoader 来加载,若 AppClassLoader 还是加载失败,会抛出异常 ClassNotFoundException

当成功加载好某个类时,会将这个类缓存起来,下次使用时直接使用缓存,不会再次加载

类加载过程

加载器加载到 jvm 中,步骤:

  1. 加载, 通过类名查找并加载该类的二进制流,在堆中生成该类的 class 类对象

加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了

  1. 连接
    a. 验证,文件格式验证→元数据验证→字节码验证→符号引用验证
    b. 准备,为类变量分配内存并设置类变量初始值的阶段,这里指的事类的静态变量,不包括实例变量
    c. 解析,将常量池的符号引用代替为直接引用

    JIT 即时编辑器

    JVM 解析过程:

    对热点代码进行重新编译优化,生成机器代码,让 CPU 直接执行,非热点代码直接解析

    热点代码: 多次调用的方法,多次执行的循环体
    使用 热点探测 来检测是否为热点代码:采样和计数器,jvm 使用的是计数器,当计数器超过阈值溢出了,就会触发 JIT 编译

  2. 初始化


图片来源:https://segmentfault.com/a/11…

JVM 内存结构

基于 JDK1.8

  1. 堆:线程共享,内存中最大的一块,存放对象实例,基本所有对象实例以及数组都在这里分配内存,是垃圾收集器的主要区域,现在收集器基本采用分代垃圾收集算法,所有堆又分为新生代和老年代
  2. 虚拟机栈:线程私有,生命周期和线程相同,描述的是 Java 方法执行的内存结构,每次方法调用的数据通过栈传递,所以每个方法被执行时都会同时创建一个栈帧
  3. 本地方法栈:线程私有,与虚拟机栈类似,区别是虚拟机栈为 Java 方法执行服务,本机方法栈为虚拟机使用到的 Native 方法服务
  4. 程序计数器:线程私有,是当前线程所执行的字节码的行号指示器,因此每条线程都有一个独立的程序计数器
  5. 方法区:线程共享的内存区域,用于储存被虚拟机加载的类信息、常量、静态变量等数据

常量池:

主要针对 String
https://tech.meituan.com/2014…

JVM 垃圾回收——GC

JVM 垃圾回收主要针对的是内存结构中的堆

JVM 回收的是 "垃圾",即是程序不再使用了,不再需要了,因此回收的第一步需要判断哪些是垃圾,即对象死亡
常用有:** 引用计数法 ** 和 ** 可达性分析算法 **

引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的

可达性分析算法:主流的 JVM 采用的是这种方式,这个算法的基本思想就是通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。能够做 GC Roots 的有:

- 虚拟机栈中引用的对象(局部变量)
- 方法去中类静态属性引用的对象(static 对象实例)
- 方法区中常量引用的对象(常量实例)
- 本地方法引用的对象

引用:强引用、软引用、弱引用和虚引用

  • 强引用:最普遍的引用,如 Object object = new Object()就是强引用,即使内存不足也不会回收这类引用,抛出 OutOfMemory 的异常
  • 软引用:软引用引用的对象,在垃圾回收时,如果发现内存资源不足,即使被引用了依然会被回收,但是在内存资源充足的情况下是不会回收的
  • 弱引用:弱引用引用的对象,在垃圾回收时,不管内存资源充不充足,都会被回收
  • 虚引用:形同虚设的引用,在任何时候都可能被垃圾回收

不可达的对象并非一定回收
如果一个不可达的对象覆盖了 finalize 方法,并且这个方法从来没被执行过,垃圾回收器就不会马上回收,而是放在一个队列,进行二次标记,但在下一次回收前,重新建立引用,便不会被回收。

** 不推荐在 finalize 方法中去释放资源,因为它什么时候会被调用是不确定的。**

垃圾回收算法

  • 标记 - 清除算法
  • 复制算法
  • 标记 - 整理算法
  • 分代收集算法

分代收集算法
将 java 堆分为新生代和老年代,根据各个年代的特点选择合适的垃圾收集算法

  • 新生代(Minor GC):复制算法,所以分为 Eden 区、Survivor1、Survivor2 三个区
  • 老年代(Full GC):标记 - 清除算法 或标记 - 整理算法

垃圾收集器

算法只是方法论,收集器才是干活的

  • Serial 收集器
  • ParNew 收集器
  • Parallel Scavenge 收集器
  • Serial Old 收集器
  • Parallel Old 收集器
  • CMS 收集器
  • G1 收集器

根据具体应用场景选择适合自己的垃圾收集器

什么情况下触发垃圾回收

  • Minor GC:当新对象生成,并且在 Eden 申请空间失败时,就会触发 Minor GC,对 Eden 区域进行 GC,清除非存活对象,并且把尚且存活的对象移动到 Survivor 区。然后整理 Survivor 的两个区。这种方式的 GC 是对年轻代的 Eden 区进行,不会影响到年老代。因为大部分对象都是从 Eden 区开始的,同时 Eden 区不会分配的很大,所以 Eden 区的 GC 会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使 Eden 去能尽快空闲出来。
  • Full GC:对整个堆进行整理,所以比 Minor GC 要慢,因此应该尽可能减少 Full GC 的次数。触发 Full GC 的原因有几个:

    • 老年代被写满
    • 持久带被写满
    • System.gc()被显示调用
    • 上一次 GC 之后 Heap 的各域分配策略动态变化

JVM 参数与调优

在对 JVM 调优的过程中,很大一部分工作就是对于 FullGC 的调节

  • 年轻代:

    1. 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
    2. 吞吐量优先的应用:尽可能的设置大,可能到达 Gbit 的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合 8CPU 以上的应用。
  • 老年代:

    1. 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

      1. 并发垃圾收集信息
      2. 持久代并发收集次数
      3. 传统 GC 信息
      4. 花在年轻代和年老代回收上的时间比例
      5. 减少年轻代和年老代花费的时间,一般会提高应用的效率
    2. 吞吐量优先的应用
      一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
** 较小堆引起的碎片问题 **

因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

1. -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次 Full GC 后,对年老代进行压缩

常见配置汇总

堆设置

-Xms: 初始堆大小
-Xmx: 最大堆大小
-XX:NewSize=n: 设置年轻代大小
-XX:NewRatio=n: 设置年轻代和年老代的比值。如: 为 3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代年老代和的 1 /4
-XX:SurvivorRatio=n: 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。如:3,表示 Eden:Survivor=3:2,一个 Survivor 区占整个年轻代的 1 /5
-XX:MaxPermSize=n: 设置持久代大小

收集器设置

-XX:+UseSerialGC: 设置串行收集器
-XX:+UseParallelGC: 设置并行收集器
-XX:+UseParalledlOldGC: 设置并行年老代收集器
-XX:+UseConcMarkSweepGC: 设置并发收集器

垃圾回收统计信息

-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

并行收集器设置

-XX:ParallelGCThreads=n: 设置并行收集器收集时使用的 CPU 数。并行收集线程数。-XX:MaxGCPauseMillis=n: 设置并行收集最大暂停时间
-XX:GCTimeRatio=n: 设置垃圾回收时间占程序运行时间的百分比。公式为 1 /(1+n)

并发收集器设置

-XX:+CMSIncrementalMode: 设置为增量模式。适用于单 CPU 情况。-XX:ParallelGCThreads=n: 设置并发收集器年轻代收集方式为并行收集时,使用的 CPU 数。并行收集线程数。

学习链接:

https://segmentfault.com/a/11…
https://snailclimb.top/JavaGu…
感谢两位大佬的文章,受益匪浅

正文完
 0