JVM

深入理解JVM

  1. 第二章——JVM内存模型
  2. 第三章——GC算法和收集器
  3. 第四章——调优工具
  4. 第七章——类加载

1. JVM内存模型:堆、栈、程序计数器、方法区

  1. 堆:是jvm中最大的一块内存空间,堆主要存放用new创建的对象,gc回收主要回收的就是堆中的对象空间。堆又可以分为年轻代和老年代
    1. 年轻代用于存放新生对象和短期存活的对象。年轻代又可以分为eden区、FromSurvivor区和ToSurvivor区。对象优先分配到eden区,当eden区空间满了之后,会执行MirrorGc(复制算法),回收年轻代的空间。如果对象经过多次gc依旧存活(默认15次),会移动到老年代
    2. 老年代主要存放长期存活对象和大对象。如果是大对象,直接分配到老年代。当老年代空间满了之后,会执行FullGc,回收堆空间,如果FullGc之后空间依旧不足,抛出OOM
  2. 栈:分为java虚拟机栈和本地方法栈。
    1. java虚拟机栈执行java方法,创建栈帧,存放着局部变量表,局部变量表上存放着基本数据类型和引用数据类型的引用指针。java虚拟机栈用于执行线程。可能导致OOM。
    2. 本地方法栈执行System方法
    3. java虚拟机栈可能出现栈溢出,因为每次递归产生的信息都会存放在java虚拟机栈
  3. 程序计数器:每个线程都有一个程序计数器,是当前线程在所执行的字节码的行号指示器。单线程下可有可无,多线程中CPU通过时间片轮转调度方式来调用线程,发生中断时,当前线程在字节码执行的位置会被保存,当线程再次获得时间片时,从保存的位置继续执行,实现快速恢复状态。如果是java方法,记录字节码文件的行号,如果是本地方法,值为0
  4. 方法区:用于存放类信息,常量、静态变量和编译后的代码,1.8之前用永久代实现,容易发生OOM。1.8之后被元空间替代,元空间是本地内存。可以通过-XX:MetaSpaceSize设置元空间大小。
    1. 方法区也会参与GC回收,但条件苛刻,会进行常量池回收和类卸载
      1. 当堆中不存在该类的实例
      2. 加载该类的类加载器被回收
      3. class对象以及被回收
  5. 运行时常量池:属于方法区

2. 哪些是线程共享的,哪些是线程私有的

  1. 线程共享:堆、方法。因为存放着对象和常量、静态变量。
  2. 线程私有:栈、程序计数器。

3. 哪些区域可能发生OOM:堆、栈、方法区

  1. 堆:
    1. 老年代存在大对象,导致无法继续存入对象。可以通过jmap查看进程的堆内存使用情况。如果是存在大对象,因为对象必须要存活着,所以可以通过-Xmx和-Xms调整堆内存大小
    2. 存在未被回收的引用,发生内存泄漏。可以通过可达性分析查找泄漏对象,通过Gcroots查找
    1. 大量创建线程,导致java虚拟机栈内存不足。可以通过-Xss调整栈大小。
    2. 如果栈递归深度过深,会发生栈溢出
  2. 方法区
    1. 运行时常量池存放大量的String
    2. 运行时创建大量的类,存放大量类信息到方法区
    3. 1.8之前,永久代和方法区绑定,可以通过调整永久代的大小解决:-XX:MaxPermSize
    4. 1.8之后,方法区被元空间替代,通过调用元空间大小解决:-XX:MetaSpaceSize

4. 各种变量在JVM的位置:局部变量、全局变量、静态变量

  1. 局部变量:基本数据类型变量和值都在栈。引用数据类型的变量在栈,值在堆
  2. 全局变量:变量和值都在堆中
  3. 静态变量、常量:方法区

5. 类加载过程:加载、连接、初始化

  1. 加载:将字节码文件加载到jvm,将类的静态变量、静态方法、常量和编译后的代码存入方法区,生成一个class对象
  2. 连接
    1. 验证:检查字节码文件符合Java虚拟机规范,确保加载后不会发生错误
    2. 准备:在方法区中为静态变量分配内存空间并设初值为0
    3. 解析:将常量的符号引用转化未直接引用。"a"会被替换为内存地址
  3. 初始化:为静态变量赋值

6. 类加载器:

  1. BootstrapClassLoader:启动类加载器。加载jre中的rt.jar
  2. ExtensionClassLoader:拓展类加载器。加载lib中的ext文件
  3. ApplicationClassLoader:应用类加载器。加载环境变量classpath和java.class.path的类

7. 双亲委派机制,如何破坏,全盘委托

  1. 流程:当某个类加载器接到加载任务时,先检查该类是否被加载,如果已经被加载,返回class对象,否则将加载任务交给父类加载器进行加载。当父类加载器无法加载类时,才会由子类加载器进行加载
  2. 优点:避免类重复加载
  3. 破坏双亲委派:重写ClassLoader类的loadClass()方法
  4. 不想破坏双亲委派:重写ClassLoader类的findClass()方法
  5. 全盘委托:当ClassLoaderA加载一个类时,如果没有指定使用ClassLoaderB加载类的相关依赖类,那么这个类的相关依赖类也会由ClassLoaderA进行加载

8. Java引用类型,GC标记方法

  1. 强引用:不会被GC回收,new创建的对象是强引用

  2. 弱引用:一定会被GC

  3. 虚引用:随时会被GC

  4. 软引用:内存不足时会被GC

  5. 引用计数法:每个对象都有一个引用计数器,有引用计数+1,释放引用计数-1。0表示可以回收。

    1. 存在循环引用问题:如果两个对象相互引用,那么它们的计数都不为0,无法被回收
    2. 优点:实时计算,几乎没有延迟
    3. 缺点:实时计算,开销大,吞吐量下降
  6. 可达性分析:从Gcroots开始往下搜索,当一个对象到gcroots中没有任何一条引用链与之相连,表明该引用可以被回收。

    1. GCroot引用链类似树结构,GcRoot会与需要存活的对象相连,而可以被回收的对象则不会相连
    2. 对象会经历两次标记才能被确定为可回收。第二次标记通过finalize()方法判断
  7. Gcroots:java虚拟机栈的(栈帧的局部变量表)引用对象,本地方法栈的引用,方法区的静态变量引用,方法区的常量引用

9. GC算法

  1. 标记清除算法
    1. 过程:先对需要存活的对象进行标记,标记完成之后清除没有被标记的对象
    2. 问题:如果堆中存在大量对象,标记和清除效率低,清除过程会产生空间碎片
  2. 标记复制算法
    1. 过程:先将内存分成大小相等的两块,先使用其中一块,当这一块空间满了之后,先堆需要存活的对象进行标记,然后清除没有被标记的对象,将存活的对象移到另一快
    2. 问题:运行高效,空间浪费
  3. 标记整理算法
    1. 过程:先对需要存活的对象进行标记,标记完成后将标记的对象移到一边,然后清除边界以外的空间
    2. 优点:不会产生空间碎片
    3. 缺点:用于老年代,需要标记的对象多且移动开销大,效率低
  4. 分代收集算法:目前hot spot虚拟机使用分代收集算法管理堆空间。分为老年代和新生代。
    1. 新生代:朝生夕死,存活率低,使用复制算法
    2. 老年代:长期存活与大对象,存活率高,使用标记整理算法

10. 为什么复制算法比标记整理要快,为什么老年代不用复制算法

  1. 因为复制算法是用于年轻代,年轻代对象大多存活时间都比较短且都比较小,所以标记的对象少而且占用空间小,因此复制算法可以将内存分成两块也不用担心放不下。且移动对象的开销也小。因此复制算法的运行速度快
  2. 因为老年代的对象大多都是长期存活且大对象,如果使用复制算法,内存开销大,再者就算标记的过程也会慢,因为老年代的对象大多都是要存活的,并且老年代存在大对象,复制移动的开销也大。

11. GC收集器

  1. CMS:并发标记清除
    1. 过程:初始标记、并发标记、重新标记、并发清除
    2. 初始标记和重新标记会发生STW,但因为是并发,停顿短
    3. 优点:并发标记清除,效率高,停顿短
    4. 缺点:
      1. 和用户线程并发执行,会占用一部分线程,程序变慢
      2. 基于标记清楚算法,会产生空间碎片
    5. 使用:-XX:UseConcMarkSweep=true
  2. G1:并行并发不分代,将堆内存划分为大小相等的Region区域,进行回收,基于标记整理
    1. 过程:初始标记(简单标记一下)、并发标记(可达性分析)、最终标记(STW)、筛选清除
    2. 初始标记和最终标记会发生STW,但因为是并行并发,停顿短
    3. 优点:并行并发效率高,基于标记整理,不会产生空间碎片

12. 为什么会发生STW

  1. 在进行gc时,需要移动对象(比如复制算法移动对象),进而会导致对象引用发生更新,为了保证引用更新的正确性,需要在进行gc时暂停其他所有线程。
  2. gc在进行回收垃圾时,其他线程要停止才能清除干净,要是一边清除以便产生垃圾,会影响gc的效率和gc的负担。尤其是在进行标记时

13. 为什么要进行分代GC

  1. 堆对中存活时间不同的对象采用不同的gc策略,管理起来更高效
  2. 对于年轻代,对象大多存活时间比较短且对象小,一般采用复制算法,存活对象少且空间小,使用较小的内存开销和移动开销就能进行管理
  3. 对于老年代,对象存活时间长且对象大,不适合采用复制算法。采用标记整理算法。

14. JVM参数

  1. -Xms:堆大小,1/64
  2. -Xmx:最大堆大小,1/4
  3. -XX:NewRatio:年轻代和老年代比值:1:2
  4. -XX:SurvivorRatio:eden区、FS区、TS区比值:8:1:1
  5. -Xss:线程大小
  6. -XX:MetaSpaceSize:元空间大小
  7. -XX:MaxTeruningThreashold:年轻代对象最大存活次数,默认15次

15. JVM内存分配原则和空间担保机制

  1. 内存分配原则
    1. 对象优先分配到年轻代的eden区
    2. 大对象直接进入老年代
    3. 年轻代中长期存活对象进入老年代
  2. 空间担保机制:在年轻代进行MirrorGc之前,先检查老年代的内存是否足够存放年轻代的所有对象,如果不够,老年代进行FullGc

16. FullGc触发

  1. System.gc
  2. 老年代空间不足
  3. 空间担保机制

17. 如果频繁触发FullGc要怎么办

  1. 通过jmap下载dump文件(前提要开启headDumpBeforeFullGc),通过visualVM导入dump文件进行分析
jmap -dump:format=b,file=xxx.dump [进程id]

18. System.gc一定会执行吗

  1. 不一定会执行,jvm会记下这个请求,但是不一定不执行,当System.runFinallization()返回true时,表明可以执行System.gc()。
System.gc();
runtime.runFinalizationSync();
System.gc();

19. jvm故障处理工具:jps、jmap、jstack

  1. jps:查看虚拟机进程,列出当前正在运行的虚拟机进程
  2. jmap:用于生成堆转存快照dump文件。还可以查看堆内存使用情况和堆所使用的gc收集器。可用于解决堆的oom问题
jmap -histo [进程id]
  1. jstack:生成线程快照,可以定位死锁问题。
jstack -l [进程id]
  1. jstat:统计信息监视工具。显示当前进程的堆内存使用情况、gc次数。
jstat -gcutil [进程id]
E代表eden区、S0、S1.
O代表老年代
P代表永久代
YGC代表年轻代MirrorGC
YGCT代表MirrorGc耗时
FGC代表FullGc
FGCT代表FullGc耗时
GCT代表总耗时
  1. jhat:用于查看jmap导出的dump文件,但是一般不用
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容