JVM学习笔记

引言


定义: Java Virtual Machine - java程序的运行环境(java二进制字节码的运行环境)

好处:

  • 一次编写,到处运行
  • 自动内存管理,垃圾回收功能
  • 数组下标越界越界检查
  • 多态

比较:
jvm jre jdk
在这里插入图片描述


## 学习JVM有什么用
  • 面试
  • 理解底层实现原理
  • 中高级程序员的必备技能

常见的JVM

在这里插入图片描述


学习路线

在这里插入图片描述


内存结构

1.程序计数器

在这里插入图片描述
在这里插入图片描述
定义:是记住下一条jvm指令的执行地址

特点

  • 是线程私有的
  • 不会存在内存溢出的区

2.虚拟机栈

2.1定义:

Java Virtual Machine Stacks (Java 虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

栈帧:每个方法运行时需要的内存

一个栈由多个栈帧组成
在这里插入图片描述

问题辨析

1.垃圾回收是否涉及栈内存?

垃圾回收只是会回收我们堆内存的垃圾回收对象,栈内存一次次的方法调用产生的栈帧内存 
使用完会自动弹出栈,所以根本不需要

2.栈内存分配越大越好吗?

栈不是越大越好,划分大了只是越多的方法递归调用,线程会变少

3.方法内的局部变量是否线程安全?

局部变量是线程私有的  每个线程都会创建一个属于自己的栈 

 ==结论==:如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
 如果是局部变量引用了对象 并逃离了方法的作用范围,需要考虑线程安全

2.2栈内存溢出

  • 栈帧过多导致栈内存溢出
  • 栈帧过大导致栈内存溢出

栈内存溢出 java.lang.StackOverflowError
无限递归

2.3线程运行诊断

案例1:cpu占用过多
定位

  • 用top定位哪一个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu |grep 进程id (用ps进一步的命令定位是哪一个线程引起的 cpu过高)
  • jstack进程id
    • 可以根据线程id找到有问题的线程,进一步定位到问题代码的源代码行号

案例2:程序运行很长时间没有结果
可能发生了死锁


3.本地方法栈

在这里插入图片描述
本地方法栈
java虚拟机调用本地方法时需要给本地方法提供的一个内存空间 本地方法所使用的方法栈是本地方法栈

本地方法:
不是由java代码编写的方法

比如:Object 类中 native的方法就是本地方法没有实现的 是由c或者c++写的
在这里插入图片描述

在这里插入图片描述

4.堆

在这里插入图片描述

4.1定义

Heap 堆

  • 通过new关键字,创建对象都会使用 堆内存

特点

  • 它是线程共享的,堆中对象都需要考虑线程安全问题
  • 有垃圾回收机制

4.2堆内存溢出

java heap space
在这里插入图片描述

4.3 堆内存诊断

1.jps 工具

查看当前系统中有哪些java进程

2.jmap工具

 查看堆内存占用的情况  jmap - heap 进程id

3.jconsole工具

图形界面的,多功能的检测工具,可以连续监测

5.方法区

5.1定义

在这里插入图片描述

5.2组成

所有java虚拟区共享的区 它存储了 类的结构相关的信息.

在虚拟机启动时被创建
逻辑上是堆的组成部分

在这里插入图片描述

5.3方法区内存溢出

  • 1.8以前会导致永久代内存溢出
    在这里插入图片描述

  • 1.8以后会导致元空间内存溢出
    在这里插入图片描述

场景:

  • Spring
  • mybatis

5.4运行时常量池

  • 常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息

  • 运行时常量池,常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

常量池
在这里插入图片描述
运行时常量池会把这些变成真实的地址

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述new String(“c”) 会把c放入常量池对象

5.5StringTable 特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象

  • 利用串池的机制,来避免重复创建字符串对象

  • 字符串变量拼接的原理是StringBuilder ( 1.8)·字符串常量拼接的原理是编译期优化

  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池

    • 1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回
    • 1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把对象复制一份放入串池,会把串池中的对象返回

5.6Sstring Table 位子

1.8之前在永久代中
1.8之后在元空间中

JDK1.8中字符串常量池和运行时常量池逻辑上属于方法区,但是实际存放在堆内存中,因此既可以说两者存放在堆中,也可以说两则存在于方法区中,这就是造成误解的地方。

5.8String Table性能调优

调整 -XX:StringTableSize=桶个数

考虑将字符串是否入池
如果需要使用大量重复的字符串 考虑将字符串入池

6.直接内存

6.1 定义

不属于java虚拟机管理 属于操作系统的内存

Direct Memory

  • 常见于NIO操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高.
  • 不受JVM内存回收管理

6.3分配和回收原理

  • 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法

  • ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
    在这里插入图片描述


7.垃圾回收

1.如何判断对象可以回收

1.1引用计数法

有一个对象引用它给它值+1 一个对象不再引用则 -1 如果技术为0 会被回收

弊端如图: 如果2个对象相互引用 没有其他对象再进行引用 这2个计数不会为0所以不会被回收
在这里插入图片描述

1.2可达性分析算法

首先要确定一系列根对象(肯定不能被垃圾回收的对象) 垃圾回收前对堆中所有对象进行扫描 然后看看每一个对象是不是被根对象直接或者间接的引用 如果是就不能被回收 如果不是则会被 回收

例子 葡萄连在根上的不会被回收 掉落的则会被回收

  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
  • 哪些对象可以作为GC Root ?
    • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表
    • 方法区中的类静态属性引用的对象。
    • 方法区中常量引用的对象
    • 本地方法栈中N( Native方法)引用的对象

1.3 四种引用

  1. 强引用
    - 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

  2. 软引用(SoftReference)
    仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收,回收软引用 对象 可以配合引用队列来释放软引用自身

  3. 弱引用(WeakReference)
    仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象 可以配合引用队列来释放弱引用自身

  4. 虚引用(PhantomReference)
    必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存

  5. 终结器引用(FinalReference)
    无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象 暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象


软引用

在这里插入图片描述
结果
在这里插入图片描述

软引用配合引用队列

在这里插入图片描述
配合引用队列 把软引用的对象给删除了
在这里插入图片描述

弱引用实例
在这里插入图片描述


2. 垃圾回收算法

2.1标记清除

定义: Mark Sweep
速度较快

zMzQ4Nw==,size_16,color_FFFFFF,t_70#pic_center)

清除操作是记录对象所占用的起始结束地址记录 放在一个空闲的计时列表里 下次分配新对象的时候 在空闲的计时列表中找 有没有一块足够的空间容纳新的对象 如果有进行内存分配 并不是将里面的字节清0

优点:速度快 只记录清除的地址就好
缺点: 不连续 容易dde4照成内存碎片 会照成内存溢出的问题

2.2标记整理

定义:Mark Compact
速度慢

与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

在这里插入图片描述

2.3复制算法

定义:Copy
不会有内存碎片 需要占用双倍内存空间

第一步
在这里插入图片描述
把标记的内存移到To的地方
在这里插入图片描述
复制完成 清空左边的
在这里插入图片描述
并且交换From 和To的位子
在这里插入图片描述

小节

JVM会根据不同情况来采用 结合多种算法实现垃圾回收


3.分代垃圾回收

在这里插入图片描述

  • 对象首先分配在伊甸园区域

  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的 对象年龄加 1并且交换 from to

  • minor gc 会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)

  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW的时 间更长

3.1 相关 VM 参数

在这里插入图片描述


4.垃圾回收器

4.1 串行
  • 单线程
  • 堆内存较小 , 适合个人电脑

-XX:+UseSerialGC = Serial + SerialOld
会开启串行垃圾回收器
在这里插入图片描述

4.2 吞吐量优先
  • 多线程
  • 堆内存较大 ,多核cpu
  • 让单位时间内,stw的时间最短 0.2 0.2 =0.4
    在这里插入图片描述

并行的 :多个线程同一时刻干同一个事情

Parallel :复制算法并行回收
在这里插入图片描述

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC

  • 开关 1.8默认开启

-XX:+UseAdaptivesizePolicy

  • 采用自适应的大小调整策略 (伊甸园 幸运区 晋升阈值)

-XX:GCTimeRatio=ratio

  • 调整垃圾回收时间与总时间占比 1/(1+ratio)

-XX:MaxGCPauseMillis=ms

  • 最大暂停的毫秒数 和上面一个冲突

-XX:ParallelGCThreads=n

  • 指定垃圾回收的线程的个数

在这里插入图片描述

在这里插入图片描述

4.3 响应时间优先

-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld

  • 多线程
  • 堆内存较大,多核cpu
  • 尽可能让单次的stw的时间最短 0.1 0.1 0.1 0.1 0.1 =0.5

用户线程和垃圾回收器并发执行
并发:多个线程同一时刻,干多件事情

CMS

  • -XX:+UseConcMarkSweepGC (CMS是针对老年代进行回收的GC):
    并发的标记删除垃圾回收器 使用标记-清除算法的 ,当垃圾碎片多的时候 会出现并发失败问题 退化成 SerialOld 单线程垃圾回收器 (工作于老年代的垃圾回收器)
    在这里插入图片描述

在这里插入图片描述

ParNew

  • -XX:+UseParNewGC :关于新生代的复制算法的垃圾回收器
    在这里插入图片描述

  • SerialOld:基于标记整理的老年代垃圾回收器

-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
并行的线程垃圾回收线程个数 ~ 并发的线程垃圾回收器个数 一般是并行线程的1/3

-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

4.4G1

定义:Garbage First
2004 论文发布
2009 JDK 6u14 体验
2012 JDK 7u4 官方支持
2017 JDK 9 默认

适用场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
  • 超大堆内存,会将堆划分为多个大小相等的 Region ,每个区都会作为伊甸园 幸运区 老年代
  • 整体上是 标记+整理 算法,两个区域之间是 复制 算法

相关 JVM 参数
-XX:+UseG1GC (JDK8启动开关 jdk9就默认使用)

-XX:G1HeapRegionSize=size
设置区的大小

-XX:MaxGCPauseMillis=time
设置STW 默认是200ms

1) G1 垃圾回收阶

在这里插入图片描述

2) Young Collection

会 STW
白色表示空闲的区 ,E代表伊甸园区
在这里插入图片描述
把幸存的对象放入S幸存区
在这里插入图片描述

在这里插入图片描述

3) Young Collection + CM
  • 在 Young GC 时会进行 GC Root 的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定

-XX : InitiatingHeapOccupancyPercent=percent(默认45%)

标记为O的是老年代
在这里插入图片描述

4) Mixed Collection

会对 E、S、O 进行全面垃圾回收

  • 最终标记(Remark)会 STW
  • 拷贝存活(Evacuation)会 STW
    -XX:MaxGCPauseMillis=ms
    在这里插入图片描述
    老年代会先回收垃圾最多的回收的区域 其他有垃圾的区域后面慢慢清理
5) Full GC

SerialGC

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足发生的垃圾收集 - full gc

ParallelGC

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足发生的垃圾收集 - full gc

CMS
标记清除算法

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足

G1

  • 新生代内存不足发生的垃圾收集 - minor gc
  • 老年代内存不足
    :有一个阈值 当老年代内存和堆内存占比达到45%默认值,会触发并发标记阶段,以及后续的混合收集的阶段,工作过程中 回收速度高于新产生垃圾的时候 不叫 full GC 虽然也有暂停 时间相对于很短 , 当你的垃圾回收的速度跟不上垃圾产生的速度 ,并发收集失败,和cms类似。会退化成串行的收集器(标记整理算法)(多线程的),这时候叫full GC STW 会变长。 判断依据 打印GC日志 判断有full GC 字样才算发生full GC
6.Young Collection 跨代引用

新生代回收的跨代引用(老年代引用新生代)问题

在这里插入图片描述

卡表与 Remembered Set
在引用变更时通过 post-write barrier + dirty card queue
concurrent refinement threads 更新 Remembered Set

在这里插入图片描述

7) Remark

重新标记阶段

pre-write barrier(写屏障) + satb_mark_queue(队列)

:为了防止并发时 引用的对象已经扫描完 后再添加新的引用而没有被标记 当成垃圾回收了

对象引用发生改变时就会给 该对象加入一个写屏障 只要写屏障的对象发生了引用 写屏障的代码就会发生执行
会执行到写屏障指令 会将该对象放入一个队列 重新标记阶段会使用户暂停 会从队列中取出来 重新扫描

重新标记阶段
黑色的是处理完成的
灰色的是正在处理当中的
白色的是尚未处理的

在这里插入图片描述
最后没有引用的是白色

被引用的是黑色

例子
在这里插入图片描述
有强引用引用着B
将B变为黑色将来会存活

在这里插入图片描述
处理到C的时候 用户线程将C的引用断了 此时将C变为白色标记
在这里插入图片描述
并发标记结束后 C由于是白色会被当成垃圾回收掉

当在C的被标记完成后 C对象被当成是A的引用
在这里插入图片描述
这时候问题来了 C已经处理过了 A是黑色的也已经处理过了 等到整个并发标记结束后 会将C进行回收 但是C有一个强引用在引用着C 如果回收掉就伤害很大

此时Remark 重新标记阶段就是为了防止这样的现象发生

当对象引用发生改变时就会给 该对象加入一个写屏障 只要写屏障的对象发生了引用 写屏障的代码就会发生执行
在这里插入图片描述
此时A的一个属性 引用了C C的对象发生了引用
在这里插入图片描述
会执行到写屏障指令 将C加入到一个队列当中 把C变成灰色 表示没有处理完 并发标记结束后 会进入重新标记阶段 重新标记阶段会使用户暂停 会从队列中取出来 进行一个一个检查 这样就能防止并发标记的时候 有新被引用的对象给漏标记了 清理出错
在这里插入图片描述

8) JDK 8u20 字符串去重
  • 优点:节省大量内存
  • 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加

-XX:+UseStringDeduplication (默认打开)

String s1 = new String("hello"); // char[]{'h','e','l','l','o'} 
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}

  • 将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果它们值一样,让它们引用同一个 char[]
  • 注意,与 String.intern() 不一样
    • String.intern() 关注的是字符串对象
    • 而字符串去重关注的是 char[] 在
    • JVM 内部,使用了不同的字符串表
9) JDK 8u40 并发标记类卸载

所有对象都经过并发标记后,就能知道哪些类不再被使用当一个类加载器的所有类都不再使用,则卸 载它所加载的所有类 -

XX:+ClassUnloadingWithConcurrentMark 默认启用

一般对于自定义类加载器

10) JDK 8u60 回收巨型对象

一个对象大于 region 的一半时,称之为巨型对象

  • G1 不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1 会跟踪老年代所有 incoming(卡表) 引用,这样老年代 incoming 引用为0 的巨型对象就可以在新生 代垃圾回收时处理掉

当老年代中的卡表引用了巨型对象时 会被标记被脏卡

在这里插入图片描述

11) JDK 9 并发标记起始时间的调整
  • 并发标记必须在堆空间占满前完成,否则退化为 FullGC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent (设置老年代超过堆内存的阈值触发 并发收集 默认45%)
  • JDK 9 可以动态调整
    • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
    • 进行数据采样并动态调整
    • 总会添加一个安全的空档空间
12) JDK 9 更高效的回收

250+增强
180+bug修复
https://docs.oracle.com/en/java/javase/12/gctuning


5.垃圾回收调优

预备知识

  • 掌握 GC 相关的 VM 参数,会基本的空间调整
  • 掌握相关工具
  • 明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则
5.1 调优领域

内存
锁竞争
cpu 占用
io

5.2 确定目标

【低延迟】还是【高吞吐量】,选择合适的回收器
CMS,G1,ZGC (相应时间优先)
ParallelGC (高吞吐量垃圾回收器)
Zing

5.3 最快的 GC

答案是不发生 GC

  • 查看 FullGC 前后的内存占用,考虑下面几个问题
    • 数据是不是太多?
      • resultSet = statement.executeQuery(“select * from 大表 limit n”)
  • 数据表示是否太臃肿?
    • 对象图
    • 对象大小 16 Integer 24 int 4
  • 是否存在内存泄漏?
    使用缓存尽量不要使用Map 使用第三方的缓存 比如redis
    • static Map map =
      使用软弱引用
    • 第三方缓存实现
5.4 新生代调优

新生代的特点

  • 所有的 new 操作的内存分配非常廉价

    • TLAB thread-local allocation buffer
  • 死亡对象的回收代价是零

  • 大部分对象用过即死

  • Minor GC 的时间远远低于 Full GC

    new一个对象时 对象首先会在伊甸园分配 时间非常快 每个线程都会在伊甸园中分配一个私有的区域 TLAB 线程局部私有缓存区 new一个对象时先会检查TLAB缓存区中是否有可用内存 如果有优先会在这个区域中进行对象分配 为什么这么做呢 因为对象分配也有线程安全问题 线程1用这块内存 在还没结束的时候线程2也要用这个内存 这会照成内存的分配混乱 因此在做对象的分配时 也要做个线程的安全保护 当然这个操作是由jvm进行操作的

    TLAB:让每个线程用自己私有的内存进行伊甸园分配 这样的话多个线程同时进行对象分配时也不会产生这个 对内存占用的干扰

所有垃圾回收器都是采用的是复制算法

新生代内存优化更大一些

建议新生代堆内存占总堆的25%(1/4)-50%(1/2)

■新生代能容纳所有【并发量(请求-响应)】的数据*

■幸存区大到能保留【当前活跃对象+需要晋升对象】

■晋升阈值配置得当,让长时间存活对象尽快晋升
-XX:MaxTenuringThreshold=threshold (最大晋升阈值)
-XX :+PrintTenuringDistribution (幸运区详情)

在这里插入图片描述

5.5 老年代调优

以 CMS 为例

  • CMS 的老年代内存越大越好
  • 先尝试不做调优,如果没有 Full GC 那么已经…,否则先尝试调优新生代
  • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大1/4 ~ 1/3 -
    • XX:CMSInitiatingOccupancyFraction=percent
      (老年代达到老年代内存45%(默认值)时发生full GC)
      一般设置为75%-80% 预留20%-25%的浮动垃圾
5.6 案例

■ 案例1 Full GC和Minor GC频繁

问题分析:

说明空间紧张 如果是新生代空间紧张 高峰期来了 大量对象被创建 新生代被塞满了 幸存区空间紧张了 晋升阈值降低 本来生存周期很短的对象 也会被晋升代老年代去 情况就恶化了 老年代存了很多生存周期很短的对象 触发full GC 新生代内存太小了
解决方法:

增大新生代内存大小 同时增大了幸存区的空间以及新生代对象的阈值 这样让生命周期较短的对象 尽量留在新生代 不进入老年代

■ 案例2请求高峰期发生Full Gc,单次暂停时间特别长(CMS)

CMS重新标记时会扫描整个堆内存
在进行Full Gc 重新标记之前 对新生代的对象 先做一次垃圾回收 减少新生代垃圾数量 可用减少重新标记的时间

■ 案例3老年代充裕情况下,发生FullGC (1.7)

jdk1.7是使用永久代的,1.8后使用元空间,是使用操作系统的空间的,所以内存充足。
所以是永久代空间不足,可以增加永久代和初始值和最大值。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值