学习笔记之崛起"/>
JVM学习笔记之崛起
JVM学习笔记
- JVM组成
- JVM内存结构
- 本地方法栈
- 堆
- 堆内存溢出
- 堆内存诊断
- 方法区
- 方法区内存溢出
- StringTable
- 直接内存
- 垃圾回收
- 引用计数法
- 可达性分析算法
- 四种引用
- 垃圾回收算法
- 标记清除
- 标记整理
- 复制
- 分代垃圾回收
- 相关 VM 参数
- 垃圾回收器
- G1
JVM组成
JVM内存结构
1. 程序计数器
Program Counter Register 程序计数器(寄存器)
作用:
是记住下一条jvm指令的执行地址
0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // --
4: aload_1 // out.println(1);
5: iconst_1 // --
6: invokevirtual #26 // --
9: aload_1 // out.println(2);
10: iconst_2 // --
11: invokevirtual #26 // --
14: aload_1 // out.println(3);
15: iconst_3 // --
16: invokevirtual #26 // --
19: aload_1 // out.println(4);
20: iconst_4 // --
21: invokevirtual #26 // --
24: aload_1 // out.println(5);
25: iconst_5 // --
26: invokevirtual #26 // --
29: return
特点:
是线程私有的
不会存在内存溢出
2. 虚拟机栈
Java Virtual Machine Stacks (Java 虚拟机栈)
每个线程运行时所需要的内存,称为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
注意点:
1.垃圾回收不涉及栈内存,栈内存就是一次次方法的调用所产生的内存,栈帧内存在每次方法调用结束后都会被弹出栈,也就会自动被回收,所以不需要垃圾回收。
2.栈内存不是越大越好,因为栈越大线程数就会越少
设置线程堆栈大小(以字节为单位)
不同系统默认大小不同
Linux/x64(64 位):1024 KB
macOS(64 位):1024 KB
Oracle Solaris/x64(64 位):1024 KB
Windows:默认值取决于虚拟内存
以下示例以不同的单位将线程堆栈大小设置为 1024 KB:
-Xss1m
-Xss1024k
-Xss1048576
栈内存溢出场景
栈帧过多会导致栈内存溢出,如果递归方法没有设置一个正确的推出条件,则会导致栈帧过多。
栈帧过大也会导致栈内存溢出,但是一般一个方法不太容易把栈占满。
占内存溢出错误
案例:
public class Demo_3 {public static void main(String[] args) {method();}public static void method(){method();}
}
报错:
Exception in thread “main” java.lang.StackOverflowError
at Demo_3.method(Demo_3.java:8)
at Demo_3.method(Demo_3.java:8)
at Demo_3.method(Demo_3.java:8)
at Demo_3.method(Demo_3.java:8)
线程运行诊断
用top定位哪个进程对cpu的占用过高
通过如下命令查看cup占用线程
ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
jstack 进程id
可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
列出进程所有运行的线程
找出cpu占用最高的线程,将线程号转换成16进制,就能在堆栈中找到对应的堆栈信息,定位到出问题的代码位置
程序运行很长时间没有结果定位
线程死锁会导致该问题,可以通过以上方式定位
本地方法栈
本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。
本地方法栈是一个后入先出(Last In First Out)栈。
由于是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。
本地方法栈会抛出 StackOverflowError 和 OutOfMemoryError 异常。
堆
Heap 堆
通过 new 关键字,创建对象都会使用堆内存。
特点
它是线程共享的,堆中对象都需要考虑线程安全的问题。
有垃圾回收机制。
堆内存溢出
内存溢出案例代码
堆内存诊断
- jps 工具
查看当前系统中有哪些 java 进程 - jmap 工具
查看堆内存占用情况 jmap - heap 进程id - jconsole 工具
图形界面的,多功能的监测工具,可以连续监测
方法区
虚拟机结构
方法区内存溢出
1.8 以前会导致永久代内存溢出
- 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
- -XX:MaxPermSize=8m
1.8 之后会导致元空间内存溢出
- 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
- -XX:MaxMetaspaceSize=8m
StringTable
StringTable 特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是编译期优化
- 可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池
- 1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
1.6 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池, 会把串池中的对象返回
1.6与1.8的区别,可以通过如下代码验证
public static void main(String[] args) {String s1 = "a";String s2 = "b";String s3 = "a" + "b"; // abString s4 = s1 + s2; // new String("ab")String s5 = "ab";String s6 = s4.intern();// 问System.out.println(s3 == s4); // falseSystem.out.println(s3 == s5); // trueSystem.out.println(s3 == s6); // trueString x2 = new String("c") + new String("d"); // new String("cd")x2.intern();String x1 = "cd";// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢System.out.println(x1 == x2);}
StringTable 位置
- 1.6在永久代的常量池中
- 1.8在堆中
-XX:+UseGCOverheadLimit
允许使用限制 JVM 在OutOfMemoryError抛出异常之前花费在 GC 上的时间比例的策略。默认情况下启用此选项,OutOfMemoryError如果超过 98% 的总时间花在垃圾收集上并且回收了不到 2% 的堆,则并行 GC 将抛出一个异常。当堆很小时,此功能可用于防止应用程序长时间运行而几乎没有进展。要禁用此选项,请指定选项-XX:-UseGCOverheadLimit。
默认开关是开启的,如果关闭需要关闭
-XX:-UseGCOverheadLimit 关闭开关
XX:+UseGCOverheadLimit 开启开关
/*** 演示 StringTable 位置* 在jdk8下设置 -Xmx10m -XX:-UseGCOverheadLimit* 在jdk6下设置 -XX:MaxPermSize=10m*/
public class Demo1_6 {public static void main(String[] args) throws InterruptedException {List<String> list = new ArrayList<String>();int i = 0;try {for (int j = 0; j < 260000; j++) {list.add(String.valueOf(j).intern());i++;}} catch (Throwable e) {e.printStackTrace();} finally {System.out.println(i);}}
}
StringTable 垃圾回收
如果内存空间不足会触发垃圾回收,StringTable中没有被引用的字符串常量会被回收,可以通过如下代码验证
/*** 演示 StringTable 垃圾回收* -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc*/
public class Demo1_7 {public static void main(String[] args) throws InterruptedException {int i = 0;try {for (int j = 0; j < 100000; j++) { // j=100, j=10000String.valueOf(j).intern();i++;}} catch (Throwable e) {e.printStackTrace();} finally {System.out.println(i);}}
}
设置参数-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc后执行,输出结果如下
其中[GC (Allocation Failure)的信息为由于内存空间不足,触发的垃圾回收机制的信息。
[GC (Allocation Failure) [PSYoungGen: 2042K->488K(2560K)] 2042K->723K(9728K), 0.0017752 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2536K->488K(2560K)] 2771K->790K(9728K), 0.0007996 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2536K->504K(2560K)] 2838K->806K(9728K), 0.0010235 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
100000
HeapPSYoungGen total 2560K, used 1745K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)eden space 2048K, 60% used [0x00000000ffd00000,0x00000000ffe366f0,0x00000000fff00000)from space 512K, 98% used [0x00000000fff00000,0x00000000fff7e030,0x00000000fff80000)to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)ParOldGen total 7168K, used 302K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)object space 7168K, 4% used [0x00000000ff600000,0x00000000ff64b8c0,0x00000000ffd00000)Metaspace used 3236K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 351K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets : 20011 = 160088 bytes, avg 8.000
Number of entries : 13238 = 317712 bytes, avg 24.000
Number of literals : 13238 = 588752 bytes, avg 44.474
Total footprint : = 1066552 bytes
Average bucket size : 0.662
Variance of bucket size : 0.662
Std. dev. of bucket size: 0.814
Maximum bucket size : 6
StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 23421 = 562104 bytes, avg 24.000
Number of literals : 23421 = 1391800 bytes, avg 59.425
Total footprint : = 2434008 bytes
Average bucket size : 0.390
Variance of bucket size : 0.401
Std. dev. of bucket size: 0.633
Maximum bucket size : 4
StringTable 性能调优
如果常量比较多,建议StringTableSize设置大一点,StringTableSize最小值是1009,如果StringTableSize设置的大一点,性能会有明显提升。
/*** 演示串池大小对性能的影响* -Xms500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=1009*/
public class Demo1_24 {public static void main(String[] args) throws IOException {try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {String line = null;long start = System.nanoTime();while (true) {line = reader.readLine();if (line == null) {break;}line.intern();}System.out.println("cost:" + (System.nanoTime() - start) / 1000000);}}
}
/*** 演示 intern 减少内存占用* -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics* -Xsx500m -Xmx500m -XX:+PrintStringTableStatistics -XX:StringTableSize=200000*/
public class Demo1_25 {public static void main(String[] args) throws IOException {List<String> address = new ArrayList<>();System.in.read();for (int i = 0; i < 10; i++) {try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream("linux.words"), "utf-8"))) {String line = null;long start = System.nanoTime();while (true) {line = reader.readLine();if(line == null) {break;}address.add(line.intern());//字符串入池,占用少量内存 //address.add(line);//字符串不入池,占用大量内存}System.out.println("cost:" +(System.nanoTime()-start)/1000000);}}System.in.read();}
}
直接内存
Direct Memory
- 常见于 NIO 操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受 JVM 内存回收管理
分配和回收原理 - 使用了 Unsafe 对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory 方法
ByteBuffer 的实现类内部,使用了 Cleaner (虚引用)来监测 ByteBuffer 对象,一旦
ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler 线程通过 Cleaner 的 clean 方法调
用 freeMemory 来释放直接内存
垃圾回收
如何判断对象可以回收
引用计数法
一个对象被引用,计数就会加1,一个对象不在引用,就减1,如果没有对象引用,那么计数就是0,就意味着可以被回收。
但是引用计数法有个问题,如果两个对象相互引用,就无法被回收,所以现在JVM虚拟机不使用引用计数法,一般python使用引用计数法,所以python的垃圾回收不如JVM的的垃圾回收快。
可达性分析算法
Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
哪些对象可以作为 GC Root ?
收集dump包,再通过MAT去分析哪些是GC Root
演示代码:
/**
- 演示GC Roots
*/
public class Demo2_2 {public static void main(String[] args) throws InterruptedException, IOException {List<Object> list1 = new ArrayList<>();list1.add("a");list1.add("b");System.out.println(1);System.in.read();list1 = null;System.out.println(2);System.in.read();System.out.println("end...");}
}
- 首先通过jps获取进程ID
通过如下命令生产dump文件
jmap -dump:format=b,live,file=1.dump 3472
format=b:二进制文件
live:只关注存活对象,已回收的不关注,并且收集前先做一次垃圾回收
再通过MAT工具进行分析
四种引用
- 强引用
只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收 - 软引用(SoftReference)
仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用
对象
可以配合引用队列来释放软引用自身 - 弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
可以配合引用队列来释放弱引用自身 - 虚引用(PhantomReference)
必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,
由 Reference Handler 线程调用虚引用相关方法释放直接内存 - 终结器引用(FinalReference)
无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象
暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize
方法,第二次 GC 时才能回收被引用对象
垃圾回收算法
标记清除
定义: Mark Sweep
速度较快
会造成内存碎片
标记整理
定义:Mark Compact
速度慢
没有内存碎片
复制
定义:Copy
不会有内存碎片
需要占用双倍内存空间
分代垃圾回收
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加 1并且交换 from to
- minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)
- 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时间更长
相关 VM 参数
含义 | 参数 |
---|---|
堆初始大小 | -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 |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
垃圾回收器
-
串行
单线程
堆内存较小,适合个人电脑
-XX:+UseSerialGC = Serial + SerialOld
-
吞吐量优先
多线程
堆内存较大,多核 cpu
让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高
-XX:+UseParallelGC ~ -XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicy -XX:GCTimeRatio=ratio -XX:MaxGCPauseMillis=ms -XX:ParallelGCThreads=n
-
响应时间优先(CMS垃圾回收器)
多线程
堆内存较大,多核 cpu
尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
- -XX:+UseConcMarkSweepGC 老年代并行并发CMS垃圾回收器
- -XX:+UseParNewGC ~ SerialOld 年轻代垃圾回收器,在出现问题时使用SerialOld回到单线程回收
- -XX:ParallelGCThreads=n 并行GC数量
- -XX:ConcGCThreads=threads
- -XX:CMSInitiatingOccupancyFraction=percent
- -XX:+CMSScavengeBeforeRemark
放两个图,都是关于CMS流程的,第二个关于STW更直观一些
G1
定义:Garbage First
2004 论文发布
2009 JDK 6u14 体验
2012 JDK 7u4 官方支持
2017 JDK 9 默认
CMS与G1垃圾回收器的区别
因为CMS垃圾回收器的三色标记会出现漏标的BUG,所以G1会在并发处理时增加堆栈快照,在最终标记阶段处理漏标问题,另外G1在最后使用筛选回收(分区模型优势),筛选最有价值的区域,提高了垃圾回收的效率。
-
CMS垃圾回收模型
-
G1垃圾回收模型
更多推荐
JVM学习笔记之崛起
发布评论