JVM学习笔记之崛起

编程入门 行业动态 更新时间:2024-10-23 16:20:55

JVM<a href=https://www.elefans.com/category/jswz/34/1770117.html style=学习笔记之崛起"/>

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 关键字,创建对象都会使用堆内存。
特点
它是线程共享的,堆中对象都需要考虑线程安全的问题。
有垃圾回收机制。

堆内存溢出

内存溢出案例代码

堆内存诊断

  1. jps 工具
    查看当前系统中有哪些 java 进程
  2. jmap 工具
    查看堆内存占用情况 jmap - heap 进程id
  3. 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工具进行分析

四种引用

  1. 强引用
    只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
  2. 软引用(SoftReference)
    仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用
    对象
    可以配合引用队列来释放软引用自身
  3. 弱引用(WeakReference)
    仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
    可以配合引用队列来释放弱引用自身
  4. 虚引用(PhantomReference)
    必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,
    由 Reference Handler 线程调用虚引用相关方法释放直接内存
  5. 终结器引用(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

垃圾回收器

  1. 串行
    单线程
    堆内存较小,适合个人电脑
    -XX:+UseSerialGC = Serial + SerialOld

  2. 吞吐量优先
    多线程
    堆内存较大,多核 cpu
    让单位时间内,STW 的时间最短 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高
    -XX:+UseParallelGC ~ -XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicy -XX:GCTimeRatio=ratio -XX:MaxGCPauseMillis=ms -XX:ParallelGCThreads=n

  3. 响应时间优先(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学习笔记之崛起

本文发布于:2024-03-10 10:19:36,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1727703.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:学习笔记   JVM

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!