JVM

基本概念

      JVM运行在操作系统上与硬件没有直接交互的假象计算机,包含一套字节码指令集、一组寄存器、堆、栈、垃圾回收机制、方法区。
在这里插入图片描述

运行过程

  1. Java编程的原文件都是java结尾的,当我们要运行一个java文件时会通过编译器产生对应的class文件,也就是字节码文件,然后通过虚拟机中的解释器编译成特定的机器码。

    如: java源文件 ——>编译器——>字节码文件(class)——>JVM——>机器码
    如上一系列流程。
          每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是java跨平台的原因了。当一个程序运行开始,虚拟机就已经开始实例化了,多个程序同时启动就存在多个虚拟机,多个程序之间数据不共享,当程序关闭或者退出,虚拟机实例消亡。
    在这里插入图片描述
  2. 线程
    这里说的线程是指一个程序执行的最小单位,JVM允许一个应用并发多个线程。
    Hotspot JVM 中的java线程与原生操作系统线程有直接的映射关系。
    当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好之后,就创建一个操作系统原生线程。java线程结束,原生线程随之被收回。操作系统负责调度所有的线程,并分配到任何可用的CPU上,当原生线程初始化完毕,就会调用java线程run()方法。当线程结束会释放原生线程和java线程的所有资源。

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

虚拟机线程(VM thread)这个线程等待JVM到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要JVM位安全点。这些操作的类型有:stop-the-world垃圾回收、线程栈dump、线程暂停、线程偏向锁(biased locking)解除
周期性任务线程这线程负责定事件(也就是中断),用来调度周期性操作的执行
GC线程这些线程支持JVM中不同的垃圾回收机制
编译器线程这些线程在运行时将字节码动态编译成本地平台相关的机器码
信号分发线程这个线程接收发送到JVM的信号并调度是到的方法处理
  1. JVM内存区域
    在这里插入图片描述
    JVM内存区域主要分为线程私有区域(程序计数器、虚拟机栈、本地方法区)、线程共享区(java堆、方法区)、直接内存。
           线程私有数据区域生命周期与线程相同,依赖用户线程的启动和结束,而创建和销毁(在HotspotJVM内,每个线程都与操作系统的本地线程直接映射,因此这部分内存区域的存在与否跟随本地线程的生命周期)。
           线程共享区域随虚拟机的启动和关闭而创建销毁。
           直接内存并不是JVM运行时数据区的一部分,当也会被频繁的使用:在1.4引入的NIO提供了基于Channel与Buffer的IO方式。它可以使用Native函数库直接分配堆外内存,然后DirectByteBuffer对象作为这块内存的引用进行操作,这样就避免了在Java堆和Native堆中来回复制数据,因此在一些场景中可以显著提高性能。
    在这里插入图片描述
3.1.1 程序计数器
  1. 一块较小的内存空间,是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
  2. 正在执行的java方法,计数器记录的是虚拟机字节码指令的地址(当前指令的地址),如果还是Native方法,则为null。
  3. 这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。
3.1.2 虚拟机栈(线程私有)
  1. 是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
  2. 栈帧(Frame)是用来存储数据和部分过程结果的数据结构,同时也被处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕捉的异常)都算作方法结束。
    在这里插入图片描述
3.1.3 本地方法区(线程私有)

本地方法去和java stack作用类似,区别是虚拟机栈为执行java方法服务,而本地方法栈则为Native方法服务,如果一个VM实现使用C-linkage模型来支持Native调用,那么该栈将会是一个C栈,但HstSpotVM直接就把本地方法栈和虚拟机栈合二为一。

3.1.4 堆(Heap-线程共享)-运行时数据去

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

3.1.5 方法去/永久代

永久带(Permanet Generation),用于存储被JVM加载的类信息、量、静态变量、即时编译器编译后的代码等数据。HotSpotVM把GC分代收集扩展至方法区,即使用java堆的永久带来实现方法区,这样HotSpot的垃圾收集器就可以想管理java堆一样管理这部分内存,而不必为方法去开发专门的内存管理机制(永久代的内存回收主要是常量是的回收和类型的写在,所以收益较小)。
运行时常量是(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法去的运行时常量池中。java虚拟机堆Class文件的每一部分(自然也包括常量池)的格式都是有严格规定的,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。

3.2 JVM运行时内存

Java堆从GC的角度可以分为:新生代(Eden区,From Servivor区,TO Servivor区)老年代
在这里插入图片描述

3.2.1 新生代

是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁的出发MinorGC进行垃圾回收。新生代又分为Eden区,ServivorFrom区、ServivorTo区。

3.2.1.1 Eden区

java新生对象的出生地(如果创建的对象占用内存很大,内直接分配到老年代)。Eden区域内存不够的时候会出发MinorGC,对新生代进行一次垃圾回收。

3.2.1.2 Servivor From区

上一次GC回收的幸存者,这一次回收的被扫描者。

3.2.1.3 Servivor TO区

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

3.2.1.4 MinorGC的过程(复制->清空->替换)

MinorGC采用的是复制算法。

  1. 先把Eden区和From区的数据复制到TO区
    首先,把 Eden 和ServivorFrom 区域中存活的对象复制到 ServivorrTo 区域(如果有对象的年 龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServivorTo 不 够位置了就放到老年区);
  2. 清空Eden区和From区
    然后,清空 Eden 和 ServivorFrom 中的对象;
  3. 在把TO区的内容替换到From区。
    最后,ServivorTo 和 ServivorFrom 互换,原 ServivorTo 成为下一次 GC 时的 ServivorFrom
    区。
3.2.2 老年代

主要存放应用程序中生命周期长的内存对象。

  1. 老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行 了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足 够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
  2. MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没 有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减 少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的 时候,就会抛出 OOM(Out of Memory)异常。
3.2.3 永久带

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

3.2.3.1 JAVA8 与元数据

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

3.3 垃圾回收与算法

在这里插入图片描述

3.3.1 如何确定垃圾
3.3.1.1 引用计数法

在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单 的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关 联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收 对象。

3.3.1.2 可达性分析法

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

3.3.1.3 标记清除算法(Mark-Sweep)

最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清 除阶段回收被标记的对象所占用的空间。
在这里插入图片描述
从图中我们就可以发现,该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可 利用空间的问题。

3.3.1.3 复制算法(copying)

为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小 的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用 的内存清掉在这里插入图片描述这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原 本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。

3.3.1.4 标记整理算法(Mark-Compact)

结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清 理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
在这里插入图片描述

3.3.1.5 分代收集算法

分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存 划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃 圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法

3.3.1.5.1 新生代与复制算法

目前大部分 JVM 的 GC 对于新生代都采取 Copying 算法,因为新生代中每次垃圾回收都要 回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代 划分为一块较大的 Eden 空间和两个较小的 Servivor 空间(From Space, To Space),每次使用
Eden 空间和其中的一块 Servivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另 一块 Servivor 空间中。
在这里插入图片描述

3.3.1.5.2 老年代与标记复制算法

而老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。

  1. JAVA 虚拟机提到过的处于方法区的永生代(Permanet Generation),它用来存储 class 类,
    常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
  2. 对象的内存分配主要在新生代的 Eden Space 和 Servivor Space 的 From Space(Servivor 目
    前存放对象的那一块),少数情况会直接分配到老生代。
  3. 当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 From Space 进行清理。
  4. 如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。
  5. 在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。
  6. 当对象在 Servivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被 移到老生代中。
3.4 JAVA 四中引用类型
3.4.1 强引用

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

3.4.2 软引用

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

3.4.3 弱引用

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

3.4.4 虚引用

虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚 引用的主要作用是跟踪对象被垃圾回收的状态。

3.5 GC分代收集算法 VS分区收集算法
3.5.1 分代收集算法

当前主流 VM 垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据 对象存活周期的不同将内存划分为几块, 如 JVM 中的 新生代、老年代、永久代,这样就可以根据 各年代特点分别采用最适当的 GC 算法

3.5.1.1 在新生代-复制算法

每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量 存活对象的复制成本就可以完成收集.

3.5.1.2 在老年代-标记整理算法

因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标 记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存.

3.5.2 分区收集算法

分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间,根据目前停顿时间,每次合理地回收若干个小区间(而不是整个堆),从而减少一次GC所产生的停顿。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值