JVM 知识点
1 基础知识
1.1 JVM 运行内存的分类
- 程序计数器 当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机 字节指令地址,线程私有 注: 如果正在执行的是 Native 方法,计数器值则为空
- Java 线程栈 存放基本数据类型、对象的引用、方法出口等,线程私有
- 局部变量表
- 操作数栈
- 动态链接
- 方法出口地址
- 本地方法栈 和线程栈相似,只不过它服务于 Native 方法,线程私有
- Java 堆 Java 内存最大的一块,所有对象实例、数组都存放在 Java 堆,GC 回收 的地方,线程共享
- 元空间 存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据
等。(即永久代),回收目标主要是常量池的回收和类型的卸载,各线程共享
- 元空间是 JDK 1.7 之前叫方法区
1.2 Java 内存堆和栈区别
- 栈内存用来存储基本类型的变量和对象的引用变量,堆内存用来存储 Java 中的对象, 无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中
- 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线 程中可见,即栈内存可以理解成线程的私有内存,堆内存中的对象对所有线程可见。 堆内存中的对象可以被所有线程访问
- 如果栈内存没有可用的空间存储方法调用和局部变量,JVM 会抛出
java.lang.StackOverFlowError
, 如果是堆内存没有可用的空间存储生成的对象, JVM 会抛出java.lang.OutOfMemoryError
- 栈的内存要远远小于堆内存,如果你使用递归的话,那么你的栈很快就会充满,
-Xms<size>
set initial Java heap size-Xmx<size>
set maximum Java heap size-Xss<size>
set java thread stack size
1.3 Java 四引用
- 强引用 (StrongReference)强引用是使用最普遍的引用。如果一个对象具有强引
用,那垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出
OutOfMemoryError
错误,使程序异常终止,也不会靠随意回收具有强引用的对象 来解决内存不足的问题 - 软引用 (SoftReference) 如果内存空间不足了,就会回收这些对象的内存。只 要垃圾回收器没有回收它,软引用可以和一个引用队列(ReferenceQueue)联合使用, 如果软引用所引用的对象被垃圾回收器回收,Java 虚拟机就会把这个软引用加入到 与之关联的引用队列中
- 弱引用 (WeakReference) 弱引用与软引用的区别在于: 只具有弱引用的对象拥 有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发 现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。弱引用 可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾 回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中
- 虚引用 (PhantomReference)虚引用在任何时候都可能被垃圾回收器回收,主要 用来跟踪对象被垃圾回收器回收的活动,被回收时会收到一个系统通知。虚引用与软 引用和弱引用的一个区别在于: 虚引用必须和引用队列 (ReferenceQueue)联合使 用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的 内存之前,把这个虚引用加入到与之关联的引用队列中
1.4 GC 回收机制
- Java 中对象是采用
new
或者反射的方法创建的,这些对象的创建都是在堆(Heap) 中分配的,所有对象的回收都是由 Java 虚拟机通过垃圾回收机制完成的。GC 为了 能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋 值等状 况进行监控 - Java 程序员不用担心内存管理,因为垃圾收集器会自动进行管理
- 可以调用下面的方法之一
System.gc()
或Runtime.getRuntime().gc()
但 JVM 可以屏蔽掉显示的垃圾回收调用
1.5 GC 标记对象的死活
- 引用计数法 给对象添加一个引用计数器,没当被引用的时候,计数器的值就加一。
引用失效的时候减一,当计数器的值为 0 的时候就表示改对象可以被 GC 回收了,弊
端:
A->B,B->A
, 那么 AB 将永远不会被回收了。也就是引用有环的情况 - 根搜索算法 (可达性算法) GC Roots Tracing: 通过一个叫 GC Roots 的对象作为
起点,从这些结点开始向下搜索,搜索所走过的路径称为引用链,当一个对象没有与任
何的引用链相连的时候则改对象就可以被。 GC 回收回收了 Roots 包括: Java 虚拟
机栈中引用的对象,本地方法栈中引用的对象,方法区中常量引用的对象,方法区中静
态属性引用的对象在 Java 语言里,可作为 GC Roots 的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中的引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中 JNI(即一般说的 Native 方法)的引用的对象
1.6 GC 回收算法
- 标记-清除法: 标记出没有用的对象,然后一个一个回收掉
- 缺点: 标记和 清除两个过程效率不高,产生内存碎片导致需要分配较大对象时无 法找到足够的连续内存而需要触发一次 GC 操作
- 复制回收算法: 按照容量划分二个大小相等的内存区域,当一块用完的时候将活着
的对象复制到另一块上,然后再把已使用的内存空间一次清理掉
- 缺点: 将内存缩小为了原来的一半
- 标记-整理法: 标记出没有用的对象,让所有存活的对象都向一端移动,然后直接
清除掉端边界以外的内
- 优点: 解决了标记-清除算法导致的内存碎片问题和在存活率较高时复制算法效率 低的问题
- 分代收集法: 根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代, 新生代基本采用复制算法,老年代采用标记整理算法
1.7 MinorGC 和 FullGC
- Minor GC 通常发生在新生代的 Eden 区,在这个区的对象生存期短,往往发生 GC 的频率较高,回收速度比较快,一般采用复制回收算法
- Full GC/Major GC 发生在老年代,一般情况下,触发老年代 GC 的时候不会触发 Minor GC,所采用的是标记-清除算法
1.8 内存分配与回收策略
- 结构(堆大小 = 新生代(1/3) + 老年代(2/3)):
- 新生代: 初始对象,生命周期短,
Eden:s0:s1 = 8:1:1
- 老年代: 长时间存在的对象
- 新生代: 初始对象,生命周期短,
- 一般小型的对象都会在 Eden 区上分配,如果 Eden 区无法分配,那么尝试把活着的
对象放到 survivor0 中去(
Minor GC
)- 如果 survivor0 可以放入,那么放入之后清除 Eden 区
- 如果 survivor0 不可以放入,那么尝试把 Eden 和 survivor0 的存活对象放到
survivor1 中
- 如果 survivor1 可以放入,那么放入 survivor1 之后清除 Eden 和 survivor0, 之后再把 survivor1 中的对象复制到 survivor0 中,保持 survivor1 一直为 空。
- 如果 survivor1 不可以放入,那么直接把它们放入到老年代中,并清除 Eden
和 survivor0,这个过程也称为 分配担保 (
Full GC
)
- 大对象、长期存活 (年龄 15 的对象, CMS 收集器默认 6 岁) 的对象则直接进入老 年代
- 动态对象年龄判定,当进入一批进入 survivor0 对象大小大于等于 survivor0 总内
存 50% 时会直接进入老年代 (可通过
-XX:TargetSurvivorRatio
控制 ) - 空间分配担保,Full GC
1.9 GC 垃圾收集器
- 年轻代常见垃圾收集器
- Serial New 收集器是针对新生代的收集器,
- 采用的是复制算法
- 每次收集使用单线程来做 GC 操作
- Parallel New 收集器,是 SN 收集器的多线程版本
- 使用复制算法
- 每次收集使用多线程并行来做 GC 操作
- Parallel Scavenge 收集器,针对新生代,采用复制收集算法
- 提供了参数
-XX:+UseAdaptiveSizePolicy
, 打开参数后,就不需要手工指定 新生代的大小(-Xmn), Eden 和 Survivor 区的比例-XX:SurvivorRatio
, 晋升老年代对象年龄-XX:PretenureSizeThreshold
等细节参数 - 吞吐量高 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些 参数以提供最合适的停顿时间或者最大的吞吐量
- 提供了参数
- Serial New 收集器是针对新生代的收集器,
- 老年代常见垃圾收集器
- Serial Old 收集器,老年代采用标记清理,一般配合年轻代 SN 收集器使用
- CMS 收集器,基于标记清理,目的是
减少单次 GC的响应时间
, 一般配合年轻 代 PN 收集器一起使用- 初次标记(触发 STW),仅标记 GC Roots 直接引用对象,耗时短
- 并发标记(不 STW),多线程标记待回收的对象,耗时长
- 重新标记(触发 STW, 修正初次标记),并发标记因为没有 STW, 可能产生错 误标记,这里修正这些错误
- 并发清理
- Parallel Old 收集器,针对老年代,标记整理,一般配合年轻代 PS 收集使用
- G1 收集器(JDK): 整体上是基于标记清理,局部采用复制
- 标记成 Region 的网格区域
- E, S, O 分片存在不同 Region
- 综上: 新生代基本采用复制算法,老年代采用标记整理算法。CMS 采用标记清理
1.10 内存泄漏的几种情形
- 静态集合类: 如 HashMap、LinkedList 等等
- 如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序 结束之前将不能被释放
- 长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的
- 对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收
- 各种连接不释放: 如数据库连接、网络连接和 IO 连接等
- 在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需
要调用
close()
方法来释放与数据库的 连接。只有连接被关闭后,垃圾回收器 才会回收对应的对象 - 如果在访问数据库的过程中,对 Connection、Statement 或 ResultSet 不显性地 关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
- 在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需
要调用
- 变量不合理的作用域:
- 变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏
- 如果没有及时地把对象设置为 null,很有可能导致内存泄漏的发生
- 内部类持有外部类:
- 如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象 被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的 实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露
- 改变哈希值:
- 当一个对象被存储进 HashSet 集合中以后,就不能修改这个对象中的那些参与计算 哈希值的字段了,
- 否则,对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了,在 这种情况下,即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集 合中检索对象,也将返回找不到对象的结果,这也会导致无法从 HashSet 集合中单 独删除当前对象,造成内存泄露
- 缓存泄漏:
- 内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易 遗忘
- 对于这个问题,可以使用 WeakHashMap 代表缓存,此种 Map 的特点是,当除了自 身有对 key 的引用外,此 key 没有其他引用那么此 map 会自动丢弃此值
- 监听器和回调:
- 内存泄漏第三个常见来源是监听器和其他回调
- 如果客户端在你实现的 API 中注册回调,却没有显示的取消,那么就会积聚
- 需要确保回调立即被当作垃圾回收的最佳方法是只保存他的若引用,例如将他们保 存成为 WeakHashMap 中的键
1.11 Java 类加载机制
- 概念:
- 虚拟机把描述类的数据文件(字节码)加载到内存,并对数据进行验证、准备、解
析以及类初始化,最终形成可以被虚拟机直接使用的
java.lang.Class
Java 类 型对象
- 虚拟机把描述类的数据文件(字节码)加载到内存,并对数据进行验证、准备、解
析以及类初始化,最终形成可以被虚拟机直接使用的
- 类的生命周期:
- 加载: 通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所
代表的静态存储结构转化为方法区的运行时数据结构。在内存中(方法区)生成一个
代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口 - 验证: 为了确保
Class
文件的字节流中包含的信息符合当前虚拟机的要求, 文件格式验证、元数据验证、字节码验证、符号引用验证 - 准备: 正式为类属性分配内存,设置类属性初始值,这些内存都将在方法区中进 行分配
- 解析: 虚拟机将常量池内的符号引用替换为直接引用
- 初始化: 类初始化阶段是类加载过程最后一步。初始化阶段就是执行类构造器
<clinit>()
方法的过程 - 使用:
- 卸载:
- 加载: 通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所
代表的静态存储结构转化为方法区的运行时数据结构。在内存中(方法区)生成一个
代表这个类的
- 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
- 双亲委派模型的工作过程:
- 首先会先查找当前
ClassLoader
是否加载过此类,有就返回 - 如果没有,查询父
ClassLoader
是否已经加载过此类,如果已经加载过,就直 接返回 Parent 加载的类 - 如果整个类加载器体系上的
ClassLoader
都没有加载过,才由当前ClassLoader
加载(调用findClass
),整个过程类似循环链表一样。
- 首先会先查找当前
- 双亲委托机制的作用:
- 共享功能: 可以避免重复加载,当父亲已经加载了该类的时候,子类不需要再次加
载,一些 Framework 层级的类一旦被顶层的
ClassLoader
加载过就缓存在内存 里面,以后任何地方用到都不需要重新加载。 - 隔离功能: 因为 String 已经在启动时被加载,所以用户自定义类是无法加载一个 自定义的类装载器,保证 Java/Android 核心类库的纯净和安全,防止恶意加载。
- 共享功能: 可以避免重复加载,当父亲已经加载了该类的时候,子类不需要再次加
载,一些 Framework 层级的类一旦被顶层的
- 如何打破双亲委派模型?
- 双亲委派模型的逻辑都在
loadClass()
中,重写loaderClass()
, 在实际工 程中一般是通过重写findClass()
实现类加载 - 系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一 个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载 器加载
- 双亲委派模型的逻辑都在
- 自定义
ClassLoader
:loadClass(String name, boolean resolve)
: 根据指定的二进制名称加载类findClass(String name)
: 根据二进制名称来查找类- 直接使用或继承已有的
ClassLoader
实现:java.net.URLClassLoader
,java.security.SecureClassLoader
,java.rmi.server.RMIClassLoader
- 在调用
loadClass()
, 会先根据委派模型在父加载器中加载,如果加载失败,则 会调用自己的findClass
方法来完成加载
1.12 引起类加载操作的五个行为
- 遇到
new
,getstatic
,putstatic
或invokestatic
这四条字节码指令 - 反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化
- 子类初始化的时候,如果其父类还没初始化,则需先触发其父类的初始化
- 虚拟机执行主类的时候(有
main(string[] args)
) - JDK1.7 动态语言支持
1.13 Java 对象创建时机
- 使用
new
关键字创建对象 - 使用
Class
类的newInstance
方法(反射机制) - 使用
Constructor
类的newInstance
方法(反射机制) - 使用
Clone
方法创建对象 - 使用(反)序列化机制创建对象
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 知识点
- 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
目录下的配置文件找到实现类的全限定名,把类 加载到 JVMAccessController.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; } });
- 三方驱动在
使用
DriverManager
中定义了registeredDrivers
来存储项目中引用的确定类 信息// List of registered JDBC drivers private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
调用
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"); }
- 打破双亲委派
由于
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 提供可以动态创建对象和操作对象的方法,一般叫做反射
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); }
反射的一些注意事项
- 内部类调用
getConstructor()
时可能没有默认的构造器,声明成静态内部类