jvm介绍
什么是jvm
JVM(Java Virtual Machine,Java虚拟机),Java程序的跨平台特性主要是指字节码⽂件可以在任何具有Java虚拟机的计算机或者电⼦设备上运⾏,Java虚拟机中的Java解释器负责将字节码⽂件解释成为特定的机器码进⾏运⾏。因此在运⾏时,Java源程序需要通过编译器编译成为.class⽂件。
jvm结构
内存结构
程序计数器
程序计数器(Program Counter Register)也叫寄存器,他的作⽤是记住下⼀条jvm指令的执⾏地址。
特点:
-
是线程私有的
-
不会存在内存溢出
虚拟机栈
Java Virtual Machine Stacks (Java 虚拟机栈),每个线程只能有⼀个活动栈帧,对应着当前正在执⾏的那个⽅法。⼀个栈可以对应多个栈帧,当递归调⽤时,会存在多个栈帧。
栈溢出
当栈的内存沾满时,会导致溢出现象
导致栈溢出的原因:
-
栈帧过多导致栈内存溢出,栈多次递归调⽤
-
栈帧过⼤导致栈内存溢出(这种情况⽐较少)
线程运行诊断
案例一:诊断cpu占用过多
- 在linux命令中,⽤top命令定位哪个进程对cpu占⽤过⾼
DevinWang:~ devinwang$ top
PID COMMAND %CPU TIME #TH #WQ #PORT MEM PURG CMPRS PGRP
3333 top 1.7 00:00.82 1/1 0 25 3112K 0B 0B 3333
3328 java 100.4 00:34.33 26/1 1 91 21M 0B 0B 3265
3327 java 0.0 00:02.43 26 1 91 120M 0B 0B 3265
3318 ReportMemory 0.0 00:00.02 2 2 51 1396K 0B 228K 3318
发现进程号为3328的PID进程占⽤104%
-
查找进程中的哪⼀个线程占⽤过⾼
ps H -eo pid,tid,%cpu|grep 3464
我的mac执⾏该命令⽆效,所以⽆显示结果
-
jstack查看进程id
DevinWang:~ devinwang$ jstack 3328 2021-04-08 15:15:25 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.211-b12 mixed mode): "Attach Listener" #11 daemon prio=9 os_prio=31 tid=0x00007fc0e6055800 nid=0x5903 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Service Thread" #10 daemon prio=9 os_prio=31 tid=0x00007fc0e6893000 nid=0x5703 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE ...... "main" #1 prio=5 os_prio=31 tid=0x00007fc0e6004800 nid=0x1803 runnable [0x0000700002328000] java.lang.Thread.State: RUNNABLE at com.lenkee.lambda.TeachTest.main(TeachTest.java:11)
jstack⽤于打印出给定的java进程ID,在上⾯,最后⼀个#1显示了TeachTest类中的main⽅法的第11⾏正在运⾏,这⾥正是我写的while(true)代码的地⽅。
所以,jstack可以显示当前正在运⾏的代码位置。
排查线程死锁问题
-
也是跟上⼀步⼀样,找到java的进程PID。
-
和上⾯⼀样,⽤ jstack 进程号 查找运⾏线程
DevinWang:~ devinwang$ jstack 3398 "pool-1-thread-2" #12 prio=5 os_prio=31 tid=0x00007fe088857000 nid=0xa603 waiting for monitor entry [0x00007000057f0000] java.lang.Thread.State: BLOCKED (on object monitor) at com.lenkee.lambda.TeachTest.lambda$main$1(TeachTest.java:29) - waiting to lock <0x00000007957c6df8> (a java.lang.Object) - locked <0x00000007957c6de8> (a java.lang.Object) at com.lenkee.lambda.TeachTest$$Lambda$2/815033865.run(Unknown Source) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) "pool-1-thread-1" #11 prio=5 os_prio=31 tid=0x00007fe088853800 nid=0x5803 waiting for monitor entry [0x00007000056ed000] java.lang.Thread.State: BLOCKED (on object monitor) at com.lenkee.lambda.TeachTest.lambda$main$0(TeachTest.java:20) - waiting to lock <0x00000007957c6de8> (a java.lang.Object) - locked <0x00000007957c6df8> (a java.lang.Object) at com.lenkee.lambda.TeachTest$$Lambda$1/1401420256.run(Unknown Source) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
由上⾯可以看出,有两个线程状态为“BLOCKED”,说明发⽣阻塞,直接定位到下⾯指向的详细代码⾏(TeachTest.java:29),可以知道在这⾥出现阻塞现象,因此可以在这⾥判断死锁。
本地方法栈
对于一个运行中的Java程序而言,它还可能会用到一些跟本地方法相关的数据区。当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。
堆
堆内存是一片运行时数据区域,JVM使用该块内存为所有的类实例和数组分配内存。堆大小可以是固定的或者可变的。GC是一个自动内存管理系统,它将从(不可达)对象上收回堆内存。
堆内存溢出
当java运行时内存不够,会抛出异常
public static void main(String[] args) {
int count = 0;
try {
List<String> list = new ArrayList<>();
String a= "hello";
while (true){
list.add(a);
a = a+a;
count++;
}
} catch (Exception e) {
e.printStackTrace();
}
}
将idea执行参数设置为-Xms 8m,意思是运行内存为8M,所以很容易抛出异常
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at com.lenkee.lambda.Test.main(Test.java:19)
堆内存诊断
1. jps 工具
查看当前系统中有哪些 java 进程
DevinWang:~ devinwang$ jps
3280 RemoteMavenServer36
3664 Jps
3265
3643 Launcher
3308 KotlinCompileDaemon
3644 Demo1_4
此时Demo_4这个类进程号为3644
2. jmap 工具
查看堆内存占用情况 jmap - heap 进程id,详细介绍:jvm 性能调优工具之 jmap
DevinWang:~ devinwang$ jmap -heap 3644
由于mac系统的保护机制,无法访问进程信息,所以这里没有显示。
3. jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
4. jvisualvm工具,可视化检测虚拟机
里面有一个堆Dump功能,是抓取此刻的快照,抓取后可以分析里面的堆占用情况。详细介绍:Dump文件的生成和分析
方法区
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。JDK1.7以后移除永久代的工作。
常量池
-
常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
-
运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
StringTable
// 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
// ldc #2 会把 a 符号变为 "a" 字符串对象
// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象
public static void main(String[] args) {
String s1 = "a"; // 懒惰的,用的时候才放入串池中
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab") 放入堆中
String s5 = "a" + "b"; // javac 在编译期间的优化,结果已经在编译期确定为ab
System.out.println(s4 == s5); // 返回false
System.out.println(s3 == s5); // 返回true
}
特性
-
常量池中的字符串仅是符号,第一次用到时才变为对象
-
利用串池的机制,来避免重复创建字符串对象
-
字符串变量拼接的原理是 StringBuilder (1.8)
-
字符串常量拼接的原理是编译期优化
-
可以使用 intern (jdk1.8以后)方法,主动将串池中还没有的字符串对象放入串池
StringTable位置
Jdk1.7以后将StringTable放入,堆内存中。
StingTable的垃圾回收
当StringTable数量过多时,会发生垃圾回收。
StringTable性能调优
StringTable底层是一个hash table ,调优StringTable实际上就是调整它的buckets大小。
- 调整 -XX:StringTableSize=桶个数
- 考虑将字符串对象入池
直接内存
直接内存属于系统内存。
-
常见于 NIO 操作时,用于数据缓冲区
-
分配回收成本较高,但读写性能高
-
不受 JVM 内存回收管理
直接内存使用原理:
直接内存-内存释放
java中拿到Unsafe对象,调用系统分配内直接存,和释放直接内存分配功能。
垃圾回收
如何判断对象是垃圾
引用计数法
给对象增加一个计数器,当有引用它时,计数器就加一,当引用失效时,计数器就减一;
JVM并没有采用这种方式来判断对象是否已死
原因:循环引用会导致引用计数法失效,循环引用就是A类中一个属性引用了B类对象,B类中一个属性引用了A类对象,这样一来,就算你把A类和B类的实例对象引用置为null,它们还是不会被回收;
可达性分析
ava则是用了这种方法来判断是否需要回收对象;
此算法的核心思想为 : 通过一系列称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到GC Roots没有任何的引用链相连时(从GC Roots到这个对象不可达)时,证明此对象是不可用的;
四种引用
-
强引用
如:
Object object= new Object();
这种方式就是强引用,强引用在任何时候都不会被jvm回收,即使抛出OutOfMemoryError。
-
软引用(SoftReference)
如:
Object object= new Object(); SoftReference<Object> softReference = new SoftReference<>(object); Object result = softReference.get();
通过SoftReference的get方法来获取对象。软引用,在jvm内存不足的情况下会被回收。
-
弱引用(WeakReference)
如:
Object object= new Object(); WeakReference<Object> weakReference= new WeakReference<>(object); Object result = weakReference.get();
通过WeakReference的get方法来获取对象。在gc的时候就会被回收,不管内存是否充足。
-
虚引用(PhantomReference)
如:
Object object= new Object(); ReferenceQueue queue = new ReferenceQueue(); PhantomReference pr = new PhantomReference(object, queue);
虚引用和没有引用是一样的,需要和队列(ReferenceQueue)联合使用。当jvm扫描到虚引用的对象时,会先将此对象放入关联的队列中,因此我们可以通过判断队列中是否存这个对象,来进行回收前的一些处理。
-
终结器引用(FinalReference)
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 fifinalize方法,第二次 GC 时才能回收被引用对象
垃圾回收算法
标记清除法
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
优点:速度快
缺点:容易产生内存碎片
标记整理
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
优点:没有内存碎片
缺点:标记整理,会移动数据,改变了引用了地址,涉及复杂修改,速度慢
复制算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:
优点:不会产生垃圾碎片
缺点:占用双倍空间
分代垃圾回收
当前商业虚拟机的垃圾收集都采用“分代收集”(Generational Collection)算法,根据对象存活周期的不同将内存划分为几块并采用不用的垃圾收集算法。
一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清理”或者“标记—整理”算法来进行回收。
-
对象首先分配在伊甸园区域
-
新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
-
minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
-
当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
-
当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
相关参数
参数 | 含义 |
---|---|
-Xms | 堆初始大小 |
-Xmx 或 -XX:MaxHeapSize=size | 堆最大大小 |
-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) | 新生代大小 |
-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy | 幸存区比例(动态) |
-XX:SurvivorRatio=ratio | 幸存区比例 |
-XX:MaxTenuringThreshold=threshold | 晋升阈值 |
-XX:+PrintTenuringDistribution | 晋升详情 |
-XX:+PrintGCDetails -verbose:gc | GC详情 |
-XX:+ScavengeBeforeFullGC | FullGC 前 MinorGC |
垃圾回收器
Serial(串行垃圾回收器)
它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程(“stop the world”),所以不适合服务器环境。
设置命令:
-XX:+UseSerialGC = Serial + SerialOld
Parallel(并行垃圾回收器)
多个垃圾收集线程并行工作,此时用户线程时暂停的,适用于科学计算/大数据处理首台处理等弱交互场景。
设置命令:
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC
# 动态调整eden区和survivor区
-XX:+UseAdaptiveSizePolicy
# 调整垃圾回收时间大小比例
-XX:GCTimeRatio=ratio
# 调整垃圾回收时间吞吐量
-XX:MaxGCPauseMillis=ms
# 设置线程数
-XX:ParallelGCThreads=n
CMS(并发垃圾回收器)
用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要停顿用户线程,互联网公司多用它,适用于对响应时间有要求的场景。
设置命令:
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
# 设置并行的线程数
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
# 控制何时cms垃圾回收
-XX:CMSInitiatingOccupancyFraction=percent
# 重新标记前对新生代进行垃圾回收
-XX:+CMSScavengeBeforeRemark
G1(Garbage First)
G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收(Java8开始使用)
适用场景:
-
同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
-
超大堆内存,会将堆划分为多个大小相等的 Region
-
整体上是 标记+整理 算法,两个区域之间是 复制 算法
设置JVM 参数:
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
详细查看:深入理解Java G1垃圾收集器
垃圾回收调优
查看虚拟机运行参数,命令行运行:
java -XX:+PrintFlagsFinal -version |grep "GC"
调优领域
-
内存
-
锁竞争
-
cpu 占
确定目标
确定调优项目是什么类型
-
【低延迟】还是【高吞吐量】,选择合适的回收器
-
CMS,G1,ZGC
-
ParallelGC
-
Zing
最好少让GC发生
查看 FullGC 前后的内存占用,考虑下面几个问题:
-
数据是不是太多?
-
数据表示是否太臃肿?
对象用多少查多少
-
是否存在内存泄漏?
例如,不断加入static对象,可以考虑软、弱引用,使用第三方缓存
新生代调优
jvm性能调优一般从新生代开始,新生代的特点:
- 所有的 new 操作的内存分配非常廉价
- 死亡对象的回收代价是零
- 大部分对象用过即死
- Minor GC 的时间远远低于 Full GC
如何配置新生代?
新生代调节得尽可能大,但不是越大越好(太大了会增加标记垃圾的时间),需要与吞吐量设置适量的比例。
-
新生代能容纳所有【并发量 * (请求-响应)】的数据
-
幸存区要足够大,大到能保留【当前活跃对象+需要晋升对象】
-
让晋升阈值配置比较小,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold -XX:+PrintTenuringDistribution
Desired survivor size 48286924 bytes, new threshold 10 (max 10) - age 1: 28992024 bytes, 28992024 total - age 2: 1366864 bytes, 30358888 total - age 3: 1425912 bytes, 31784800 total ...
老年代调优
以 CMS 为例:
-
CMS 的老年代内存越大越好,预留更过空间,避免浮动垃圾引起的并发失败
-
先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代
-
观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
# 控制老年代触发垃圾回收比例 -XX:CMSInitiatingOccupancyFraction=percent
说明
笔记整理自黑马程序员学习视频,方便以后复习查阅。