JVM 简介
JVM 的参数类型
标配参数
- -version
- -herp
- java -showversion
X参数
- Xint:解释执行
- Xcomp:第一次使用就编译成本地代码
- Xmixed:混合模式
XX参数
Boolean 类型
公式
-XX:+或者-某个属性
+表示开启
-表示关闭
案例
是否打印 GC 收集细节
-XX:-PrintGCDetails
-XX:+PrintGCDetails
C:\Users\17505>jps -l
6656 sun.tools.jps.Jps
22356
14776 org.jetbrains.jps.cmdline.Launcher
22332 com.lhn.demo1.jvm.HelloGC
C:\Users\17505>jinfo -flag PrintGCDetails 22332
-XX:-PrintGCDetails
C:\Users\17505>jps -l
22356
792 sun.tools.jps.Jps
9496 com.lhn.demo1.jvm.HelloGC
21948 org.jetbrains.jps.cmdline.Launcher
C:\Users\17505>jinfo -flag PrintGCDetails 9496
-XX:+PrintGCDetails
是否使用串行垃圾回收器
KV 设置类型
公式
-XX:属性 key = 属性值 value
jinfo 举例,如何查看当前运行程序的配置
jinfo -flag 具体参数 PID
jinfo -flags PID
C:\Users\17505>jps -l
22356
792 sun.tools.jps.Jps
9496 com.lhn.demo1.jvm.HelloGC
21948 org.jetbrains.jps.cmdline.Launcher
C:\Users\17505>jinfo -flag PrintGCDetails 9496
-XX:+PrintGCDetails
题外话(坑题)
-Xms:等价于 -XX:InitialHeapSize
-Xmx:等价于 -XX:MaxHeapSize
JVM 的参数默认值
-XX:+PrintFlagsInitial
主要查看初始默认
公式
java -XX:+PrintFlagsInitial -version
java -XX:+PrintFlagsInitial
-XX:+PrintFlagsFinal
主要查看修改过的参数
java -XX:+PrintFlagsFinal-version
java -XX:+PrintFlagsFinal
bool UseLargePagesIndividualAllocation := false
uintx InitialHeapSize := 268435456
uintx InitialCodeCacheSize = 2555904
= 默认值
:= 修改以后的值
运行公式的同时打印出参数:
java -XX:+PrintFlagsFinal -Xss128k T(运行的java 类名字)
-XX:+PrintCommandLineFlags -version
可查看默认的垃圾回收器
C:\Users\17505> java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=267598784 -XX:MaxHeapSize=4281580544 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)
JVM 常见参数
-
-Xms
- 初始大小内存,默认为物理内存 1/64。
- 等价于 -XX:InitialHeapSize
-
-Xms
- 最大分配内存,默认为物理内存 1/4。
- 等价于 -XX:MaxHeapSize
-
-Xss
- 设置单个线程栈的大小,一般默认为 512k~1024k
- 等价于 -XX:ThreadStackSize
-
-Xmn
- 设置年轻代大小
-
-XX:MetaspaceSize
- 设置元空间大小
- 元空间的本质和永久代类似,都是对 JVM 规范中方法区对的实现。
- 不过元空间与永久代之间最大的区别在于:
元空间并不在虚拟机中,而是使用本地内存。
因此,默认情况下,元空间的大小仅受本地内存限制。
- -Xms10m -Xms10m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal
- 设置元空间大小
-
经典案例
-
-Xms128m -Xms4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrinitCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC
-
-
-XX:+PrintGCDetails
-
-XX:SurvivorRatio
- 设置新生代中 eden 和 S0/S1 空间比例
- 默认:-XX:SurvivorRatio=8,Eden:S0:S1=8:1:1
- 假如:-XX:SurvivorRatio=4,Eden:S0:S1=4:1:1
- SurvivorRatio 值就是设置 eden 区的比例占多少,S0/S1 相同
-
-XX:NewRatio
- 配置年轻代与老年代在堆结构的占比
- 默认:-XX:NewRatio=2 新生代占 1,老年代 2,年轻代占整个堆的 1/3
- 假如:-XX:NewRatio=4 新生代占1,老年代4,年轻代占整个堆的 1/5 ,NewRatio 值就是设置老年代的占比,剩下的的 1 给新生代。
-
-XX:MaxTenuringThresold
强引用、软引用、弱引用、虚引用
整体架构
强引用
当内存不足,JVM 开始垃圾回收,对于强引用的对象,就算是出现了 OOM 也不会对该对象进行回收。
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。
因此强引用是造成Java内存泄漏的主要原因之一。
对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。
软引用
软引用是一种相对强引用弱化了一些的引用,需要用 java.lang.ref.SoftReference 类来实现,可以让对象豁免一些垃圾收集。
对于只有软引用的对象来说:
当系统内存充足时它不会被回收;
当系统内存不足时它会被回收。
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!
弱引用
弱引用需要用 java.lang.ref.WeakReference 类来实现,它比软引用的生存期更短。
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
应用场景
假如有一个应用需要读取大量的本地图片:
- 如果每次读取图片都从硬盘读取则会严重影响性能。
- 如果一次性全部加载到内存中又可能造成内存溢出。
此时使用软引用可以解决这个问题。
设计思路是:用一个 HashMap 来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM 会自动回收这些缓存图片对象所占用的空间,从而有效地避免了 OOM 的问题。
Map<String, SoftReference<Bitmap>>imageCache =new HashMap<String,SoftReference<Bitmap>>();
虚引用
虚引用需要 java.lang.ref.PhantomReference 类来实现。
顾名思义,就是形同虚设
,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。
PhantomReference 的 get 方法总是返回 null,因此无法访问对应的引用对象。其意义在于说明一个对象已经进入 finalization 阶段,可以被 gc 回收,用来实现比 finalization 机制更灵活的回收操作。
换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
引用队列 ReferenceQueue
被回收前需要被引用队列保存下
总结
java提供了4引用类型,在到级回收的时候,都有自己各自的特点。
ReferenceQuede 是用来配合引用工作的,没有 ReferenceQueue 一样可以运行。
创建引用的时候可以指定关联的队列,当 Gc 释放对象内存的时候,会将引用加入到引用队列,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的可象的内存被回收之前采取必要的行动,这郁当于是一种通知机剂。
当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVM 允许我们在对象被销毁后,能一些我们自己想做的事情。
OOM 的那些事
- java.lang.StackOverflowError
- java.lang.outOfMemoryError:Java heap space
- java.lang.outOfMemoryError:GC overhead limit exceeded
- GC 回收时间过长时会触出 OutOfMemroyError。过长的定义是,超过98%的时间用来做 GC 并且回收了不到 2% 的堆内存。连续多次 GC 都只回收了不 2% 的堆内存情况下才会抛出。
- 假如不抛出 GC overhead Limit 错误会发生什么信况呢?
那就是 GC 清理的这么点内存很的会原次填满,迫使 GC 再执行,这就形成了恶性循环 CPU 使用率一直是 100%,而 GC 都没有任何效果。
- java.lang.outOfMemoryError:Direct buffer memory
- 导致原因:
写 NIO 程序经常使 ByteBuffer 来读取或者写入数据,这是一种基子通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分品堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。 - 这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来复制数据。
- ByteBuffer.allocate(capability) 第一种方式是分配 JVM 堆内存,属于 GC 管里范围,由于需要拷贝所以速度都相对较慢。
- ByteBuffer.allocteDirect(capability) 第二种方式是分配 OS 本地内存,不属于 GC 管辖范围,由于不需要内存拷贝所以速度相对较快。
- 但如果不断分配本越内存,堆内存很少使用,那么 JVM 就不需要执行 GC,DirectByteBuffer 对象们底不会被回收,这时侯堆内存充足,但本地内存可能己经使用光了,再次尝试分配本地内存就会出现 OutofMemoryError,那程序就直接崩溃了。
- 导致原因:
- java.lang.outOfMemoryError:unable to create new native thread
- 高并发请求服务器时,经常出现如上异常,准确的讲该 native thread 异常与对应的平台有关。
- 导致原因:
- 应用创建了大多线程了,一个应用进程创建了多个线程,超多系统承载极限
- 你的服务器并不允许你的应用程序创建这么多线程,Linux 系统默认允许单个进程可以创建的线程就是 1024 个,你的应用创建超过这个数量,就会报
java.lang.outOfMemoryError:unable to create new native thread
- 解决方法
- 想办法降低你应用程字创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数量降到最低;
- 如果有的应用,确实需要创建很多线程,远超过 Linux 系统的默 1024 个线程的限制,可以通过修改 linux 服务器配置,扩大Linux 默认限制。
- java.lang.outOfMemoryError:Metaspace
- Java 8 及之后的版本使用 Metaspace 来替代永久代。
- Metaspace 是方法区在 HotSpot 中的实现,他与持久代最大的区别就是:Metaspace 并不在虚拟机中而是使用本地内存,也即在 java 8 中,classe metadata,被存储在叫做 metaspace 的 native memory。
- 永久代存放了以下信息
- 虚拟机加载的类信息
- 常量池
- 静态变量
- 即时编译后的代码
JVM 垃圾回收
垃圾区分
引用计数算法
简介
对每个对象保存一个整形的引用计数器属性,用于记录对象被引用的情况。
对象 A,只要有一个对象引用了 A,则其引用计数器就加 1;当引用失效时,引用计数器就减一。当其引用计数器值为 0 时,即表示对象 A不可能再被使用,可以回收。
优点
- 实现简单,垃圾对象便于辨识。
- 判定效率高,回收没有延迟性。
缺点
- 增加了存储空间的开销。
- 每次赋值需要更新计数器,增加了时间的开销。
- 无法处理循环引用(Java 没有使用的主要原因)
可达性分析算法
简介
通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果每个对象到 GC Roots 间没有引用链向相连,或者用图论的话来说就是从 GC Roots 到这个对象不可达时,则证明此对象时可能在被使用的。
在可达性分析算法中,只有能够被根节点直接或间接访问的引用对象才是存活对象。
GC Roots
- 虚拟机栈中引用的对象;
- 本地方法栈中引用的对象;
- 类静态属性引用的对象;
- 常量引用的对象(字符串常量池里的引用);
- 所有被同步锁 synchronize 持有的对象;
注意:
使用可达性分析算法来判断对象是否可以被回收,那么分析工作必须在一个能够保障一致性的快照中进行,这不满足则不能保证分析结果的准确性。
这点也导致 GC 进行时必须“stop then world”的原因。
可以使用 MAT 查看 GC Roots。
垃圾回收算法
标记清除算法
简介
算法分为”标记“和”清除“两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收掉所有未被标记的对象。此操作会停止整个程序,也被称为 stop the world。
缺点
- 效率不算高;
- 在进行 GC 的时候需要停止整个应用程序,导致用户体验差。
- 清理出的空闲内存不是连续的,产生内存碎片,需要维护一个空闲列表。
何为清除?
所谓的清除并不是真正的置空,而是把需要清理的对象地址存放在空闲的地址列表里。下次有新对象需要加载时,判断空间是否足够,足够并存放。
复制算法
简介
为了解决标记清除算法在垃圾收集效率方面的问题。
其主要思想便是将活着的内存分为两块,每次只使用其中一块。在垃圾收集时,将正在使用的内存中的存活对象复制到未被使用的内存中,之后清除正在使用的内存中的所有对象,交换两个内存的角色,完成回收。
优点
- 没有标记和清除的过程,实现简单,运行高效。
- 复制过去后保证空间连续性,不会出现内存碎片。
缺点
- 需要两倍的内存空间。
- 对于 G1 这种,复制而不移动,意味着 GC 需要维护 region 之间对象引用关系,不管是内存占用或时间开销也不小。
标记整理算法
简介
复制算法的高效是建立在存活对象少,垃圾对象多的前提下。新生代比较适合,但是老年代对象存活较多,所以需要用其它算法。
从根节点开始,标记所有被引用对象。
将存活对象压缩到内存的一端,按顺序排放之后,清理边界外所有的空间。
优点
- 消除了标记整理算法中,内存区域分散的缺点,我们需要给对象重新分配内存时,JVM 只需要持有一个内存的起始地址即可。
- 无内存减半的高额代价。
缺点
- 从效率上来说,标记整理要低于复制算法。
- 移动对象的同时,需要调整引用的地址。
- 移动过程中,存在 stop the world。
分带收集算法
具体问题,具体分析。
简介
不同对象的生命周期是不一样的,因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
年轻代:复制算法。
老年代:标记—清除,标记—整理。
垃圾回收器
java -XX:+PrintCommandLineFlags -version:查看默认的 Java 收集器。
概述
- 垃圾收集器没有在规范中进行过多的规范,可以由不同厂商和不同版本的 JVM 来实现;
- JDK 的版本迭代;
- 角度不同,GC 版本不同;
- 按线程数,并行与串行;
- 工作模式分,并发式与独占式;
- 碎片处理方式:压缩式和非压缩式;
- 工作区域分:年轻代收集器和老年代收集器;
评估 GC 的性能指标:
- 吞吐量:运行用户代码的时间占总运行时间的比例。(总运行时间 = 垃圾回收时间 + 运行用户代码时间);
- 垃圾收集开销:吞吐量补数;
- 暂停时间:执行垃圾收集时,用户线程被暂停的时间。
- 收集频率;
- 内存占用:Java 堆区所占的内存大小;
- 快速:对象生命周期;
1,3,5 构成了一个“不可能三角”。一个优秀的收集器最多满足其中的两项。
其中吞吐量和暂停时间是重点。
吞吐量 VS 暂停时间
- 高吞吐量较好是因为这会让应用程序的的最终用户感觉只有应用程序在做“生产性”工作,直接上,吞吐量越高程序运行越快。
- 低暂停时间较好是因为可以给用户更好的体验,交互式应用程序,有时候甚至短暂的 200 毫秒暂停都可能打断终端用户体验。
- 吞吐量与暂停时间是一对相互竞争的目标
- 系统以吞吐量优先的话,那必然是降低内存回收的频率,但是这样会导致 GC 需要更长的暂停时间来执行内存回收。
- 相反,如果选择以低延迟优先,那么为了降低每次内存回收的暂定时间,也只有频繁的执行内存回收。
- 现在的标准:在最大吞吐量优先的情况下,降低暂停时间。
不同的垃圾回收器
串行回收器: Serial、Serial old;
并行回收器: Parnew、Parallel Scavenge、Parallel Old;
并发回收器: CMS、G1;
新生代:Serial Copying、ParNew、Parallel Scavenge
老年代:Serial Old、Parallel Old、CMS
整堆收集器:G1
Serial
它为但现场环境设计且只是用一个线程进行垃圾回收,会暂定所有的用户线程。所以不适合服务器环境。
HotSpot 中 Client 模式下默认新生代选择的选择器。
Serial 收集器采用的是复制算法,串行回收和“stop then world”机制执行垃圾回收。
优点:简单高效(单线程下),没有线程交互的开销。
Parallel
并行回收器,吞吐量优先。多个垃圾收集线程并行工作,用户线程是暂停的。
Parallel 的目标是达到一个可控制的吞吐量,可以高效利用 CPU,尽快完成程序的运行任务,主要适用于后台运算不需要太多交互的任务,常见在服务器环境性使用。如:执行批量处理,订单处理,科学计算等应用。
CMS
这款收集器是 Hotspot 中第一款真正意义上的并发收集器,第一次实现了让垃圾收集器线程与用户线程同时工作。
CMS 的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间。
CMS 执行步骤:
- 初始化标记和重新标记这两个阶段仍然需要执行 “stop the world”机制,只是时间不长。
- 由于最耗时的
并发标记
与并发清理
不需要暂停工作,所以整体来说是低停顿的。
优点
并发收集,低延迟。
缺点
- 会产生内存碎片;
- CMS 收集器对CPU资源非常敏感;
- CMS 收集器无法处理
浮动碎片
;
G1
G1(garbage first) 是为了不断扩大的内存和不断增加的处理器数量,进一步降低停顿时间,同时兼顾好良好的吞吐量。
G1 的目标是在延迟可控的情况下或得尽可能高的吞吐量,所以才担当成“全能收集器”;
G1 是一个并行回收器,它把内存分割为很多不同的区域(Region,物理上不连续);
G1 GC 有计划的避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的的 Region。
G1 的优势
分区算法
- 并行与并发
- 并行性:G1 在回收期可以多个 GC 线程同时工作,有效利用多核计算能力,此时用户线程 STW。
- 并发性:G1 拥有与程序交替执行的能力,部分工作可能和应用程序同时执行,一般来说,不会发生完全阻塞的情况。
- 分代收集
- 从分带上看,G1 依然属于分带型垃圾回收器,它会区分
年轻代
和老年代
,但从堆的结构上看,它不再要求整个 Eden 区、年轻代或老年代是连续的,也无固定大小。 - 将堆空间分为若干个区域(Region),这些区中逻辑包含了年轻代和老年代。
- 从分带上看,G1 依然属于分带型垃圾回收器,它会区分
- 空间整理
- Region 之间是复制算法,其整体上又是标记—压缩;
- 可预测的停顿时间模型
G1 的缺点
相对于 CMS,G1 不具备全方位压倒优势,比如在用户程序远行过程中,无论是为了垃圾收集产生的 内存占用
还是程序运行时的额外执行负载,都比 CMS 高。
**所以小内存 CMS 占优,大内存 G1 占优。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律