UP | HOME

JVM 知识点

Table of Contents

1 基础知识

1.1 JVM 运行内存的分类

  1. 程序计数器 当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机 字节指令地址,线程私有 注: 如果正在执行的是 Native 方法,计数器值则为空
  2. Java 线程栈 存放基本数据类型、对象的引用、方法出口等,线程私有
    • 局部变量表
    • 操作数栈
    • 动态链接
    • 方法出口地址
  3. 本地方法栈 和线程栈相似,只不过它服务于 Native 方法,线程私有
  4. Java 堆 Java 内存最大的一块,所有对象实例、数组都存放在 Java 堆,GC 回收 的地方,线程共享
  5. 元空间 存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据 等。(即永久代),回收目标主要是常量池的回收和类型的卸载,各线程共享
    • 元空间是 JDK 1.7 之前叫方法区

1.2 Java 内存堆和栈区别

  1. 栈内存用来存储基本类型的变量和对象的引用变量,堆内存用来存储 Java 中的对象, 无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中
  2. 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线 程中可见,即栈内存可以理解成线程的私有内存,堆内存中的对象对所有线程可见。 堆内存中的对象可以被所有线程访问
  3. 如果栈内存没有可用的空间存储方法调用和局部变量,JVM 会抛出 java.lang.StackOverFlowError, 如果是堆内存没有可用的空间存储生成的对象, JVM 会抛出 java.lang.OutOfMemoryError
  4. 栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满,
    • -Xms<size> set initial Java heap size
    • -Xmx<size> set maximum Java heap size
    • -Xss<size> set java thread stack size

1.3 Java 四引用

  1. 强引用 (StrongReference)强引用是使用最普遍的引用。如果一个对象具有强引 用,那垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象 来解决内存不足的问题
  2. 软引用 (SoftReference) 如果内存空间不足了,就会回收这些对象的内存。只 要垃圾回收器没有回收它,软引用可以和一个引用队列(ReferenceQueue)联合使用, 如果软引用所引用的对象被垃圾回收器回收,Java 虚拟机就会把这个软引用加入到 与之关联的引用队列中
  3. 弱引用 (WeakReference) 弱引用与软引用的区别在于: 只具有弱引用的对象拥 有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发 现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。弱引用 可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾 回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中
  4. 虚引用 (PhantomReference)虚引用在任何时候都可能被垃圾回收器回收,主要 用来跟踪对象被垃圾回收器回收的活动,被回收时会收到一个系统通知。虚引用与软 引用和弱引用的一个区别在于: 虚引用必须和引用队列 (ReferenceQueue)联合使 用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的 内存之前,把这个虚引用加入到与之关联的引用队列中

1.4 GC 回收机制

  1. Java 中对象是采用 new 或者反射的方法创建的,这些对象的创建都是在堆(Heap) 中分配的,所有对象的回收都是由 Java 虚拟机通过垃圾回收机制完成的。GC 为了 能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋 值等状 况进行监控
  2. Java 程序员不用担心内存管理,因为垃圾收集器会自动进行管理
  3. 可以调用下面的方法之一 System.gc()Runtime.getRuntime().gc() 但 JVM 可以屏蔽掉显示的垃圾回收调用

1.5 GC 标记对象的死活

  1. 引用计数法 给对象添加一个引用计数器,没当被引用的时候,计数器的值就加一。 引用失效的时候减一,当计数器的值为 0 的时候就表示改对象可以被 GC 回收了,弊 端: A->B,B->A, 那么 AB 将永远不会被回收了。也就是引用有环的情况
  2. 根搜索算法 (可达性算法) GC Roots Tracing: 通过一个叫 GC Roots 的对象作为 起点,从这些结点开始向下搜索,搜索所走过的路径称为引用链,当一个对象没有与任 何的引用链相连的时候则改对象就可以被。 GC 回收回收了 Roots 包括: Java 虚拟 机栈中引用的对象,本地方法栈中引用的对象,方法区中常量引用的对象,方法区中静 态属性引用的对象在 Java 语言里,可作为 GC Roots 的对象包括以下几种:
    • 虚拟机栈(栈帧中的本地变量表)中的引用的对象
    • 方法区中的类静态属性引用的对象
    • 方法区中的常量引用的对象
    • 本地方法栈中 JNI(即一般说的 Native 方法)的引用的对象

1.6 GC 回收算法

  1. 标记-清除法: 标记出没有用的对象,然后一个一个回收掉
    • 缺点: 标记和 清除两个过程效率不高,产生内存碎片导致需要分配较大对象时无 法找到足够的连续内存而需要触发一次 GC 操作
  2. 复制回收算法: 按照容量划分二个大小相等的内存区域,当一块用完的时候将活着 的对象复制到另一块上,然后再把已使用的内存空间一次清理掉
    • 缺点: 将内存缩小为了原来的一半
  3. 标记-整理法: 标记出没有用的对象,让所有存活的对象都向一端移动,然后直接 清除掉端边界以外的内
    • 优点: 解决了标记-清除算法导致的内存碎片问题和在存活率较高时复制算法效率 低的问题
  4. 分代收集法: 根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代, 新生代基本采用复制算法,老年代采用标记整理算法

1.7 MinorGC 和 FullGC

  1. Minor GC 通常发生在新生代的 Eden 区,在这个区的对象生存期短,往往发生 GC 的频率较高,回收速度比较快,一般采用复制回收算法
  2. Full GC/Major GC 发生在老年代,一般情况下,触发老年代 GC 的时候不会触发 Minor GC,所采用的是标记-清除算法

1.8 内存分配与回收策略

  1. 结构(堆大小 = 新生代(1/3) + 老年代(2/3)):
    • 新生代: 初始对象,生命周期短, Eden:s0:s1 = 8:1:1
    • 老年代: 长时间存在的对象
  2. 一般小型的对象都会在 Eden 区上分配,如果 Eden 区无法分配,那么尝试把活着的 对象放到 survivor0 中去( Minor GC
    • 如果 survivor0 可以放入,那么放入之后清除 Eden 区
    • 如果 survivor0 不可以放入,那么尝试把 Eden 和 survivor0 的存活对象放到 survivor1 中
      • 如果 survivor1 可以放入,那么放入 survivor1 之后清除 Eden 和 survivor0, 之后再把 survivor1 中的对象复制到 survivor0 中,保持 survivor1 一直为 空。
      • 如果 survivor1 不可以放入,那么直接把它们放入到老年代中,并清除 Eden 和 survivor0,这个过程也称为 分配担保Full GC
  3. 大对象、长期存活 (年龄 15 的对象, CMS 收集器默认 6 岁) 的对象则直接进入老 年代
  4. 动态对象年龄判定,当进入一批进入 survivor0 对象大小大于等于 survivor0 总内 存 50% 时会直接进入老年代 (可通过 -XX:TargetSurvivorRatio 控制 )
  5. 空间分配担保,Full GC

1.9 GC 垃圾收集器

  1. 年轻代常见垃圾收集器
    1. Serial New 收集器是针对新生代的收集器,
      • 采用的是复制算法
      • 每次收集使用单线程来做 GC 操作
    2. Parallel New 收集器,是 SN 收集器的多线程版本
      • 使用复制算法
      • 每次收集使用多线程并行来做 GC 操作
    3. Parallel Scavenge 收集器,针对新生代,采用复制收集算法
      • 提供了参数 -XX:+UseAdaptiveSizePolicy, 打开参数后,就不需要手工指定 新生代的大小(-Xmn), Eden 和 Survivor 区的比例 -XX:SurvivorRatio, 晋升老年代对象年龄 -XX:PretenureSizeThreshold 等细节参数
      • 吞吐量高 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些 参数以提供最合适的停顿时间或者最大的吞吐量
  2. 老年代常见垃圾收集器
    1. Serial Old 收集器,老年代采用标记清理,一般配合年轻代 SN 收集器使用
    2. CMS 收集器,基于标记清理,目的是 减少单次 GC的响应时间, 一般配合年轻 代 PN 收集器一起使用
      1. 初次标记(触发 STW),仅标记 GC Roots 直接引用对象,耗时短
      2. 并发标记(不 STW),多线程标记待回收的对象,耗时长
      3. 重新标记(触发 STW, 修正初次标记),并发标记因为没有 STW, 可能产生错 误标记,这里修正这些错误
      4. 并发清理
    3. Parallel Old 收集器,针对老年代,标记整理,一般配合年轻代 PS 收集使用
  3. G1 收集器(JDK): 整体上是基于标记清理,局部采用复制
    1. 标记成 Region 的网格区域
    2. E, S, O 分片存在不同 Region
  4. 综上: 新生代基本采用复制算法,老年代采用标记整理算法。CMS 采用标记清理

1.10 内存泄漏的几种情形

  1. 静态集合类: 如 HashMap、LinkedList 等等
    • 如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序 结束之前将不能被释放
    • 长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的
    • 对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收
  2. 各种连接不释放: 如数据库连接、网络连接和 IO 连接等
    • 在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需 要调用 close() 方法来释放与数据库的 连接。只有连接被关闭后,垃圾回收器 才会回收对应的对象
    • 如果在访问数据库的过程中,对 Connection、Statement 或 ResultSet 不显性地 关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
  3. 变量不合理的作用域:
    • 变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏
    • 如果没有及时地把对象设置为 null,很有可能导致内存泄漏的发生
  4. 内部类持有外部类:
    • 如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象 被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的 实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露
  5. 改变哈希值:
    • 当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算 哈希值的字段了,
    • 否则,对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在 这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集 合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中单 独删除当前对象,造成内存泄露
  6. 缓存泄漏:
    • 内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易 遗忘
    • 对于这个问题,可以使用 WeakHashMap 代表缓存,此种 Map 的特点是,当除了自 身有对 key 的引用外,此 key 没有其他引用那么此 map 会自动丢弃此值
  7. 监听器和回调:
    • 内存泄漏第三个常见来源是监听器和其他回调
    • 如果客户端在你实现的 API 中注册回调,却没有显示的取消,那么就会积聚
    • 需要确保回调立即被当作垃圾回收的最佳方法是只保存他的若引用,例如将他们保 存成为 WeakHashMap 中的键

1.11 Java 类加载机制

  1. 概念:
    • 虚拟机把描述类的数据文件(字节码)加载到内存,并对数据进行验证、准备、解 析以及类初始化,最终形成可以被虚拟机直接使用的 java.lang.Class Java 类 型对象
  2. 类的生命周期:
    • 加载: 通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所 代表的静态存储结构转化为方法区的运行时数据结构。在内存中(方法区)生成一个 代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
    • 验证: 为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求, 文件格式验证、元数据验证、字节码验证、符号引用验证
    • 准备: 正式为类属性分配内存,设置类属性初始值,这些内存都将在方法区中进 行分配
    • 解析: 虚拟机将常量池内的符号引用替换为直接引用
    • 初始化: 类初始化阶段是类加载过程最后一步。初始化阶段就是执行类构造器 <clinit>() 方法的过程
    • 使用:
    • 卸载:
  3. Java 类加载器:
    • 类加载器负责加载所有的类,同一个类(一个类用其全限定类名(包名加类名)标志) 只会被加载一次
    • Bootstrap ClassLoader : 根类加载器,负责加载 Java 的核心类,它不是 java.lang.ClassLoader 的子类,而是由 JVM 自身实现, 主要负责加载 jre/lib 目录中或被 -Xbootclasspath 指定的路径中的并且文件名是被虚拟 机识别的文件
    • Extension ClassLoader: 扩展类加载器,扩展类加载器的加载路径是 JDK 目录 下 jre/lib/ext, 扩展类的 getParent() 方法返回 null,实际上扩展类加载 器的父类加载器是根加载器,只是根加载器并不是 Java 实现的
    • System ClassLoader: 系统(应用)类加载器,它负责在 JVM 启动时加载来自 Java 命令的 -classpath 选项, java.class.path 系统属性或 CLASSPATH 环境变量所指定的 jar 包和类路径。程序可以通过 getSystemClassLoader() 来获取系统类加载器。系统加载器的加载路径是程序运行的当前路径

      ClassLoader loader = ClassLoader.getSystemClassLoader();
      System.out.println(loader); // => sun.misc.Launcher$AppClassLoader@73d16e93
      System.out.println(loader.getParent()); // => sun.misc.Launcher$ExtClassLoader@15db9742
      System.out.println(loader.getParent().getParent()); // => null
      
  4. 双亲委派模型的工作过程:
    • 首先会先查找当前 ClassLoader 是否加载过此类,有就返回
    • 如果没有,查询父 ClassLoader 是否已经加载过此类,如果已经加载过,就直 接返回 Parent 加载的类
    • 如果整个类加载器体系上的 ClassLoader 都没有加载过,才由当前 ClassLoader 加载(调用 findClass),整个过程类似循环链表一样。
  5. 双亲委托机制的作用:
    • 共享功能: 可以避免重复加载,当父亲已经加载了该类的时候,子类不需要再次加 载,一些 Framework 层级的类一旦被顶层的 ClassLoader 加载过就缓存在内存 里面,以后任何地方用到都不需要重新加载。
    • 隔离功能: 因为 String 已经在启动时被加载,所以用户自定义类是无法加载一个 自定义的类装载器,保证 Java/Android 核心类库的纯净和安全,防止恶意加载。
  6. 如何打破双亲委派模型?
    • 双亲委派模型的逻辑都在 loadClass() 中,重写 loaderClass(), 在实际工 程中一般是通过重写 findClass() 实现类加载
    • 系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一 个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载 器加载
  7. 自定义 ClassLoader:
    • loadClass(String name, boolean resolve): 根据指定的二进制名称加载类
    • findClass(String name): 根据二进制名称来查找类
    • 直接使用或继承已有的 ClassLoader 实现: java.net.URLClassLoader, java.security.SecureClassLoader, java.rmi.server.RMIClassLoader
    • 在调用 loadClass(), 会先根据委派模型在父加载器中加载,如果加载失败,则 会调用自己的 findClass 方法来完成加载

1.12 引起类加载操作的五个行为

  1. 遇到 new, getstatic, putstaticinvokestatic 这四条字节码指令
  2. 反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
  3. 子类初始化的时候,如果其父类还没初始化,则需先触发其父类的初始化
  4. 虚拟机执行主类的时候(有 main(string[] args) )
  5. JDK1.7 动态语言支持

1.13 Java 对象创建时机

  1. 使用 new 关键字创建对象
  2. 使用 Class 类的 newInstance 方法(反射机制)
  3. 使用 Constructor 类的 newInstance 方法(反射机制)
  4. 使用 Clone 方法创建对象
  5. 使用(反)序列化机制创建对象

2 实践案例

2.1 JVM 调优参数

设置堆空间,栈空间和元数据空间大小

java -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=512M -jar app.jar

2.2 JDK 调试工具

2.2.1 jps

打印 JVM 进程号

$ jps
 7 jar
 2078 Jps

2.2.2 jstat

通过 jstat 动态查看堆内存使用情况

# 查看堆内存使用情况
jstat -gcutil <pid> 1000

# 查看老年代使用情况
jstat -gcold <pid> 1000

# 查看年轻代使用情况
jstat -gcnew <pid> 1000

使用示例,每一秒刷新一次

# 查看堆内存使用情况
$ jstat -gcutil 70670 1000
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00   0.00   6.00   7.90  93.92  84.70      2    0.009     2    0.150    0.159
  0.00   0.00   6.00   7.90  93.92  84.70      2    0.009     2    0.150    0.159
  0.00   0.00   6.00   7.90  93.92  84.70      2    0.009     2    0.150    0.159
  0.00   0.00   6.00   7.90  93.92  84.70      2    0.009     2    0.150    0.159
  0.00   0.00   6.00   7.90  93.92  84.70      2    0.009     2    0.150    0.159

# 查看老年代使用情况
$ jstat -gcold 70670 1000
     MC       MU      CCSC     CCSU       OC          OU       YGC    FGC    FGCT     GCT
 21888.0  20556.1   2432.0   2059.9     79872.0      6313.3      2     2    0.150    0.159
 21888.0  20556.1   2432.0   2059.9     79872.0      6313.3      2     2    0.150    0.159
 21888.0  20556.1   2432.0   2059.9     79872.0      6313.3      2     2    0.150    0.159
 21888.0  20556.1   2432.0   2059.9     79872.0      6313.3      2     2    0.150    0.159
 21888.0  20556.1   2432.0   2059.9     79872.0      6313.3      2     2    0.150    0.159

# 查看年轻代使用情况
$ jstat -gcnew 70670 1000
 S0C    S1C    S0U    S1U   TT MTT  DSS      EC       EU     YGC     YGCT
 0.0    0.0    0.0    0.0  1  15 1024.0  51200.0   3072.0      2    0.009
 0.0    0.0    0.0    0.0  1  15 1024.0  51200.0   3072.0      2    0.009
 0.0    0.0    0.0    0.0  1  15 1024.0  51200.0   3072.0      2    0.009
 0.0    0.0    0.0    0.0  1  15 1024.0  51200.0   3072.0      2    0.009
 0.0    0.0    0.0    0.0  1  15 1024.0  51200.0   3072.0      2    0.009

2.2.3 jmap

使用 jmap 查看 JVM 虚拟机内存

# 查看所有内存分布
jmap <pid>

# 打印堆内存信息
jmap -heap <pid>

# 显示堆中对象的统计信息
jmap -histo:live <pid>

# 打印类加载器信息
jmap -clstats <pid>

# 打印等待终结的对象信息
jmap -finalizerinfo <pid>

# 生成堆转储快照 dump 文件
jmap -dump:format=b,file=heapdump.hprof <pid>
# live 子选项表示只 dump 存活的对象
jmap -dump:live,format=b,file=heapdump.hprof <pid>

jmap -dump:live,format=b,file=heapdump-live-$(date +'%Y%m%d-%H%M%S').hprof <pid>
jmap -dump:format=b,file=heapdump-all-$(date +'%Y%m%d-%H%M%S').hprof <pid>
$ jmap 19112
Attaching to process ID 19112, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.191-b12
0x0000000069790000      144K    C:\Local\Java\jdk1.8.0_191\jre\bin\sunec.dll
0x0000000069800000      52K     C:\Local\Java\jdk1.8.0_191\jre\bin\management.dll
0x0000000069810000      140K    C:\Local\Java\jdk1.8.0_191\jre\bin\instrument.dll
0x0000000069840000      68K     C:\Local\Java\jdk1.8.0_191\jre\bin\nio.dll
0x0000000069860000      104K    C:\Local\Java\jdk1.8.0_191\jre\bin\net.dll
0x0000000069880000      88K     C:\Local\Java\jdk1.8.0_191\jre\bin\zip.dll
0x00000000698a0000      164K    C:\Local\Java\jdk1.8.0_191\jre\bin\java.dll
0x00000000698d0000      60K     C:\Local\Java\jdk1.8.0_191\jre\bin\verify.dll
0x00000000698e0000      8848K   C:\Local\Java\jdk1.8.0_191\jre\bin\server\jvm.dll
0x000000006a190000      840K    C:\Local\Java\jdk1.8.0_191\jre\bin\msvcr100.dll
0x00007ff78f6f0000      220K    C:\Local\Java\jdk1.8.0_191\bin\java.exe
... 省略
0x00007ffaebe60000      72K     C:\WINDOWS\System32\winrnr.dll
0x00007ffaebe80000      84K     C:\WINDOWS\system32\wshbth.dll
0x00007ffaebfa0000      108K    C:\WINDOWS\system32\pnrpnsp.dll
0x00007ffb16e60000      756K    C:\WINDOWS\System32\KERNEL32.DLL
0x00007ffb16f20000      1196K   C:\WINDOWS\System32\RPCRT4.dll
0x00007ffb17680000      1664K   C:\WINDOWS\System32\USER32.dll
0x00007ffb17820000      696K    C:\WINDOWS\System32\SHCORE.dll
0x00007ffb178d0000      632K    C:\WINDOWS\System32\msvcrt.dll
0x00007ffb17970000      168K    C:\WINDOWS\System32\GDI32.dll
0x00007ffb17a30000      192K    C:\WINDOWS\System32\IMM32.DLL
0x00007ffb17ac0000      428K    C:\WINDOWS\System32\WS2_32.dll
0x00007ffb17b30000      7420K   C:\WINDOWS\System32\SHELL32.dll
0x00007ffb18310000      2004K   C:\WINDOWS\SYSTEM32\ntdll.dll

$ jmap -heap 19112
Attaching to process ID 19112, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.191-b12

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4276092928 (4078.0MB)
   NewSize                  = 89128960 (85.0MB)
   MaxNewSize               = 1425014784 (1359.0MB)
   OldSize                  = 179306496 (171.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 538968064 (514.0MB)
   used     = 39151888 (37.33815002441406MB)
   free     = 499816176 (476.66184997558594MB)
   7.2642315222595455% used
From Space:
   capacity = 24117248 (23.0MB)
   used     = 24101024 (22.984527587890625MB)
   free     = 16224 (0.015472412109375MB)
   99.93272864300272% used
To Space:
   capacity = 35651584 (34.0MB)
   used     = 0 (0.0MB)
   free     = 35651584 (34.0MB)
   0.0% used
PS Old Generation
   capacity = 213385216 (203.5MB)
   used     = 68250752 (65.0889892578125MB)
   free     = 145134464 (138.4110107421875MB)
   31.98476130605037% used

38914 interned Strings occupying 4036696 bytes.

2.2.4 jstack

jstack 可以打印线程栈

# jstack 7
2021-07-05 10:50:34
Full thread dump OpenJDK 64-Bit Server VM (25.212-b04 mixed mode):

"Attach Listener" #2044 daemon prio=9 os_prio=0 tid=0x00007f2820001000 nid=0x99a waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"logback-6" #1731 daemon prio=5 os_prio=0 tid=0x00007f27d8090000 nid=0x6df waiting on condition [0x00007f27b1de6000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000007b9cc83c0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1081)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

"logback-5" #1730 daemon prio=5 os_prio=0 tid=0x00007f27d8405800 nid=0x6de waiting on condition [0x00007f27b1ee7000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000007b9cc83c0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1081)
        at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

... 省略

"Service Thread" #20 daemon prio=9 os_prio=0 tid=0x00007f28b0431000 nid=0x33 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread14" #19 daemon prio=9 os_prio=0 tid=0x00007f28b042e000 nid=0x32 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread13" #18 daemon prio=9 os_prio=0 tid=0x00007f28b042c000 nid=0x31 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

... 省略

"VM Thread" os_prio=0 tid=0x00007f28b03c6000 nid=0x20 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f28b0020000 nid=0x9 runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f28b0022000 nid=0xa runnable

... 省略

"VM Periodic Task Thread" os_prio=0 tid=0x00007f28b0433800 nid=0x34 waiting on condition

JNI global references: 2614

2.2.5 arthas

阿里巴巴开源的 JVM 监控调试工具 Arthas 提供更多调试方法

2.2.6 Eclipse Memory Analyzer

Eclipse mat 工具提供分析内存镜像的方法,可以自行前往官网下载

2.3 逃逸分析

Java 在发现 new 出来的对象如果只在当前方法中运行,会直接进入线程栈,而不会放入 堆空间,这个现象叫做逃逸分析

实验时可以添加下列 Java 启动参数来显示,为达到演示效果需要现在堆空间为 10M

-Xmx10m -Xms10m -XX:+PrintGC -XX:-DoEscapeAnalysis

JDK 1.6 后默认开启逃逸分析 -XX:-DoEscapeAnalysis 选项,为完成实验需要临时关 闭一下逃逸分析选项

添加如下实验代码

public class Example02 {
  // -Xmx10m -Xms10m -XX:+PrintGC -XX:-DoEscapeAnalysis
  public static void main(String[] args) {
    while (true) {
      Integer temp = new Integer(999999999);
    }
  }
}

添加 -XX:-DoEscapeAnalysis 选项关闭逃逸分析后

[GC (Allocation Failure)  2048K->488K(9728K), 0.0015437 secs]
[GC (Allocation Failure)  2536K->504K(9728K), 0.0013609 secs]
[GC (Allocation Failure)  2552K->504K(9728K), 0.0004373 secs]
[GC (Allocation Failure)  2552K->488K(9728K), 0.0004283 secs]
[GC (Allocation Failure)  2536K->488K(9728K), 0.0004708 secs]
[GC (Allocation Failure)  2536K->488K(9728K), 0.0004193 secs]
[GC (Allocation Failure)  2536K->433K(9728K), 0.0004879 secs]
[GC (Allocation Failure)  2481K->433K(8704K), 0.0002465 secs]
[GC (Allocation Failure)  1457K->433K(9216K), 0.0002974 secs]
[GC (Allocation Failure)  1457K->433K(9216K), 0.0002519 secs]
[GC (Allocation Failure)  1457K->433K(9216K), 0.0002466 secs]
[GC (Allocation Failure)  1457K->433K(9216K), 0.0002524 secs]
... 疯狂地 GC

2.4 JDBC 知识点

  1. SPI (Service Provider Interface) 实际上是基于接口的编程, 策略模式和配置文 件组合实现的动态加载机制
    • 三方驱动在 META-INF/services/java.sql.Driver 中添加驱动配置信息
    • DriverManager 的静态代码块中通过调用 loadInitialDrivers(); 来读取使 用驱动信息

      /**
       * Load the initial JDBC drivers by checking the System property
       * jdbc.properties and then use the {@code ServiceLoader} mechanism
       */
      static {
          loadInitialDrivers();
          println("JDBC DriverManager initialized");
      }
      
    • loadInitialDrivers(); 方法中, ServiceLoader.load(Driver.class) 通过扫描 META-INF/services 目录下的配置文件找到实现类的全限定名,把类 加载到 JVM

      AccessController.doPrivileged(new PrivilegedAction<Void>() {
          public Void run() {
      
              ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
              Iterator<Driver> driversIterator = loadedDrivers.iterator();
      
              /* Load these drivers, so that they can be instantiated.
               * It may be the case that the driver class may not be there
               * i.e. there may be a packaged driver with the service class
               * as implementation of java.sql.Driver but the actual class
               * may be missing. In that case a java.util.ServiceConfigurationError
               * will be thrown at runtime by the VM trying to locate
               * and load the service.
               *
               * Adding a try catch block to catch those runtime errors
               * if driver not available in classpath but it's
               * packaged as service and that service is there in classpath.
               */
              try{
                  while(driversIterator.hasNext()) {
                      driversIterator.next();
                  }
              } catch(Throwable t) {
              // Do nothing
              }
              return null;
          }
      });
      
  2. 使用 DriverManager 中定义了 registeredDrivers 来存储项目中引用的确定类 信息

    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    
  3. 调用 getConnection 方法时通过变量所有注册的驱动来获取连接信息

    //  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         * classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }
    
        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }
    
        println("DriverManager.getConnection(\"" + url + "\")");
    
        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;
    
        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }
    
            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }
    
        }
    
        // if we got here nobody could connect.
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }
    
        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }
    
  4. 打破双亲委派
    • 由于 java.sql.Driver 的类加载器是 Bootstrap ClassLoader, 而实际加载三 方实现类的 Launcher.AppClassLoader.getAppClassLoader(var1); 方法的线程 上下文应该是 AppClassLoader, 这违背了双亲委派模型

      public class Launcher {
          private static URLStreamHandlerFactory factory = new Launcher.Factory();
          private static Launcher launcher = new Launcher();
          private static String bootClassPath = System.getProperty("sun.boot.class.path");
          private ClassLoader loader;
          private static URLStreamHandler fileHandler;
      
          public static Launcher getLauncher() {
              return launcher;
          }
      
          public Launcher() {
              Launcher.ExtClassLoader var1;
              try {
                  var1 = Launcher.ExtClassLoader.getExtClassLoader();
              } catch (IOException var10) {
                  throw new InternalError("Could not create extension class loader", var10);
              }
      
              try {
                  this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
              } catch (IOException var9) {
                  throw new InternalError("Could not create application class loader", var9);
              }
      
              Thread.currentThread().setContextClassLoader(this.loader);
              String var2 = System.getProperty("java.security.manager");
              if (var2 != null) {
                  SecurityManager var3 = null;
                  if (!"".equals(var2) && !"default".equals(var2)) {
                      try {
                          var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                      } catch (IllegalAccessException var5) {
                      } catch (InstantiationException var6) {
                      } catch (ClassNotFoundException var7) {
                      } catch (ClassCastException var8) {
                      }
                  } else {
                      var3 = new SecurityManager();
                  }
      
                  if (var3 == null) {
                      throw new InternalError("Could not create SecurityManager: " + var2);
                  }
      
                  System.setSecurityManager(var3);
              }
      
          }
      
        // 省略
      }
      
    • JDBC 4.0 之前使用 Class.forName("") 方式加载驱动是不会破坏双亲委派的, JDBC 4.0 之后使用 SPI 机制才会破坏双亲委派机制
    • Bootstrap ClassLoader 和 Extension ClassLoader, 他们只能加载自己指定目录 下的类,无法加载运行时的,其他目录下的类
    • JDBC 接口都是由 JDK 提供,具体实现是由第三方数据库产商提供的,所以也只能 采用这种方式去加载驱动实现类

3 反射

Java 提供可以动态创建对象和操作对象的方法,一般叫做反射

  1. private 属性处理时需要设置可见性
public static void main(String[] args) throws Exception {
  // 获取 Class 类
  Class<?> clz = Class.forName("io.github.jeanhwea.app03.bean.Person");

  // 获取类的构造器
  Constructor<?> ctor = clz.getConstructor();
  // 新建对象
  Object obj = ctor.newInstance();

  // 获取定义的字段
  Field field = clz.getDeclaredField("name");
  field.setAccessible(true); // private 修饰的字段需要配置可见性
  // 设置字段值
  field.set(obj, "Injected Person Name");

  // 获取类的方法
  Method method = clz.getMethod("getName");
  // 调用方法
  String name = (String) method.invoke(obj);
  System.out.println(name);
}

反射的一些注意事项

  1. 内部类调用 getConstructor() 时可能没有默认的构造器,声明成静态内部类

Last Updated 2021-10-16 Sat 09:40. Created by Jinghui Hu at 2021-06-21 Mon 14:27.