「笔记」Java 虚拟机

Java 内存区域与内存溢出异常

image-20190628160404148

  • 程序计数器、Java 虚拟机栈、本地方法栈 均为线程私有
  1. 程序计数器

    • 当前线程所执行的字节码的行号指示器
    • 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令
      • 分支、循环、跳转、异常处理、线程恢复等功能均依赖计数器
    • Java多线程是通过线程轮流切换并分配处理器执行时间的方式来实现
      • 通过计数器保证线程切换后能恢复到正确的执行位置
    • 若执行的是 Native 方法,计算器值则为空(Undefined)
  2. Java 虚拟机栈

    • 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息
      • 从方法调用直至执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程
    • 局部变量表存放了编译期可知的各种基本数据类型、对象引用、returnAddress类型
    • 通过 -Xss 虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小
    • 该区域可能会抛出 StackOverflowError 异常 与 OutOfMemoryError 异常
  3. 本地方法栈

    • 本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为(Native)本地方法服务
    • 该区域可能会抛出 StackOverflowError 异常 与 OutOfMemoryError 异常
    • 几乎所有的对象实例以及数组都在堆上分配
      • JIT编译器编译与逃逸分析除外
    • 现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法
      • 从内存回收角度来看,由于现代的垃圾收集器基本都是采用分代收集算法,可分为:新生代、老年代
        • 更细致可分为:Eden空间、From Survivor空间、To Survivor空间(默认8:1:1)
      • 从内存分配角度来看,线程共享的 Java 堆可划分出多个线程私有的分配缓冲区(TLAB)
    • Java 堆可以处于物理上不连续的内存空间,只要逻辑上是连续即可
    • 通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小
    • 该区域可能会抛出 OutOfMemoryError 异常
  4. 方法区

    • 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
    • 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现
    • JDK 1.7 开始,将字符串常量池移入Java堆中
    • 该区域可能会抛出 OutOfMemoryError 异常
  5. 运行时常量池

    • 方法区中的一部分,Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域
    • 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern () 方法
    • 该区域可能会抛出 OutOfMemoryError 异常
  6. 直接内存

    • 直接内存不是 Java 虚拟机运行时数据区的一部分,也不在规范中定义的内存区域

    • 在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存

      • 通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作
    • 显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据

    • 该区域可能会抛出 OutOfMemoryError 异常

垃圾收集器与内存分配策略

  • 垃圾收集主要是针对堆和方法区进行(非线程私有,共享区域
  1. 判断对象是否可被回收

    • 用计数算法

      • 原理:给对象中添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1
        • 任何时刻引用计数器为 0 的对象就是不可能再被使用的
      • 缺陷:无法解决相互循环引用的问题(Java 虚拟机未采用此算法的主要原因
    • 可达性分析算法

      image-20190628160502793

      • 原理:以GC Roots为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收
      • 可作为GC Roots的对象包括以下几种
        • 虚拟机栈(栈帧中的本地变量表)中引用的对象
        • 本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象
        • 方法区中类静态属性引用的对象
        • 方法区中常量引用的对象
    • 再谈引用

      • 无论是通过引用计数算法还是可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关
      • Java 提供了四种强度不同的引用类型
        • 强引用:永不回收
        • 软引用:内存不足时,才回收
        • 弱引用:在下一次GC时一定被回收
        • 虚引用:幽灵(幻影)引用
          • 一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象
          • 唯一目的:能在这个对象被回收时收到一个系统通知
    • 生存还是死亡(finalize())

      • 当一个对象可被回收时,会判断是否需要执行 finalize() 方法,可通过在 finalize() 方法中被引用,实现“自救”
        • 不需要执行 finalize() 的情况:未覆盖 finalize() 方法 或 已执行过一次 finalize() 方法
      • 类似 C++ 的析构函数,用于关闭外部资源,但运行代价很高,不确定性大等,不建议使用
    • 回收方法区

      • 方法区主要存放永久代对象,回收性价比低
      • 永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类
      • 类满足以下3个条件才可以被回收
        • 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例
        • 加载该类的 ClassLoader 已经被回收
        • 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法
      • 在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能,以保证永久代不会溢出
  2. 垃圾收集算法

    • 标记 - 清除算法

      image-20190628160548401

      • 主要不足的点
        • 标记和清除过程效率都不高
        • 标记清除后产生大量不连续的内存碎片,导致无法给大对象分配内存
    • 复制算法
      image-20190628160621019

      • 主要不足的点:内存只能使用一半
      • 扩展关联信息:Eden:Survivor = 8:1(默认),当 Survivor 不足时通过分配担保机制进入老年代
    • 标记 - 整理算法
      image-20190628160636253

      • 在标记完后,让所有存活的对象都向一端移动,然后清除端边界以外的内存
    • 分代收集算法

      • 现在的商业虚拟机采用分代收集算法,根据对象存活周期将内存划分为几块,不同块采用适当的收集算法
      • 一般将堆分为新生代和老年代
        • 新生代使用:复制算法(朝生夕死)
        • 老年代使用:标记 - 清除 或者 标记 - 整理算法
  3. 垃圾收集器

    image-20190628160652884

    • 如果两个收集器之间存在连线,则说明他们可以搭配使用

    • Serial 收集器
      image-20190628160729489

      • 单线程收集器,只会使用一个线程进行垃圾收集工作,且工作时需暂停其他所有工作现场
        • STOP THE WORLD
      • Client 场景下的默认新生代收集器
        • 优点:简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率
    • ParNew 收集器

      image-20190628160747760

      • Serial 收集器的多线程版本
      • Server 场景下默认的新生代收集器,常与 CMS 收集器配合使用(因 CMS 无法搭档 Parallel Scavenge
    • Parallel Scavenge 收集器

      • 目标是达到一个可控制的吞吐量,因此它被称为 “吞吐量优先” 收集器
        • 吞吐量: CPU 用于运行用户程序的时间占总时间的比值
        • 其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间
          • 缩短停顿时间是以牺牲吞吐量和新生代空间来换取的
          • 新生代空间变小,垃圾回收变得频繁,导致吞吐量下降
      • 高吞吐量可以高效率地利用 CPU 时间,尽快完成程序的运算任务
        • 适合在后台运算而不需要太多交互的任务
      • 参数 -XX:+UseAdaptiveSizePolicy 决定是否启动 GC 自适应的调节策略(GC Ergonomics)
        • 不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数
        • 虚拟机会根据当前系统的运行情况收集性能监控信息动态调整这些参数,以提供最合适的停顿时间或者最大的吞吐量
    • Serial Old 收集器

    image-20190628160811362

    • Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用
    • 如果用在 Server 场景下,它有两大用途
      • 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用
      • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用
    • Parallel Old 收集器

    image-20190628160911478

    • Parallel Scavenge 收集器的老年代版本

    • 在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器

    • CMS 收集器

      image-20190628160923588

      • CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器
      • 基于 “标记—清除” 算法实现,分为以下四个流程
        • 初始标记:仅标记一下 GC Roots 能直接关联到的对象,速度快,需 STW
        • 并发标记:进行 GC Roots Tracing 的过程,耗时长,不需要 STW
        • 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的记录,需 STW
        • 并发清除:清理未被标记的对象,不需要 STW
      • 主要存在的3个明显缺点
        • CMS 收集器对 CPU 资源非常敏感,在并发阶段占用一部分线程,导致应用程序变慢,总吞吐量降低
        • 无法处理浮动垃圾,同时可能出现Concurrent Mode Failure失败而导致另一次 Full GC 产生
          • 浮动垃圾:并发清除阶段由于用户线程继续运行而产生的垃圾
          • 浮动垃圾只能到下次 GC 时才能回收,因此需要预留出一部分内存
          • 若预留的内存无法满足程序需要,就会出现Concurrent Mode Failure
            • 虚拟机将启动后备预案:临时启用 Serial Old 收集器重进行老年代的垃圾收集
        • 标记 - 清除算法导致的内存碎片,导致无法找到足够大连续空间来分配当前对象,触发再一次 Full GC
          • 可通过设置部分参数启动碎片合并整理
    • G1 收集器

      • 一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能

      • 与其他 GC 收集器相比,G1 具备如下特点

        • 并行与并发
        • 分代收集:G1能够独立管理整个 GC 堆
        • 空间整合:“标记 - 整理” 取代 “标记 - 清除”
        • 可预测的停顿:能够建立可预测的停顿时间模型

        image-20190628161055044

        • G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离
        • G1 跟踪各个 Region 里面的垃圾堆积的价值大小,通过维护一个优先队列来优先回收最大价值的 Region
          • 价值:回收所获得的空间大小以及回收所需时间的经验值
          • 以此保证了 G1 能在有限时间内获取尽可能高的收集效率
        • Region 之间以及其他收集器中的新、老年代之间的对象引用,都是通过 Remembered Set 避免全表扫描
          • 在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏
            image-20190628161456555
        • 初始标记
        • 并发标记
        • 最终标记
          • 修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
          • 虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 中,并与 Remembered Set 合并
          • 需 STW,但可并行执行
        • 筛选回收
          • 根据用户期望的 GC 停顿时间制定回收计划(按优先队列选择高价值 Region 回收
          • 需 STW,但可并行执行
  4. 垃圾收集器参数总结

    • 垃圾收集器相关的常用参数
      image-20190628161542871

内存分配与回收策略

  • Java 技术体系中所提倡的自动内存管理解决了两个问题:给对象分配内存 以及 回收分配给对象的内存
  • Minor GC 和 Full GC 的区别
    • 图xxx截图
  1. 对象优先在 Eden 分配

    • 大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,虚拟机发起一次 Minor GC
    • GC时,若无法将所有存活对象都放入 Survivor 空间,则会通过分配担保机制提前转移到老年代
  2. 大对象直接进入老年代

    • 大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组
      • 更坏的情况是一群“朝生夕灭”的短命大对象,写程序的时候都应当避免
    • 虚拟机提供了 -XX:PretenureSizeThreshold 参数,大于此值的对象直接在老年代分配
      • 目的:避免在 Eden 和 Survivor 之间的大量内存复制
      • 参数只对 Serial 和 ParNew 有两款收集器有效
  3. 长期存活的对象进入老年代

    • 虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在 Eden 出生并熬过一次 Minor gc,则年龄加1
      • 增加到一定年龄则移动到老年代中
      • -XX:MaxTenuringThreshold 用来定义年龄的阈值(默认为 15
  4. 动态对象年龄判定

    • 如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则这些对象都可以直接进入老年代
      • 无需等到 MaxTenuringThreshold 中要求的年龄
  5. 空间分配担保

    • Minor GC 之前,检查老年代最大可用的连续空间是否大于新生代所有对象总空间

      • 若成立,认为 Minor GC 是安全的

      • 查看 HandlePromotionFailure 的值是否允许担保失败

        • 老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小

          • 尝试着进行一次 Minor GC(若担保失败,则重新发起一次 Full GC

          • 执行 Full GC

        • 执行 Full GC

  • Full GC 的触发条件
    • 调用 System.gc () (虚拟机不一定真正地去执行
    • 老年代空间不足(大对象直接进入、长期存活的对象进入老年代等
    • 空间分配担保失败
    • JDK 1.7 及以前的永久代空间不足(当系统中要加载的类、反射的类和调用的方法较多,且未配置 CMS GC 时
    • Concurrent Mode Failure(CMS GC 过程中有对象要放入空间已不足的老年代时,报以上错误,并触发 Full GC

虚拟机类加载机制

  • 虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
  1. 类加载的时机

    image-20190628161608482

    • 加载(Loading)

    • 验证(Verification)

    • 准备(Preparation)

    • 解析(Resolution)

    • 初始化(Initialization)

    • 使用(Using)

    • 卸载(Unloading)

  2. 类与类加载器

    • 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性
    • 比较两个类是否相等,只有在两个类是由同一个类加载器加载的前提下才有意义、
  3. 双亲委派模型

    • 从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器

      • 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分
      • 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader
    • 从 Java 开发人员的角度看,类加载器可以划分得更细致一些:

      • 启动类加载器(Bootstrap ClassLoader)

        • 此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中
      • 扩展类加载器(Extension ClassLoader)

        • 这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的
        • 负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中
      • 应用程序类加载器(Application ClassLoader)

        • 这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的
        • 由于是 ClassLoader 中的 getSystemClassLoader () 方法的返回值,因此一般称为 系统类加载器
        • 负责加载用户类路径(ClassPath)上所指定的类库
      • 类加载器双亲委派模型

        image-20190628161651284

        • 该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器

        • 这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)

        • 工作过程

          • 一个类加载器首先将类加载请求转发到父类加载器,因此请求会传送到顶层的启动类加载器中
          • 只有当父类加载器无法完成时,子类加载器才尝试自己加载
        • 好处

          • Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一
        • 实现

               protected Class<?> loadClass(String name, boolean resolve)
                   throws ClassNotFoundException
               {
                   synchronized (getClassLoadingLock(name)) {
                       // First, check if the class has already been loaded
                       Class<?> c = findLoadedClass(name);
                       if (c == null) {
                           long t0 = System.nanoTime();
                           try {
                               if (parent != null) {
                                   c = parent.loadClass(name, false);
                               } else {
                                   c = findBootstrapClassOrNull(name);
                               }
                           } catch (ClassNotFoundException e) {
                               // ClassNotFoundException thrown if class not found
                               // from the non-null parent class loader
                           }
           
                           if (c == null) {
                               // If still not found, then invoke findClass in order
                               // to find the class.
                               long t1 = System.nanoTime();
                               c = findClass(name);
           
                               // this is the defining class loader; record the stats
                               sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                               sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                               sun.misc.PerfCounter.getFindClasses().increment();
                           }
                       }
                       if (resolve) {
                           resolveClass(c);
                       }
                       return c;
                   }
               }
          
          - 先检查类是否已经加载过,如果没有则让父类加载器去加载
          
          - 当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载
          
  4. 破坏双亲委派模型

    • 定义的类加载器重载 loadClass() 方法即可
    • 通过线程上下文类加载器(Thread Context ClassLoader)解决基础类依赖用户业务类的情况
      • 父类加载器请求子类加载器去完成类加载的动作(破坏了双亲委派模型的层次结构
      • Java中所有涉及SPI的加载动作基本上都采用这种方式
发布了98 篇原创文章 · 获赞 197 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/YangDongChuan1995/article/details/94015762