JVM知识点整理

视频参考
定义: java程序的运行环境(java二进制字节码的运行环境)

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

内存结构

JVM知识点整理_第1张图片

1.程序计数器

1.1作用

记住下一条JVM指令的执行地址。

Java源码经过编译形成二进制字节码(JVM指令),JVM指令经过解释器生成机器码,机器码在CPU中执行。

程序计数器通过寄存器实现。

1.2特点

  • 线程私有:每个线程有自己的程序计数器
  • 不会存在内存溢出

2.虚拟机栈

:线程运行需要的内存空间
栈帧:每个方法运行时需要的内存(参数、局部变量、返回地址)

2.1定义

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

问题辨析

  1. 垃圾回收是否涉及栈内存(否。每次方法调用结束后,都会自动弹出栈,自动回收掉)
  2. 栈内存分配越大越好吗?(否。栈内存越大,线程数越少,因为物理内存是固定的)
  3. 方法内的局部变量是否线程安全?(如果方法内局部变量没有逃离方法作用范围,那么是线程安全的;如果是局部变量引用了对象并逃离方法的作用范围,需要考虑线程安全问题)

2.2栈内存溢出

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

2.3线程运行诊断

案例1:CPU占用过多

定位

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

案例2:程序运行很长时间没有结果
jstack 进程id

3.本地方法栈

给本地方法提供一个内存的空间。很多本地方法都是用c语言等编写的。

4.堆

4.1定义

Heap堆

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

特点

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

4.2堆内存溢出

4.3堆内存诊断

  1. jps工具

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

  1. jmap工具

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

  1. jconsole工具

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

案例

  • 垃圾回收后,内存占用仍然较高
    jmap, jconsole=>jvitsualvm

5.方法区

5.1定义

存储类、类加载器、常量池等类的信息。

5.2组成

1.6中,永久代作为方法区的实现,永久代存储类、类加载器、运行时常量池。
1.8中,方法区使用元空间实现,不占用堆内存,不由JVM管理内存,移出到本地内存。

5.3方法区内存溢出

  • 1.8以前会导致永久代内存溢出(-XX:MaxPermSize=8m)
  • 1.8之后会导致元空间内存溢出(-XX:MaxMetaspaceSize=8m)

5.4运行时常量池

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

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

5.5StringTable特性

  • 常量池中的字符串仅是符号,第一次用到时才变为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
    • 1.8将这个字符串对象放入串池,如果有则不会放入,
      如果没有则放入串池,会把串池中的对象返回
    • 1.6将这个字符串对象放入串池,如果有则不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

5.6StringTable位置

JVM知识点整理_第2张图片

5.7StringTable垃圾回收

5.8StringTable性能调优

底层是HashTable。

桶的数量越大,冲突越少,运行用时越少。

调整 -XX:StringTableSize=桶个数

6.直接内存

6.1定义

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

6.2分配和回收原理

  • 使用了unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
  • ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,name就会由ReferenceHandler线程通过Cleaner的clean方法调用clean方法调用freeMemory来释放直接内存

垃圾回收

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

1.1引用计数法

引用一个对象,计数加1;释放引用,计数减1。存在循环引用问题,使得引用无法减为0。

1.2可达性分析算法

  • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象;

  • 扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收;

1.3四种引用

  • 强引用:只有当所有引用该对象的连接都断开时才会被回收
  • 软引用:只有软引用连接,没有其他强引用引用它,当垃圾回收时内存不够会将其回收
  • 弱引用:当没有其他强引用引用时,只要发生垃圾回收,都会将弱引用引用的对象回收
  • 虚引用:必须配合引用队列使用,虚引用(eg:垃圾回收时Cleaner放入引用队列,其中有一个直接内存地址,其不归JVM管理,调用Unsafe.freeMemory方法进行释放)
  • 终结器引用:当没有强引用引用这个对象时,有虚拟机创建对应的终结器引用,当这个对象被垃圾回收时,会将终结器引用加入到引用队列。再由一个优先级很低的线程finallize查看引用队列中是否有终结器引用,发现后会找到这个对象,调用finallize方法。调用完后下次垃圾回收就可以回收这个对象占用的内存。

2.垃圾回收算法

2.1标记清除算法

先标记,再清除。清除只需要将起始和结束地址做一个记录即可,速度快。但是容易产生内存碎片。

2.2标记整理算法

整理过程中将对象前移,整理回收的空间。没有内存碎片,但是整理设计对象移动,因此效率较低。

2.3复制算法

两块区域(From和To),先标记垃圾,接着将对象复制到To区,最后将From区的垃圾全部回收,并将From和To交换区域。优点是不产生内存碎片,缺点是占用双倍内存空间。

3.分代回收

新生代:伊甸园、幸存区From、幸存区To
老年代

创建一个新的对象首先存放在伊甸园,当伊甸园满时,触发一次Minor GC。采用复制算法将剩下的对象放在幸存区To,并将幸存对象寿命加1,伊甸园剩下的对象就被回收掉了。交换幸存区From和To的位置。

再次向伊甸园存放对象,等到满时再次触发Minor GC。这时将伊甸园里幸存的对象放入幸存区To,寿命值加1;将幸存区From存活的对象放入幸存区To,寿命值加1,此时为2。将剩下的垃圾清除掉。再次交换From和To的位置。幸存区中寿命(最大寿命是15)值超过阈值,就将其晋升到老年代。

Minor GC会引发stop the world

老年代内存不足,会先尝试Minor GC,若空间仍不足会触发Full GC。如果还是不足会报异常:out of memory。

3.1相关VM参数

JVM知识点整理_第3张图片

4.垃圾回收器

4.1串行

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

开启命令:
-XX:+UserSerialGC = Serial + SerialOld
前者工作在新生代,使用复制算法
后者工作在老年代,使用标记整理算法

4.2吞吐量优先

  • 多线程
  • 堆内存较大,多核CPU
  • 让单位时间内,STW的时间最短

-XX:+UserParallelGC -XX:+UserParallelOldGC(开启一个自动开启另一个)
-XX:UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n

4.3响应时间优先

  • 多线程
  • 堆内存较大,多核CPU
  • 尽可能让STW单次时间最短

-XX:+UseConcMarkSweepGC -XX:+UseParNewGC SerialOld
-XX:ParallelGCThreads=n -XX:ConcGCThreads = threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark

4.4G1

适用场景

  • 同时注重吞吐量和低延迟,默认暂停目标是200ms
  • 超大堆内存,会将堆划分为多个大小相等的Region
  • 整体上是标记整理算法,两个区域之间是复制算法

相关JVM参数

  • -XX:+UseG1GC
  • -XX:G1HeapRegionSize=size
  • -XX:MaxGCPauseMillis=time
1)G1垃圾回收阶段

新生代回收-新生代回收+并发标记-混合回收

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

-XX:InitiatingHeapOccupancyPercent=percent

4)Mixed Collection

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

  • 最终标记会STW
  • 拷贝存活会STW

-XX:MaxGCPauseMillis=ms

5)Full GC
  • SerialGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足,并发收集失败才会full gc
  • G1
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足,老年代占用内存达到阈值,触发并发标记和混合收集。垃圾产生速度大于回收速度->full gc
6)Young Collection跨代引用

将老年代分为card,被新生代引用的设为dirty card

7)Remark

5.垃圾回收调优

  • 掌握GC相关的VM参数,会基本的空间调整
  • 掌握相关工具

5.1调优领域

  • 内存
  • 锁竞争
  • cpu占用
  • io

5.2确定目标

  • 低延迟还是高吞吐量,选择合适的回收器
  • CMS,G1,ZGG
  • ParallelGC

5.3最快的GC是不发生GC

5.4新生代调优

  • 新生代的特点
    • 所有的new操作的内存分配非常廉价
    • 死亡对象的回收代价是0
    • 大部分对象用过即死
    • MInor GC的时间远远低于Full GC

新生代内存并非越大越好,如果过大则老年代会相应减少,这时如果内存不足会触发full gc

5.5老年代调优

以CMS为例

  • CMS的老年代内存越大越好
  • 先尝试不做调优,如果没有Full GC那么已经…,否则先尝试调优新生代
  • 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3

类加载与字节码技术

内存模型

1.java内存模型

JMM定义了一套在多线程读写共享数据时(成员变量、数组),对数据的可见性、有序性和原子性的规则和保障。

1.1原子性

i++;
i–;

1.2问题分析

以上命令在java中并不是原子操作。

1.3解决方法

synchronize

2.可见性

2.1退不出的循环

变量发生变化,但是程序却仍从高速缓存中读取数据,使得循环无法退出。

2.2解决方法

volatile(易变关键字):可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。

2.3可见性

保证在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况。

3.有序性

volatile修饰的变量,可以禁用指令重排。

4.CAS与原子类

4.1CAS

底层:CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令。

4.2乐观锁与悲观锁

  • CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量。
  • synchronized是基于悲观锁的思想:最悲观的估计,得防着其他线程来修改共享变量。

5.synchronized优化

5.1轻量锁

轻量级锁与偏向锁不同的是

  • 轻量锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁
  • 每次进入退出同步块都需要CAS更新对象头
  • 争夺轻量级锁失败时,自旋尝试抢占锁

加锁过程

  • 线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为displaced mark word。然后线程尝试使用CAS将对象头的mark word替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁,则轻量级锁会膨胀成重量级锁。

5.2锁膨胀

  • 当竞争线程尝试占用轻量级锁失败多次之后,轻量级锁就会膨胀为重量级锁,重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒它。

5.3重量锁

重量锁在JVM中又叫监视器(Monitor),它很像C中的Mutex,出了具备其互斥的功能,还实现了信号量的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列,前者负责做互斥,后者用于线程同步。

5.4偏向锁

  • 会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。

  • 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

你可能感兴趣的:(深入理解JVM虚拟机)