JVM学习笔记

1. JDK JRE JVM的区别

JDK(所有内容,编译时环境) -> JRE(运行时环境,少了Tools相关的内容) -> JVM(虚拟机) 包含关系

在这里插入图片描述

2. JVM体系

2.1 作用:JAVA虚拟机从软件层面屏蔽了底层硬件指令层面的细节

2.2 JVM的组成:类加载子系统、执行引擎、运行时数据区、本地接口

Java文件首先通过编译器编译成字节码文件,类加载将字节码文件加载到运行时数据区的方法区中。
字节码只是一套指令集合不能直接被计算机底层所理解执行,所以需要特定的命令解释器执行引擎将字节码翻译层系统底层指令,再交由CPU去执行,
而这个过程需要调用其他语言的本地库接口来实现整个程序的功能

2.3 JVM内存结构

在这里插入图片描述

2.3.1 线程方面

  • 线程共享:方法区、堆

  • 线程隔离:栈、程序计数器、本地方法栈

2.3.2 运行时数据区组成

  • 程序计数器:保存当前线程执行信息,用于记录各个线程执行的字节码地址(行号)(分支、循环、跳转、异常、线程恢复等都依赖于计数器)
  • 虚拟机栈:每个线程创建时都会创建一个虚拟机栈,每次方法调用都会在内存中开辟一段空间用于存储方法运行时需要的数据,称为栈帧,每个栈帧会包含几块内容:局部变量、操作数栈、动态链表、返回地址
  • 本地方法栈:管理本地方法的调用,本地方法指的是 非Java方法,一般的本地方法是用C实现的
  • 方法区(元空间/永久代):加载的类信息、常量、静态变量,逻辑上存在,物理上不存在
  • :对象实例和数组

在这里插入图片描述

2.4 类加载器和双亲委派机制

在这里插入图片描述

双亲委派机制:为了防止内存中存在多份同样的字节码,使用了双亲委派机制(不尝试自己加载类,而是请求委托给父加载器去完成,依次向上,直到父类加载器都没加载到才由自身加载)
JDK中的本地方法类一般由根加载器状态,JDK中实现扩展的类一般由扩展加载器装载,而程序中的类文件则由系统加载器实现装载
类加载的规则:如果一个类由类加载器A加载,那么它的依赖类也会由相同的类加载器加载

package jvm;

public class Demo01 {
    public static void main(String[] args) {

        Demo01 demo01 = new Demo01();
        Class<? extends Demo01> demo01Class = demo01.getClass();
        ClassLoader classLoader = demo01Class.getClassLoader();
        System.out.println(classLoader);
        ClassLoader parent = classLoader.getParent();
        System.out.println(parent);
        ClassLoader parentParent = parent.getParent();
        System.out.println(parentParent);
    }
}

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@7106e68e
null

  • 为什么需要性能调优:物理空间是有限的,不能无限扩展,因为有垃圾回收机制的存在,可以通过参数的调整在性能和空间之间寻找一个最佳的平衡点
  • 为什么需要分代:大部分对象都死得早,只有少部分对象会存活很长时间。为了使STW的持续时间尽可能短以及提高并发式GC所能应付的内存分配速率,在堆内存上都会在物理或逻辑上进行分代
package jvm;

import java.util.UUID;

public class Demo01 {
//    -Xms5m -Xmx5m -XX:+PrintGCDetails
    public static void main(String[] args) {
        long maxMemory = Runtime.getRuntime().maxMemory();
        long totalMemory = Runtime.getRuntime().totalMemory();
        System.out.println("max=" + maxMemory + "字节," + (maxMemory/1024/1024) + "M");
        System.out.println("total=" + totalMemory + "字节," + (totalMemory/1024/1024) + "M");

        String st = "Hello World";
        while (true) {
            st = st + (UUID.randomUUID().toString());
        }

    }


}

2.5 遇到OOM怎么排查

  • 内存快照分析工具 MAT、Jprofile
  • Debug 一行行代码分析

MAT、Jprofile的作用

  • 分析Dump内存文件,快速定位内存泄漏位置
  • 获得堆中的数据
  • 获得大的对象
package jvm;

import java.util.ArrayList;
import java.util.List;

public class Demo02 {
    byte[] bytes = new byte[1*1024*1024];
    //-Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError
    public static void main(String[] args) {
        List<Demo02> list = new ArrayList<>();
        int count = 0;
        while (true) {
            count = count + 1;
            list.add(new Demo02());
        }
    }
}

2.6 常用命令

  • 打印GC日志:-XX:+PrintGCDetails
  • 生成Dump文件:-XX:+HeapDumpOnOutOfMemoryError

注:Java内存模型和Java内存结构的区别
Java内存模型是和并发相关的,它是为了屏蔽底层细节而提出的规范,希望在上层(Java层面)操作内存时不同操作系统的表现是一致的。
Java内存结构又称运行时数据区,它描述着我们的class文件被加载到jvm后,各个分区的逻辑结构是如何,以及各个分区承担了什么作用

2.7 GC 垃圾回收

垃圾:对象不再被使用,就是垃圾
GC Roots:当前活跃的栈帧指向堆里的对象引用、类的静态变量引用、被Java本地方法引用的对象等……总之,GC Roots是一组一定活跃的引用

  • 引用计数法:使用计数器,当对象被引用时就+1,但对象引用失败就-1,当计数器为0时,说明对象未被使用,可以回收。缺点:当对象相互引用,就无法定位该对象是否应该被回收
  • 可达性分析法:从GC Roots 开始向下搜索,当对象到GC Roots之间没有任何引用相连时,说明对象不可用,可以回收
  • 标记清除法:标记没有被GC Roots 引用的对象直接清除,缺点是会造成很多内存碎片
  • 标记复制法:把标记存活的对象复制到另一块空间,清除原来的整块空间,缺点是内存利用率低,始终需要一块空着的区域
  • 标记整理算法:把存活的对象移动到一边,把垃圾移动到另一边,再将垃圾全部剔除掉
  • 年轻代使用的算法:标记复制法
  • 堆内存分配比:新生代占堆空间1/3,老年代占堆空间2/3;Eden区占新生代比例8,Survivor区占新生代比例2,From和To区各占Survivor区1
  • 对象从创建到回收的过程:首先对象不断被创建并存储在Eden区,当Eden区空间不足时会触发minor GC,回收不被使用的对象,并将幸存的对象放入Survivor区的from区,同时给对象标记幸存的次数。当Eden区再次空间不足后,Eden区触发minor GC,幸存的对象进入Survivor区的from区,此时from区也触发minor GC,幸存的对象复制到to区然后清空from区,此时from区变成to区to区变成from区,幸存的次数+1,当幸存次数达到15次(默认15次可通过-XX:MaxTenuringThreshold 参数调整)后,对象会晋升到 老年代。当老年代空间也不足的时候,就会触发 full GC,同时 stop the world(STW)所有线程都会短暂停顿,当所有空间都满了之后就是OOM
  • stop the world:在执行垃圾回收算法时,Java应用程序除了垃圾收集器线程之外的线程全部挂起,虽然垃圾收集器不断地在改进,缩短STW时间、允许部分情况下GC线程和正常线程并发执行等,但仍然无法完全避免STW
  • 对象进入老年代的条件:幸存年龄达到15或者很大的对象
  • 新生代对象引用老年代对象:HotSpot虚拟机老的GC是要求整个GC堆在连续的地址空间上,所以会有一条分界线,一侧是老年代一侧是新生代,通过地址就可以判断对象在哪个分代上,当做Minor GC时,从GC Roots触发,当发现老年代对象时就不往下走了,因此不会全堆扫描。物理分代可以通过物理地址直接区分。
  • 老年代引用新生代对象:HotSpot虚拟机下有个 card table 卡表来避免全局扫描老年代对象。堆内存的每一小块区域形成卡页,卡表实际上就是卡页的集合,当判断一个卡页中存在跨代引用的情况时则标记为 脏页 ,每次minor GC 的时候只需要到卡包中找到脏页,将脏页加入 GC Roots,就不用去遍历整个老年代的对象了。
    在这里插入图片描述
  • 年轻代的垃圾回收器:Seria(单线程)、Parallel Scavenge(多线程)、ParNew(单线程)
  • 老年代的垃圾回收器:Seria Old、Parallel Old、CMS

2.8 CMS垃圾收集器 Concurrent Mark Sweep

  • 初始标记:标记GC Roots直接关联的对象以及年轻代指向老年代的对象,只标记一层不向下追溯,会STW
  • 并发标记:从GC Roots向下追溯,标记所有可达的对象,比较耗时,不会STW
  • 并发预处理:上一个阶段是并发处理的,因此可能存在引用关系改变却未被标记的对象,此阶段就是重新标记处理这些对象,此过程可能触发minor GC来减少扫描时间
  • 重新标记:stop the world,全堆扫描标记
  • 并发清除:回收垃圾,用户线程不会停止,可能会不断产生垃圾(只能交由下次GC处理,这个过程产生的垃圾称为“浮动垃圾”)
  • 缺点:空间利用率低,标记清除法会产生很多空间碎片;空间需要预留,CMS垃圾收集器可以一边收集垃圾一边处理用户线程,那就需要过程中保证有充足的空间给用户线程使用,如果预留空间不够会触发Seria Old 对老年代的垃圾进行回收,导致很长时间的停顿;碎片整理需要很多的时间,这过程显然是需要STW的

2.9 G1垃圾收集器

  • G1垃圾收集器的世界,堆的划拨是逻辑划分的,分代的概念还是一样奏效
    在这里插入图片描述

  • 堆被逻辑划分为多个小的区域,每一个区域叫Region

  • Humongous区域:大对象区域,用来存储特别大的对象(大于Region内存的一半),一旦发现没有引用指向大对象,可以直接在年轻代的minor GC中被回收掉

  • 逻辑划分多个小区域,更容易控制 Stop The World 的时间,老年代和新生代的空间大小并不固定,会动态根据最大停顿时间进行调整(通过参数配置),

  • Minor GC:一样是在Eden区空间不足后触发,也会STW,动态地改变年轻代Region区的个数可以控制minor GC 的开销,步骤:根扫描、更新&&处理Rset、复制对象

    • 根扫描:类CMS垃圾收集器的初始标记的过程
    • 更新&&处理Rset:CMS通过 卡表 来存储老年代对象对年轻代对象的引用从而避免全表扫描老年代,G1则是通过Rset,在每一块Region区中都有一块Rset区域,存储了其他Region引用了当前Region的对象关系,而对于年轻代的Region它的Rset只存储了来自老年代的引用关系,而对于老年代的Rset也只会存储老年代对它的引用。此步骤即是处理RSet的信息并且扫描,将老年代持有年轻代对象的引用都列入GC Roots,避免被回收掉。
    • 复制对象:将存活的对象往空的Survivor区或老年区放,其他的Eden区进行清理
  • Mixed GC:当堆空间占用率达到一定阈值后触发(默认45%,可通过参数调整),依赖全局并发标记统计后的Region数据。一定会回收年轻代,并会采集一部分老年代的Region进行回收,是一个“混合”GC

    • 全局并发标记:初始标记(STW)、并发标记、最终标记(STW)、清理(STW),有点类似CMS垃圾收集器的过程
    • 初始标记:共用了Minor GC的Stop The World(Mixed GC一定会发生Minor GC),复用了扫描GC Roots 的操作,这个过程新生代和老年代都会扫描
    • 并发标记:GC线程和用户线程并发执行,GC线程收集各个Region的存活对象信息,从GC Roots往下追溯,查找整个堆存活的对象,比较耗时
    • 重新标记:标记并发标记阶段引用关系发生变化的对象,使用SATB算法,简单理解为在GC开始的时候,为存活对象做了一次快照,在并发标记阶段记录每一次发生引用关系变化的旧的引用值,然后在重新标记阶段只扫描发生过变化的引用,看有没有对象存活,加入到GC Roots
    • 清理:清点和重置标记,会根据停顿预测模型(其实就是设置的停顿时间)来决定本次回收多少Region,一般来说Mixed GC会选择全部的年轻代Region以及部分回收价值高的老年代Region(其实就是垃圾多的)进行采集,最后清除依旧是通过复制的方式做的,一次回收并不是将所有垃圾进行回收,而是会根据停顿时间做出选择
  • full GC:Mixed GC中无法跟上用户线程分配内存的速度,导致老年代无法继续进行Mixed GC,就又会降级到serial old GC 来对整个堆进行垃圾回收

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值