类的生命周期/类加载机制
类的生命周期分7个阶段:
-
加载:
- 通过全限定名获取它的字节码,将字节码载入方法区中(1.7载入到运行时内存中的永久代,1.8载入到本地内存中的元空间)
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构,内部采用 C++ 的 instanceKlass 描述 java 类。
- 在堆中生成该类的java.lang.Class对象,作为该类的各种数据的访问入口,(1.7及以后还存放有static变量),同时,instanceKlass的_java_mirror字段纪录该类的Class对象的地址,该Class对象也会在对象头中纪录instanceKlass的地址。
-
链接
-
验证:验证类的字节码中的信息是否符合 JVM规范,并做安全性检查。
- 文件格式验证
- 元数据验证
- 字节码验证
- 符合引用验证
-
准备:为 static 变量分配空间,然后赋默认值。
- 赋默认值≠赋初始值,默认值是对应数据类型的零值,赋初始值是在初始化阶段做的。
- 若static变量还是final的基本类型,以及字符串常量,编译阶段就可确定其值,在准备阶段就可不赋默认值,直接赋对应的值,不需要在初始化阶段做。
- 若static变量是引用类型,不管用什么修饰,赋初始值只能在初始化阶段做。
- 1.7以前静态变量和stringtable在方法区中分配内存。1.7及以后静态变量存放在堆中的Class对象中,stringtable单独存放在堆中。
-
解析:将常量池中的符号引用解析为直接引用。
- 符号引用:以一组符号来描述引用的目标。
- 直接引用:直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
-
-
初始化:执行类构造器方法,即执行
<cinit>()V
,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。-
<cinit>()V
带锁,所以线程安全。 -
类初始化的发生时机:(记住一句话:类初始化是懒惰初始化)
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 一个对象
-
不会导致类初始化的情况
-
访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
-
类对象.class 不会触发初始化
-
创建该类的数组不会触发初始化
-
类加载器的 loadClass 方法
-
Class.forName 的参数 2 为 false 时
-
-
-
使用:访问方法区内的数据结构的接口,对象是堆中的数据
-
卸载:该类的 Class 对象被 GC
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类加载过程必须按照这个顺序开始,但是解析就不一定,因为Java存在运行时绑定机制。
类加载器的层次
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
:
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++实现(java无法访问),负责加载
%JAVA_HOME%/lib
目录下的 jar 包和类或者被-Xbootclasspath
参数指定的路径中的所有类。 - ExtensionClassLoader(扩展类加载器) :主要负责加载
%JRE_HOME%/lib/ext
目录下的 jar 包和类,或被java.ext.dirs
系统变量所指定的路径下的 jar 包。 - AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
- 自定义类加载器
双亲委派
如果一个类加载器收到了类加载的请求,系统会首先判断当前类是否被加载过,若已加载则直接返回,若未加载,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
- 当AppClassLoader加载一个class时,系统会首先判断当前类是否被加载过,若已加载则直接返回,若未加载,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载。
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
如何破坏双亲委派:SPI。
双亲委派的优点:
因为JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类,所以采用双亲委派机制能:
- 保证Java程序安全稳定运行
- 避免重复的类加载
- 保证java核心API不被修改
- 系统类防止内存中出现多份同样的字节码
Class.forName()和ClassLoader.loadClass()区别?
ClassLoader.loadClass()
仅仅完成加载阶段(毕竟这是类加载器类的方法,只能处理加载阶段)Class.forName()
完成的阶段要多一点,除了加载阶段,还会进行准备阶段,执行static语句块。它也可以加参数来控制是否执行static块
JVM内存结构
Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。
- 线程私有:程序计数器、虚拟机栈、本地方法栈
- 线程共享:堆、方法区、直接内存
运行时数据区
JVM的内存结构主要就是运行时数据区,分为5个部分:
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 堆
- 方法区
程序计数器
记录要读取执行的指令的地址,实现代码的流程控制,执行引擎通过程序计数器依次读取指令。
-
CPU中是PC寄存器(program counter register),在JVM中的程序计数器是对它的一种模拟抽象。
-
程序计数器是唯一一个不会出现
OutOfMemoryError
的内存区域 -
线程私有,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
虚拟机栈
主管java程序的运行,管理除本地方法以外的java方法的调用。线程私有,每个线程都有一个虚拟机栈,栈中存有一个个栈帧,每调用一次方法就会将对应的栈帧压入栈中,每一个方法调用结束后都会将对应的栈帧弹出。
虚拟机栈的特点
- 访问速度快,仅次于程序计数器
- JVM对栈只有两个操作:入栈和出栈
- 不存在垃圾回收问题,返回方式有两种:return和抛异常,两种方式都会让栈帧弹出。
- 可以通过参数
-Xss
来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
虚拟机栈有哪些异常
- StackOverflowError:方法调用太深,虚拟机栈中不够空间放下压入的栈帧(一个里面的空间不够)
- OutOfMemoryError:线程太多,不够空间分配虚拟机栈(个数太多,总的空间不够)
栈帧内部结构
- 局部变量表(Local Variables):主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference)
- 操作数栈(Operand Stack):存放方法执行过程中产生的中间计算结果。
- 动态链接(Dynamic Linking):将运行时常量池中的符号引用转换为调用方法的直接引用。
- 方法返回地址(Return Address):方法正常退出或异常退出的地址。
- 一些附加信息
本地方法栈
管理本地方法的调用,在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
方法区
方法区属于是 JVM 运行时数据区的一块逻辑区域(是JVM规范的一种概念),线程共享。用于存储类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据
当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。
重要变动(永久代和元空间)
- 在1.8以前,Hotspot用永久代(PermGen)来实现方法区,与堆内存共用同一存储空间
- 1.8以后,用元空间(Metaspace)实现,存储于本地内存
- 在1.6的时候HotSpot开发团队就有放弃永久代,逐步改为采用本地内存(Native Memory)来实现方法区的规划了,到了1.7,已经把原本放在永久代的字符串常量池、静态变量等移出至堆中,符号引用移至native heap(属于本地内存)中,而到了1.8,终于完全废弃了永久代的概念,改用在本地内存中用元空间实现,把1.7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace)?
永久代有一个JVM本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机的可用内存的限制,虽然元空间仍可能溢出,但是比原来出现的几率会更小,换句话说,能加载的类更多了。
运行时常量池
- 常量池表(Constant Pool Table)是 Class 文件的一部分,用于存储编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中
- 每个类都会维护一个独立的常量池
- 运行时常量池,相对于 Class 文件常量池的另一个重要特征是:动态性,Java 语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,String 类的
intern()
方法就是这样的
堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,"几乎"所有的对象实例以及数组都在这里分配内存。
又可引出一个问题:堆是分配对象存储的唯一选择吗?
随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
堆内存的划分
1.7及以前,堆内存被通常分为下面三部分:
- 新生代(Young Generation)
- 老生代(Old Generation)
- 永久代(Permanent Generation)
1.8,分为两部分:
- 新生代(Young Generation)
- 老生代(Old Generation)
新生代 (Young Generation)
新生代是所有新对象创建的地方。在这里GC称为 Minor GC。新生一代被分为三个部分——伊甸园(Eden Memory)和两个幸存区(Survivor Memory,被称为from/to或s0/s1),默认比例是8:1:1
- 大多数新创建的对象都位于 Eden 中
- 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to(这个复制算法能避免产生内存碎片)
- 当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。
老年代(Old Generation)
存储那些经过许多轮minor GC 后仍然存活的对象。通常,垃圾回收是在老年代内存不足时执行的。老年代垃圾回收称为Major GC,通常需要更长的时间。
大对象直接进入老年代。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝
字符串常量池
JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。StringTable(就是字符串常量池),本质上是一个HashSet,StringTable存在堆中,存的仅是对象的引用,字符串对象在堆中。
什么是 TLAB (Thread Local Allocation Buffer)?
-
线程本地分配缓存区
-
从内存模型而不是垃圾回收的角度,对 Eden 区域继续进行划分,JVM 为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内
-
使用的原因:
- 堆是线程共享的,对象创建又是比较频繁的,并发环境下创建对象是线程不安全的
- 若加锁,性能变差
-
默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden 空间的 1%
-
一旦对象在 TLAB 空间分配内存失败时,JVM 就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存。
JMM java内存模型
JMM 抽象结构如下:
- 主内存 :所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的局部变量
- 线程本地内存 :每个线程都有一个私有的线程本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。线程本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。
- 正是因为这样的设计,也就出现了共享变量的可见性问题。与之对应的就有了线程间通信、进程间通信、volatile等的场景。
内存模型和内存结构两者不是一样的东西:
- 内存结构与jvm的运行时数据区相关
- 内存模型(jmm)与并发编程相关
如何判断一个对象是否可以回收?
- 引用计数法:对象增加一个引用,计时器+1,减少就-1,若计时器=0则可回收。但会出现对象间的循环引用,所以这个方法不可行。
- 可达性分析算法:以GC Roots对象为起点进行搜索,能达到的对象就存活,不可达的对象就被回收。JVM就用这个。GC Roots 一般包含以下对象:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
四种引用类型?
- 强引用:
- 不会被gc。
- 使用 new 一个新对象的方式来创建强引用
- 软引用:
- 只有在内存不够的情况下才会被gc。
- 使用 SoftReference 类来创建软引用。
- 弱引用:
- 内存够不够都要被gc,一定会被gc,即只能活到下次gc发生之前。
- 使用 WeakReference 类来创建弱引用。
- 虚引用:
- 不影响其生存时间。
- 必须配合引用队列(ReferenceQueue)使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
- 使用 PhantomReference 来实现虚引用。
有哪些基本的垃圾回收算法?
-
标记 - 清除
- 将存活的对象进行标记,然后清理掉未被标记的对象
- 缺点:
- 标记和清除过程都不高效
- 产生内存碎片
-
标记 - 整理
- 让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。不会有内存碎片。
- 缺点:慢
-
复制
- 两个区域,from存活的会被复制到to区。不会产生内存碎片。
- 缺点:要占两倍的内存空间,或原本的内存空间要分成两份,空间变小了
-
分代回收算法
- 新生代——>复制算法
- 老年代——>标记整理或标记清除算法
-
分区分代回收算法
将整个堆空间划分为连续的不同小区间,每个小区间独立使用,独立回收。好处是可以控制一次回收多少个小区间 ,根据目标阻塞时间,每次合理地回收若干个小区间(而不是整个堆),从而减少一次 GC 所产生的阻塞。G1就是用这样的算法。
HotSpot有哪些垃圾回收器?
有7个,连线的就是可以配合使用。
-
新生代:
- Serial
- ParNew
- Parallel Scavenge
-
老年代:
- CMS
- Serial Old(MSC)
- Parallel Old
-
混合回收/全流程:
- G1
- ZGC:java11出现的
-
单线程与多线程:单线程指的是垃圾回收器只使用一个线程进行回收,而多线程使用多个线程。
-
串行与并行: (这里是对多核CPU来说的)串行指的是垃圾回收器与用户程序只能有一个执行,这意味着在执行垃圾回收的时候需要阻塞用户程序;并行指的是垃圾回收器和用户程序并行执行。除了 CMS 和 G1 之外,其它垃圾回收器都是以串行的方式执行。
-
并发是对某个CPU来说的。讨论GC的时候基本都是基于多核CPU来说了。
1. Serial
-
以串行的方式执行。它是单线程的回收器,只会使用一个线程进行垃圾回收。
-
Serial是新生代中的回收器,使用复制算法。
-
可配合serialOld实现整个回收过程串行执行,serial + serialOld:
- serial —> 新生代:单线程GC + 复制
- serialOld —> 老年代:单线程GC + 标记整理
-
优点:简单高效,对于单 CPU 环境来说,由于没有线程切换的开销,因此拥有最高的单线程回收效率。主要在Client模式下使用。
2. ParNew
-
是 Serial 的多线程版本。
-
是新生代中的回收器,使用复制算法。
-
可配合serialOld,ParNew + serialOld:
- ParNew —> 新生代:多线程GC + 复制
- serialOld —> 老年代:单线程GC + 标记整理
-
默认开启的GC线程数量与 CPU 数量相同,当CPU 非常多;时,可以使用 -XX:ParallelGCThreads 参数来设置线程数。
3. Parallel Scavenge
-
多线程回收,是新生代中的回收器,也使用复制算法。
-
其它回收器关注点是尽可能缩短垃圾回收时用户线程的阻塞时间,属于”响应时间优先“;而Parallel Scavenge的目标是达到尽可能大的吞吐量,属于“吞吐量优先”。吞吐量优先是指 CPU 运行用户代码的时间占总时间的比值要尽可能大。
-
阻塞时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以尽快完成程序的运算任务,适合后台运算,不需要太多交互的任务。
-
提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾回收停顿时间的
-XX:MaxGCPauseMillis
参数以及直接设置吞吐量大小的-XX:GCTimeRatio
参数。MaxGCPauseMillis
参数允许的值是一个大于 0 的毫秒数,回收器将尽可能地保证内存回收花费的时间不超过设定值。 -
缩短阻塞时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
-
可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手动指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况回收性能监控信息,动态调整这些参数以提供最合适的阻塞时间或者最大的吞吐量。
4. Serial Old
标记整理算法。
是 Serial 的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 回收器搭配使用。
- 作为 CMS 回收器的后备预案,在并发回收发生 Concurrent Mode Failure 时使用。
5. Parallel Old
-
是 Parallel Scavenge 的老年代版本。
-
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 回收器。
6. CMS
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。分为以下四个流程:
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除:GC,不需要停顿。这一步会由于用户线程继续运行而产生浮动垃圾,只能到下一次 GC 才能被回收。
在整个过程中耗时最长的并发标记和并发清除过程中,GC线程都可以与用户线程并行执行,不需要进行阻塞用户进程,所以STW时间短。(这里的两个并发是对执行GC线程的单个CPU来说的,但多个CPU来看是并行的)。
缺点:
-
吞吐量低:停顿时间短是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
-
无法处理浮动垃圾,可能出现 Concurrent Mode Failure。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 回收不能像其它回收器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
-
标记 - 清除算法导致的内存碎片,往往会出现老年代无法找到足够大连续空间来存储当前对象,不得不提前触发一次 Full GC。
7. G1
-
可进行全流程回收(新生代和老年代一起回收)。
-
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。这样可以单独对每个小空间进行垃圾回收,具有很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的回收时间,优先回收价值最大的 Region。
-
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要阻塞用户线程,但是可多个GC线程并行执行最终标记。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并行执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高回收效率。
具备如下特点:
- 空间整合:整体来看是标记 - 整理算法,但从局部(两个 Region 之间)上来看是复制算法,这意味着不会产生内存空间碎片。
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
JVM的内存分配策略?
-
对象优先在 Eden 分配:大多数情况下,对象在新生代的 Eden 区分配,当 Eden 空间不够时,发起 Minor GC。
-
大对象直接进入老年代:
-
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
-
经常出现大对象会提前触发GC以获取足够的连续空间分配给大对象。
-
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
-
-
长期存活(年龄达到阈值)的对象进入老年代:
- 为对象定义年龄计数器,对象每经过一次Minor GC 仍能存活,年龄就增加 1 岁,达到一定年龄则进入老年代。
- -XX:MaxTenuringThreshold 用来定义年龄的阈值。
- 为对象定义年龄计数器,对象每经过一次Minor GC 仍能存活,年龄就增加 1 岁,达到一定年龄则进入老年代。
-
动态对象年龄判定:
JVM并不强制要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则大于或等于该年龄的对象直接进入老年代,无需达到阈值。
-
老年代的空间分配担保
-
在 Minor GC 之前,JVM先检查老年代最大的可用连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么才可认为 Minor GC 是安全的,进行Minor GC。
-
若不成立,JVM会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许,就会继续检查老年代最大的可用连续空间是否大于历次晋升到老年代对象的平均大小,若大于,将尝试着进行一次 Minor GC;若小于,或 HandlePromotionFailure 设置不允许冒险,就要进行一次 Full GC。
-
有哪些JVM性能监控工具(指令)?
- top:Linux系统的指令,可查看所有进程的性能情况,如CPU使用率,内存占用率等。
- jps:和Linux的ps查看进程类似,用来查询运行着的java进程。
- jstack:堆栈跟踪工具,一般用于查看某个进程中的线程的运行情况。
- jstat:监视jvm堆内存和非堆内存的大小和内存的占用情况。
- jmap:生成某个java进程的内存中所有对象的情况的快照,查看内存占用情况。
- jinfo:查看某个进程的jvm参数。
- jconsole:一个java GUI监视工具,可监视远程的服务器。
参考文章:
https://pdai.tech/md/interview/x-interview.html
https://javaguide.cn/java/jvm/memory-area.html
https://www.cnblogs.com/mazhimazhi/p/13511728.html
我的blog主要在头篇文章的基础上加上了自己的一些理解和补充,并修改了原文章的排版