镰刀

镰锤帮左护法

欢迎来到我的个人博客!


Java JVM相关知识点

基本概念

JVM是Java Virtual Machine Java虚拟机的缩写,JVM是一种计算机设备的规范,是一个虚构出来的计算机,通过实际计算机上仿真模拟各种计算机功能来实现。

引入Java语言虚拟机后,Java语言在不同平台上运行不需要重新编译。Java语言使用Java虚拟机屏蔽与具体平台相关信息,使Java语言编译只需要生成在Java虚拟机上运行的目标代码(字节码),就可在多种平台上不加修改的运行。

JVM概述图

运行过程

Java源文件,通过编译器产生对应的.class文件,也就是字节码文件,字节码文件通过Java虚拟机解释器,编译成特定机器上的机器码。

  1. Java源文件->编译器->字节码文件
  2. 字节码文件->JVM->机器码

每个平台的解释器不同,但是实现的虚拟机是相同的,这是Java跨平台的原因。

但一个程序开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。

程序退出或者关闭,虚拟机实例消亡,多个虚拟机实例之间数据不共享。

JVM图1

线程

线程是指程序执行过程中的操作系统能够运行调度的最小单元。包含在进程之中,是进程的实际运作单位。

JVM 允许一个应用并发执行多个线程。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好后,

就会创建一个操作系统原生线程。Java线程结束,原生线程回收,操作系统负责调度所有线程,把他们分配到任何可用的CPU上。

当原生线程初始化完毕,就会调用Java的run()方法,当线程结束时,会释放原生线程和Java线程的所有资源。

Hotspot Jvm 后台运行的系统线程主要有以下几种:

名称 介绍
虚拟机线程(VM thread) 这个线程等待JVM到达安全点操作出现。这些操作必须再独立线程内执行,因为当堆修改无法进行时,线程都需要JVM位于安全点。操作有stop-the-world垃圾回收、线程栈dump、线程暂停、线程偏向锁解除。
周期性任务线程 负责定时器事件(中断),用来调度周期性操作的执行。
GC线程 支持JVM中不同的垃圾回收活动。
编译器线程 这些线程在运行时将字节码动态编译成本地平台相关机器码。
信号分发线程 这个线程接受发送到JVM的信号并调用适当的JVM方法处理。

JVM内存区域

JVM图2

JVM 内存区域主要分为:

线程私有区域:
    程序计数器、虚拟机栈、本地方法区。
线程共享区域:
    Java堆、方法区。
直接内存

线程私有数据区域内生命周期与线程相同,依赖用户线程的启动/结束而创建/销毁。 每个线程都与操作系统本地线程直接映射,因此这部分内存区域与本地线程的启动/结束相对应。

线程共享数据区域内生命周期与JVM的启动/关闭对应创建/销毁。

直接内存不是JVM运行时的内存也不是JVM定义的内存区域,就是堆外单独的一块内存区域。 例如:NIO,基于通道缓冲区结合的I/O方式,可以使用native函数库直接堆外分配内存,通过一个存储在堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能在一些场景中提高性能。

JVM图3

程序计数器(线程私有)

一块较小的内存空间,是当前线程所执行的字节码行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为线程私有内存。

正在执行Java方法,计数器记录是虚拟机字节码指令的地址。如果还是Native方法为空。

这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。

虚拟机栈(线程私有)

虚拟机栈遵循LIFO思想(后进先出),虚拟机栈是描述Java方法执行的内存模型,每个方法从执行都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。 每个方法从执行到结束,都对应一个栈帧在虚拟机栈中的入出栈。

栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)方法返回值和异常分派(Dispatch Exception)。栈帧随着方法调用而创建, 方法结束而销毁,不论方法是正常完成还是异常(抛出异常未被捕获)完成都结束。

JVM图4

本地方法区(线程私有)

本地方法区和Java Stack作用类似,区别是虚拟机栈为执行Java方法服务,而本地方法栈则为Native方法服务。

在Java代码中被 native 修饰的方法,具体的方法是由Java底层的c/c++实现的。 本地方法栈也是由线程独享的,没有线程安全问题。 例如:

public class Object {

    private static native void registerNatives();
    static {
        registerNatives();
    }

    /**
     * Returns the runtime class of this {@code Object}. The returned
     * {@code Class} object is the object that is locked by {@code
     * static synchronized} methods of the represented class.
     *
     * <p><b>The actual result type is {@code Class<? extends |X|>}
     * where {@code |X|} is the erasure of the static type of the
     * expression on which {@code getClass} is called.</b> For
     * example, no cast is required in this code fragment:</p>
     *
     * <p>
     * {@code Number n = 0;                             }<br>
     * {@code Class<? extends Number> c = n.getClass(); }
     * </p>
     *
     * @return The {@code Class} object that represents the runtime
     *         class of this object.
     * @jls 15.8.2 Class Literals
     */
    public final native Class<?> getClass();

    ......
}

堆(线程共享)

堆是被线程共享的一块内存区域,创建的对象和数组都保存在Java堆内存中。 堆也是垃圾收集器进行垃圾收集重要的内存区域。由于现代VM采用分代收集算法, 因此Java堆从GC的角度还可以分为,新生代和老年代。

方法区/永久代(线程共享)

方法区,永久代,存储JVM加载的类信息,常量,静态变量,即时编译器编译后的代码等。

方法区存储的类信息:对每个加载的类型(类class、接口interface、枚举enum、注解annotation),JVM必须在方法区中存储以下类型信息:

  • 类型的完整有效名称,全名 包名.类名
  • 类型的直接弗雷的完整有效名称(java.lang.object除外,没有声明父类,默认是父类的object)
  • 类型的修饰符
  • 类型直接接口的有序列表
  • 类型的常量池
  • 域(Field)信息
  • 方法(Method)信息
  • 除了常量外的所有静态(static)变量

方法区:static final 修饰的成员变量都存储在方法区中。

方法区存储的静态变量

  • 静态变量又称类变量,类中被static修饰的成员变量都是静态变量。静态变量和类关联在一起,随着类的加载存在方法区中而不是在堆中。
  • 八种基本数据类型(byte,short,int,long,float,double,char,boolean)静态变量会在方法区开辟空间,并将对应的值存储在方法区。 引用类型的静态变量如果未使用new关键字为引用类型的静态变量分配对象(如:static Object obj;)那么对象的引用obj会存储在方法区中, 并为其指定默认值null;若对于引用类型的静态变量如果用new关键字为引用类型的静态变量分配对象(如:static Person person = new Person()) 那么对象的引用person 会存储在方法区中,并且该对象在堆中的地址也会存储在方法区中(注意此时静态变量只存储了对象的堆地址,而对象本身仍在堆内存中)

方法区存储的方法

  • 程序运行时会加载类编译生成的字节码,在这个过程中静态变量和静态方法及普通方法对应的字节码加载到方法区。
  • 方法区中没有实例变量,这是因为类加载先与对应类对象的产生,实例变量和对象关联在一起的,没有对象就不存在实例变量, 类加载时没有对象,所以方法区中没有实例变量。
  • 静态变量和静态方法及普通方法在方法区存储方式是有区别的。

JVM运行时内存

Java堆从GC的角度可以分为:新生代、老年代。

新生代

新生代用来存放新生的对象,一般占据堆的1/3空间,由于频繁创建对象,新生代会频繁触发MinorGC进行垃圾回收。 新生代又分为Eden、SurvivorFrom、SurvivorTo三个区。

Eden区

Java新对象出生地(如果新创建对象占用内存很大,则直接分配到老年代)。当Eden区内存不够就会触发MinorGc,对新生代垃圾进行回收。

SurvivorFrom

上一次GC的幸存者,作为这一次GC的被扫描者。

SurvivorTo

保留了一次MinorGC过程的幸存者。

MinorGC 过程 复制->清空->互换

1. Eden、SurvivorFrom复制到SurvivorTo,年龄+1
首先把Eden和SurvivorFrom区域存活的对象复制到SurvivorTo区域(如果有对象的年龄达到老年标准,则直接赋值到老年代),同时把对象的年龄+1
如果SurvivorTo位置不够,则直接放到老年代

2. 清空Eden、SurvivorFrom

3. SurvivorTo 和 SurvivorFrom 互换
原 SurvivorTo 成为下一代GC时的 SurvivorFrom。

老年代

主要存放生命周期长的内存对象。老年代对象比较稳定,所以MajorGC并不会频繁执行。在MajorGC前一般都会进行一次MinorGC,使得新生代的对象晋升入老年代, 导致空间不够用时才触发。当无法找到猪狗大的连续空间分配给新创建的较大对象时,也会提前触发一次MajorGC进行垃圾回收,腾出空间。

MajorGC采用编辑清除算法,首先扫描一次所有的老年代,标记出存活对象,然后回收没有标记的对象。MajorGC的耗时较长,因为要扫描再回收。 MajorGC会产生内存碎片,为减少内存损耗,一般需要合并或者标记方便下次直接分配,当老年代也满了装不下的时候,会抛出OOM(Out of Memory)异常。

永久代

内存永久保存区域,主要存放Class和Meta(元数据),Class在被加载的时候放入永久区,和存放实例的区域不同,GC不会再主程序运行期对永久区清理。 所以这导致永久代的区域会随着Class的增多而增大,最终抛出OOM异常。

  • Java8中永久代已被移除,被元数据区(元空间)的区域所替代。元空间本质和永久代类似,元空间与永久代的区别在于,元空间并不在虚拟机中,而是使用本地内存。 因此默认情况下,元空间的大小仅受本地内存限制。类的元数据放入 native memory,字符串池和类的静态变量放入Java堆中,这样加载类的元数据不再由MaxPermSize控制, 由系统实际可用空间来控制。

垃圾回收与算法

  • 如何确定垃圾

    引用计数法: 在Java中引用和对象是有关联的,如果操作对象必须引用。通过引用计数器可以判断对象是否可回收。 一个没有任何关联的引用,这个对象就是可回收对象。

    可达性分析: 为解决引用计数法循环引用问题,Java使用可达性分析。通过GC roots对象作为起点搜索。 如果在GC roots和一个对象之间没有可达路径,则该对象是不可达的,但是不可达对象并不等价于可回收对象, 不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

  • 标记清除算法

    JVM图4

    最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。 图上可见,最大问题是内存碎片化严重,大对象进来不能找到可用空间。

  • 复制算法

    JVM图5

    为了解决标记清除算法内存碎片化缺陷提出的算法。按内存容量将内存划分为等大小的两块。每次只是用其中一块,当这一块内存存满后将存活对象复制到另外一块上,把已使用的内存清清除掉。 内存效率提高了,不易产生碎片,但是可用内存被压缩到了原本的一半,存活对象增多,Copying算法效率会降低。

  • 标记整理算法

    JVM图6

    综合标记清除算法和复制算法,避免缺陷提出,标记阶段和标记清除算法相同,标记后不清理对象,将存活对象移向内存一端,然后清除端边界外的对象。

  • 分代收集算法

目前大部分JVM采用方法,核心思想是根据对象存活生命周期将内存划分不同的域,一般情况下GC堆划分为老年代新生代。老年代特点是每次垃圾回收只有少量对象需要被回收。 新生代特点是每次会有大量垃圾需要被回收,因此根据不同区域选择不同算法。

新生代与复制算法
大部分JVM GC对新生代都采取Copying算法,因为新生代每次毒药回收大部分对象,复制操作较少。
通常不按照1:1划分新生代,一般划分为Eden空间,较小的Survivor空间(from space,to space),
每次使用Eden空间和一块Survivor空间,当进行回收时,将存活的对象复制到另外一块Survivor空间中。

老年代标记复制算法
老年代每次回收少量对象,采用标记复制算法。
- Java虚拟机中提到过处于方法区的永生代,是存储class类、常量、方法描述等。对永生代的回收主要包括废弃常量和无用的类。
- 对象的内存分配主要在新生代的Eden和Survivor的From,少数情况会直接分配到老年代。
- 当新生代的Eden和From空间不足就会发生GC,GC后,Eden和From区存活对象会被挪到To,然后清理Eden和From。
- 如果To无法足够存储对象,直接存储到老年代。
- 在GC后使用的是Eden和To反复循环。
- 当对象在Survivor区未被GC,其年龄+1,默认情况下年龄15的会被移动至老年代中。
  • Java四种引用类型

    强引用 Java中常见就是强引用,把一个对象赋值给一个引用变量,这个引用变量就是强引用。当一个对象被强引用变量引用,便一直处于可达状态, 是不可能被垃圾回收机制回收的,即使该对象以后永远不会被用到JVM也不会回收,强引用时内存泄漏主要原因之一。 例:

    String str = "abc";
    List<String> list = new ArrayList<>();
    list.add(str);
    list集合内数据不会被释放,即使内存不足。
    

    软引用 软引用需要用SoftReference类来实现,对于只有软引用的对象来说,当系统内存足够时不会被回收,内存空间不足时会被回收,通常出现在对内存敏感的程序中。 例:

      public class UseBasicType {
          public static void main(String[] args) {
              // 软引用
              System.out.println("start");
              A a = new A();
              SoftReference<A> sr = new SoftReference<A>(a);
              a = null;
              // 类似缓存用法
              if(sr!=null){
                  a = sr.get();
              } else{
                  a = new A();
                  sr = new SoftReference<A>(a);
              }
              System.out.println("end");
          }
      }
      class A {
          int[] x;
          public A () {
              x = new int[80000000];
          }
      }
    

    弱引用 需要用WeakReference类实现,比软引用生命周期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM内存是否够用,总会回收该对象占用内存。 例: Object c = new Car(); //只要c还指向car object, car object就不会被回收 WeakReference weakCar = new WeakReference(Car)(car); 当要获得weak reference引用的object时, 首先需要判断它是否已经被回收: weakCar.get(); 如果此方法为空, 那么说明weakCar指向的对象已经被回收了。

    虚引用 需要用PhantomReference类实现,不能单独使用,必须和引用队列联合使用,虚引用的主要作用时跟踪对象被垃圾回收的状态。 虚引用在于跟踪垃圾回收过程,在对象被收集器回收时收到一个系统通知。 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,将这个虚引用加入引用队列,在其关联的虚引用出队前,不会彻底销毁该对象。 所以可以通过检查引用队列中是否有相应的虚引用来判断对象是否已经被回收了。 如果一个对象没有强引用和软引用,对于垃圾回收器而言便是可以被清除的,在清除之前,会调用其finalize方法,如果一个对象已经被调用过finalize方法但是还没有被释放,它就变成了一个虚可达对象。 与软引用和弱引用不同,显式使用虚引用可以阻止对象被清除,只有在程序中显式或者隐式移除这个虚引用时,这个已经执行过finalize方法的对象才会被清除。想要显式的移除虚引用的话,只需要将其从引用队列中取出然后扔掉(置为null)即可。 使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。这个虚引用对于对象而言完全是无感知的,有没有完全一样,但是对于虚引用的使用者而言,就像是待观察的对象的把脉线,可以通过它来观察对象是否已经被回收,从而进行相应的处理。 事实上,虚引用有一个很重要的用途就是用来做堆外内存的释放,DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。

引用类型 被垃圾回收时间 用途 生存时间
强引用 从来不会 对象的一般状态 JVM停止运行时终止
软引用 在内存不足时 对象缓存 内存不足时终止
弱引用 在垃圾回收时 对象缓存 GC运行后终止
虚引用 在内存不足时 跟踪对象被垃圾回收的状态,进行相应的处理 JVM停止运行时终止

分区收集算法

分区收集算法将整个堆空间划分为连续的不同的小区间,每个小区间独立相对,独立回收,这样好处可用控制一次回收多少个小区间,根据目标停顿时间,每次合理回收若干小区间,而不是整个堆,减少一次GC产生的停顿。

GC垃圾收集器

Java堆内存被划分为新生代老年代两个部分,新生代主要使用复制和标记-清除垃圾回收算法,老年代主要使用标记-整理垃圾回收算法。 Java虚拟机针对新生代和老年代分别提供多种不同的垃圾收集器,JDK中垃圾收集器如下:

JVM图7

  • Serial 垃圾收集器 (单线程、复制算法)

Serial最基本的垃圾收集器,使用复制算法,JDK1.3.1之前新生代唯一的垃圾收集器。是单线程的收集器,只会使用一个CPU或者一条线程去完成垃圾收集工作, 并且在收集垃圾的同时,暂停其他工作线程,直到垃圾收集结束。 Serial垃圾收集器虽然在执行过程中需要暂停其他工作线程,但简单高效,对于单CPU来说,没有线程交互的开销,可用获得最高的单线程垃圾收集效率, 因此Serial垃圾收集器依然是Java虚拟机运行在Client模式下默认的新生代垃圾收集器。

  • ParNew 垃圾收集器 (Serial 多线程、复制算法)

ParNew垃圾收集器是Serial收集器的多线程版,也是使用复制算法,除了使用多线程进行垃圾收集之外,其余行为和Serial收集器完全一样,ParNew垃圾收集器在垃圾收集过程中同样也暂停其他线程工作。 ParNew收集器默认开启和CPU数量相同的线程数,可用通过-XX:ParallelGCThreads来限定垃圾收集器的线程数量。 ParNew除了多线程外其他几乎与Serial垃圾收集器一致,是很多Java虚拟机运行在Server模式下新生代的默认垃圾收集器。

  • Parallel Scavenge 收集器 (多线程复制算法、高效)

Parallel Scavenge 是一个新生代垃圾收集器,同样是复制算法,也是一个多线程垃圾收集器,重点关注程序可达的可控制吞吐量,高吞吐量可用最高效率利用CPU时间,尽快完成运算任务,适用于后台运算不需要太多交互的任务。 自适应调节策略是ParallelScavenge收集器与ParNew收集器的重要区别。 吞吐量,CPU用于运行用户代码的时间/CPU总消耗时间,吞吐量=代码运行时间/(运行用户代码时间+垃圾收集时间)

  • Serial Old 收集器(单线程标记整理算法)

Serial Old是Serial垃圾收集器的老年代版本,是单线程收集器,使用标记整理算法,也是主要运行在Client默认的Java询价默认的老年代垃圾收集器。 在Server模式下主要有两个用途:

  1. 在JDK1.5之前版本中与新生代的Parallel Scavenge收集器搭配使用。
  2. 作为老年代中使用CMS收集器的搭配垃圾收集过程。
  • Parallel Old收集器(多线程标记整理算法)

Parallel Old收集器是Parallel Scavenge的老年代版本,使用多线程标记整理算法,JDK1.6开始提供。 在JDK1.6之前,新生代使用ParallelScavenge收集器只能搭配老年代的Serial old收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量。 Parallel Old是为了在老年代中同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求较高,可以优先考虑新生代Parallel Scavenge和老年代Parallel Old收集器的搭配策略。

  • CMS收集器 (多线程标记清除算法)

CMS收集器是老年代垃圾收集器,主要目的获取最短垃圾回收停顿时间,和其他老年代使用标记整理算法不同,它使用多线程标记清除算法。最短垃圾收集停顿时间可以为交互比较高的程序提高用户体验。 CMS整个过程分为四个阶段:

- 初始标记
标记GC Roots能直接关联的对象,速度很快,但是仍需要暂停所有的工作线程。

- 并发标记
进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程。

- 重新标记
为修正在并发标记期间,用户程序继续运行而导致标记产生变动的一部分对象标记记录,仍需要暂停所有的工作线程。

- 并发清除
清除GC Roots不可达对象和用户线程一起工作,不需要暂停工作线程,由于耗时最长的并发标记和并发清除过程中,垃圾收集线程和用户限制一起并发工作,
所以总体上看CMS收集器的内存回收和用户线程一起并发执行。
  • G1收集器

Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比CMS收集器,G1收集器最突出的改进是:

  1. 基于标记整理算法,不产生内存碎片。
  2. 可以非常精确控制时间停顿,在不牺牲吞吐量的前提下,实现低停顿垃圾回收。

G1收集器避免全区域的垃圾收集,他把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。 区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获得最高的垃圾收集效率。