JVM笔记

java虚拟机(JVM)

首先了解Java文件编译过程:

  1. 程序员编写Java文件
  2. Java文件被编译为字节码.class文件(JVM只认识字节码文件)
  3. 由JVM将其编译成计算机认识的二进制文件

在这里插入图片描述

这篇博客是自己学习过程中的一些总结,其中借鉴了其他博客。

这一编译过程使得Java成为一种跨平台语言,因为JVM隐藏了底层硬件,指令层面的细节,使得java文件直到在具体某一个平台执行时才转化为二进制文件。

①JVM功能

  • 字节码执行:JVM能解释和执行Java程序编译后生成的字节码文件.class文件,字节码是一种中间语言,这一特性使得Java代码的执行不依托于任一具体的平台,因而有跨平台特性
  • 内存管理:JVM能动态分配和管理Java程序的内存,其提供了**垃圾回收机制(GC)**来自动释放不再使用的内存,使得编程人员不必自己手动释放内存。并且JVM还负责对堆、栈等内存区域的的管理。
  • 即时编译:JVM使用即时编译器(JIT)将热点代码(即被频繁执行的代码)编译为本地机器码,从而提高程序执行的效率。
  • 异常处理:JVM提供了异常处理机制,可以捕获并处理Java程序中的异常。它通过异常表和异常处理器来管理异常的传播和处理流程,保证程序的稳定性和可靠性。
  • 类加载和运行时环境:JVM负责加载、验证、链接和初始化Java类。其使用类加载器(Class Loader)将类文件加载到内存中(具体来说是中),并在运行时创建和管理类的实例。

JVM由四部分组成:类加载器(Class Loader)、运行时数据区(Runtime Data Area)、执行引擎(Execution Engine)、本地库接口(Native Interface)。

  • 类加载器:负责将.class文件中的信息加载到运行时数据区中(具体点的话是方法区)。
  • 运行时数据区:存放JVM在执行Java程序时相关数据的一块内存区域
  • 执行引擎:将字节码翻译成底层系统指令再交由CPU去执行
  • 本地库接口:执行过程中可能需要调用其他语言(如C语言)的本地接口

Java类运行流程是:编写好.java文件后,程序在执行前首先会被javac语言编译器编译为.class文件,然后通过类加载器将.class文件中的字节码加载到运行时数据区中,之后由执行引擎将字节码翻译成底层系统的底层系统指令再交由CPU去执行,在这个过程中会调用其他语言的本地库接口来实现整个程序的功能。

在这里插入图片描述

②Java类加载机制

一、类的加载

前面我们已经提到,其实类的加载就是将class文件中的字节码加载到运行时数据区中。具体来说时读入到方法区中,然后再堆区创建一个java.lang.Class对象,用于封装类再方法区内的数据结构。类加载的最终产物时位于堆区中的Class对象,该对象封装了类在方法区内的数据结构,并且向编程人员提供了访问方法区内的数据结构的接口。

类加载器并不需要等到某个类被首次主动使用时再加载它,JVM允许类加载器再预料某个类将要被使用时就预先加载它,如果在预加载时遇到了class文件确实或错误,并不会立即报错,而是在程序首次主动使用该类时才报告错误(LinkageError错误),也就是说,倘若某一个类一直没有被主动使用,那么就不会报错。

二、类的生命周期

从class文件加载到内存中,到类的卸载为止,其整个生命周期有七个阶段:

在这里插入图片描述

1、加载

将Java类的字节码文件加载到内存中,并在内存中构建出一Java类的原型—类模板对象。JVM将常量池、类字段、类方法等信息存储到类模板对象中,这样JVM运行时便能通过类模板获取类中的任何信息,反射机制便是以此为基础。

在加载类时,java虚拟机必须完成三件事:

  1. 通过类的全名,获取类的而精致数据流
  2. 解析类的二进制数据流为方法区内的数据结构(java类模板)
  3. 创建Java.lang.Class类实例,错位方法区中这个类的各种数据的访问入口。(注意:凡是实例,一律存储到堆中)

需要注意的是:

  1. Class类的构造方法是私有的,只有JVM能够创建
2、验证

验证的目的是保证加载的字节码是合法、合理、符合规范的。

大体上JVM要做以下检查:

  1. 格式检查
  2. 语义检查
  3. 字节码验证
  4. 符号引用验证
3、准备

在这个阶段,虚拟机会为这个类分配相应的内存空间,并为类变量设置默认初始值

注意:

  1. JVM不支持Boolean类型,其底层是int类型实现的,默认是0,也就是说,Boolean的默认值是false。
4、解析

这个阶段,将类、接口、字段、方法的符号引用转换为直接引用。也就是得到类、字段、方法在内存中的指针或者偏移量。因此,可以说,如果直接引用存在,那么可以肯定系统中存在该类、方法、字段,但只存在符号引用,不能确定系统中一定存在该结构。

5、初始化

为类的静态字段设置正确的初始值,也就是如果类变量被设置了别的初始值,那么会为其重新赋值。

设置初始值的方法有两种:

  1. 声明类变量为指定初始值。
  2. 使用静态代码块为类变量指定初始值。
  3. 只有当主动使用类是才会导致类的初始化,具体有以下六种:
    • 创建类的示例时
    • 访问某个接口或类的静态变量,或者对静态变量赋值时
    • 条用类的静态方法
    • 反射
    • 初始化某个类的子类时,其父类也会被初始化
    • Java虚拟机启动时被表明启动类的类
6、使用

一旦完成以上的步骤,类就可以正常使用了。

7、卸载

当代表类的Class对象不在被引用,即变成不可达时,就会触发垃圾回收机制,相应的其生命周期也将结束。

类对应的Class对象结束生命周期,这个类本身的生命周期也将结束。

三、类加载器

JVM中有不止一个类加载器,在java中一个类用其全限定名(包名和类名作为其唯一标识),而在JVM中,一个类用其全限定名和其类加载器作为其全限定名,即使时同一个类,用不同的加载器加载,那么生成的Class对象也是不同的。

三个类加载器:

  • 启动类加载器(Bootstrap ClassLoader):是嵌在 Jvm 内核中的加载器,该加载器是用 C++ 语言写的,主要负载加载 JAVA_HOME/lib 下的类库,启动类加载器无法被应用程序直接使用;
  • 扩展类加载器(Extension ClassLoader):该加载器器是用JAVA编写,且它的父类加载器是 Bootstrap,是由 sun.misc.Launcher$ExtClassLoader实现的,主要加载 JAVA_HOME/lib/ext 目录中的类库。开发者可以这几使用扩展类加载器;
  • 系统类加载器(App ClassLoader):也称为应用程序类加载器,负责加载应用程序 classpath 目录下的所有 .jar 和 .class 文件。它的父加载器为 Extension ClassLoader。

③Java运行内存结构

  • JVM运行时数据区

    在这里插入图片描述

以下内容转载自

一、线程共享数据区域

1、Java堆

对于大多数应用来说,Java堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。 Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做 GC堆(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以 Java 堆中还可以细分为:新生代(Eden空间、From Survivor空间 和 To Survivor空间)老年代永久代(Hotspot虚拟机特有的概念,在 Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间 )等,不过这些区域划分仅仅是一部分垃圾回收器的红瞳特性或者说设计风格而已,而非某和 Java 虚拟机具体实现的固有的内存布局。

在Java 8中,永久代被彻底移除,取而代之的是另一块与堆不相连的本地内存——元空间。

从内存分配的角度来看,线程共享的 Java 堆中可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。不过无论从什么角度,无论如何划分,都不会改变 Java 堆中存储内容的共性,无论哪个区域,存储的都只能是对象的实例,进一步细分的目的是为了更好地回收内存,或者更快地分配内存。

参数设置:

根据Java虚拟机规范的规定,Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)

-Xms:设置在JVM启动时的堆内存初始大小

-Xmx:设置堆区允许的最大内存

如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。

2、方法区

方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被 Java 虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 “非堆(Non-Heap)” ,目的应该是与 Java 堆区分开来。

说到方法区,不得不提一下“永久代”这个概念(方法区也称为“永久代”),这是因为垃圾回收器对方法区的垃圾回收比较少,主要是针对常量池的回收以及对类型的卸载,回收条件比较苛刻。经常会导致对此内存未完全回收而导致内存泄露,最后当方法区无法满足内存分配时,将抛出 OutOfMemoryError 异常。

参数设置:

  • -XX:Max-PermSize :设置指定的最大内存进行动态扩展

二、线程独有数据区域

1、程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存;
如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是 Native 方法,这个计数器值则为空(Undefined);
程序计数器 是在 JVM 规范中,唯一一个没有规定任何 OutOfMemoryError 情况的区域(只存下一个字节码指令的地址,消耗内存小且固定,无论方法多深,它只存一条)。

2、Java 虚拟机栈

与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。

  • 虚拟机栈描述的是 Java 方法执行的内存模型:每一个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

  • JVM 对栈的直接操作只有两个,方法执行时,进栈,执行结束后,出栈,所以对于栈不存在垃圾回收问题;

  • 栈中的数据都是以栈帧的格式存在,在这个线程上正在执行的每个方法都各自对应一个栈帧;

  • 不同线程中所有的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另一个线程的栈帧;

  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧

    参数设置:

  • -Xmx-MaxPermSize:设置栈可用的内存;

  • -Xss: 每个线程的堆栈大小,Jdk5.0 以后每个线程堆栈大小为 1M,以前每个线程堆栈大小为 256K,在相同物理内存下,减小这个值能生成更多的线程,但是操作系统对一个进程内的线程数还是有限制的,不能无限生成

在Java虚拟机规范中,对这个区域规定了两种异常状况:

如果采用固定大小的Java虚拟机栈,那每个线程的Java虚拟机栈容量可以在线程创建时独立选定,如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,将抛出 StackOverflowError 异常;
如果虚拟机栈可以动态扩展(当前大部 分的Java虚拟机都可动态扩展,只不过 Java 虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛 OutOfMemoryError 异常。

3、本地方法栈

本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈为虚拟机执行 Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。在虚拟机规范中对本地方法栈中方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如 Sun HotSpo虚拟机)直接就把本地方法栈和虚拟机栈合二为一。

与虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

三、直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规 范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现,所以我们放到这里一起讲解。

在 Jdk1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓 冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著 提高性能,因为避免了在Java堆和Native堆中来回复制数据。

显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是 会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限 制。

服务器管理员在配置虚拟机参数时,会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制), 从而导致动态扩展时出现 OutOfMemoryError 异常。

虚拟机内存与本地内存的区别:

Java 虚拟机在执行时会把内存分成不同的区域,这些区域被称为虚拟机内存。对于虚拟机没有直接管理的物理内存,我们称为本地内存,但这两种内存有一定的区别:

Jvm内存:受限于虚拟机内存大小的参数控制,当大小超过参数的设置时就会报 OOM 异常
本地内存:本地内存不受虚拟机内存参数限制,只受物理机内存容量的限制。当超过物理机内存大小依然会报OOM异常。

④垃圾回收机制

讨论三个问题:

  • 有哪些内存需要回收?What
  • 什么时候回收?When
  • 如何回收?How

一、有哪些内存需要回收

首先要明确,我们回收的的是那些已经“死去”的对象的内存空间(堆中),,因此,我们需要确定哪些对象是“活”的,哪些是“死”的。

1、引用技术算法

为每个对象添加一个引用计数器,每当有对它的引用时,计数器值加1,当引用失效时,计数器值减1,当引用计数器的值为0时,代表当前这个对象没有被任何其他对象引用,那么我们可以说这个对象已死,即判定为垃圾对象。

该算法优点是实现简单,但缺点是无法解决循环引用问题。倘若两个对象互相引用,那么这两个对象的引用计数器将永远无法等于0,从而永远无法被垃圾回收。

2、可达性分析算法

可达性分析算法是通判断对象的引用链是否可达决定其是否可以被回收。程序把所有的引用关系看作一张图,通过一系列的名为GC Roots的对象作为起始点,从这些节点开始搜索,搜索所走过的路径成为引用恋,当一个对象到Root GC之间没有任何引用链相连时,标志着这个对象是不可达的,也就是说这个对象不可用。并将其标记。

被标记的对象不一定会成为可回收对象,要成为可回收对象至少需要被标记两次。

二、什么时候回收?

  • CPU空闲时自动回收
  • 堆内存满
  • 主动调用System.gc()后尝试回收(不保证一定会回收)

三、如何回收?

垃圾回收有四个算法:标记-清除算法、复制算法、标记-整理算法、分代收集算法。目前主要使用的是分代收集算法。

1、标记-清除算法

该算法是最基础的一种算法,首先将可回收对象进行标记,然后统一回收被标记的对象。该算法虽然简单,但是清除之后会产生大量不连续的内存碎片,这些碎片需要链表进行维护,成本该,效率低下。

这里我懒得画图了,借用一下参考博客的图:

在这里插入图片描述

2、复制算法

该算法将可用内存按容量划分为大小相等的两块空间,每次只是用其中的一块。当这一块内存用完了,九江还存会的对象复制到另一块空间上,然后把已使用的空间清理掉,这样就就不用考虑内存碎片的问题,也无需对存活的对象的链表进行维护。代价是内存使用率只有50%,内存使用率低。

该算法有一个非常高效的使用场景:当对象存活时间很短时。这意味着每次回收可以回收大量的内存,需要复制的数据量很小,那么其执行效率就会高很多.

在这里插入图片描述

3、标记-整理算法

标记-整理算法与标记-清除算法的区别是:标记-清除算法会在清理完内存是对存活的对象进行整理,使其不会产生内存碎片。

在这里插入图片描述

4、分代收集算法

分代收集算法会在具体的场景自动选择以上三种算法进行垃圾对象回收。

在jdk1.7之前,JVM将内存划分为三个区域:新生代、老年代、永久代。

新生代会划分为Eden、Surviror0、Survivor1区。

在这里插入图片描述

新生代

新生代的目标就是尽可能快速的处理到哪些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。

新生代内存按照 8:1:1 的比例分为一个Eden区和两个Survivor0、Survivor1区

清理流程如下:

  1. 大部分新生对象在Eden区中生成。

  2. 当Eden区满,触发 Young GC,进行垃圾回收,此时将Eden区存活对象复制到Survivor0区,然后清空Eden区,为后续新的对象分配内存。

  3. 当Survivor0区也满了时,则将Eden区和Survivor0区存活对象复制到Survivor1区,然后清空Eden区和Survivor0区。

  4. 此时Survivor0区是空的,然后交换survivor0区和survior1区的角色(即下次垃圾回收时会扫描Eden区和Survivor1区 ),即保持Survivor0区为空,如此往复。

  5. 当Survivor01区也不足以存放Eden区和Survivor0区的存活对象时,就将存活对象直接存放到老年代

  6. 注意新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高,不一定等 Eden区满了才触发。而新生代触发Young GC是当Eden区满了才触发。

老年代

老年代存放的都是一些生命周期较长的对象。

老年代的内存比新生代大很多(2:1)。如果老年代也满了,就会触发一次FullGC(MajorGC),即新生代、老年代都进行回收。

永久代

永生代只要用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能会动态生成或调用一些class,例如使用反射,动态代理,CGLib等。在这种情况下需要设置一个比较大的永生代来存放这些运行过程中新增的类。

在Jdk1.8以后废弃了永生代,取而代之的是元空间,元空间与永生代的区别是:元空间不在虚拟机中,而是使用本地内存,也因此不局限于JVM可以使用的内存空间。(因为永生代总是内存不够用或内存泄漏)。

⑤垃圾回收器

垃圾回收器是内存回收的具体实现。可以分为新生代回收期和老年代回收期。

  • 新生代回收器:Serial(单线程)、ParNew(多线程)、Parallel Scavenge(多线程)
  • 老年代回收器:Serial Old(单线程)、Parallel Old(多线程)、CMS(多线程)
  • G1收集器

注意:上述的”单线程“并不是真正的单线程,而是说在进行垃圾回收时会暂停其他所有的工作进程。

一、新生代回收器

1、Serial

采用标记-复制算法,垃圾清理是,Serial回收期不存在线程间的切换,因此,在danCPU环境下,垃圾清理效率比较高。

优点:

  • 简单高效
  • 所有收集器中额外内存消耗最少
  • 针对内存及时找或一两百兆的新生代,停顿时间能控制在100ms内。
2、ParNew

Serial的多线程版。充分利用多核心的物理优势,有更高的吞吐量,更加高效。

3、Parallel Scavenge

Parallel Scavenge 收集器同 ParNew 收集器一样,也是采用 “标记-复制” 算法,且为能够并行收集的多线程收集器。

Parallel Scavenge 的特点是其关注重点为吞吐量,高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算,但也就是说它的线程单次停止时间可能更长,因此适用于后台计算型任务程序

二、老年代回收器

1、Serial Old

同Serial

2、Parallel Old

Parallel Old 回收器和 Parallel Scavenge 回收器同样考虑了吞吐量优先这一指标,非常适合那些注重吞吐量和 CPU 资源敏感的场合。

3、CMS

CMS 收集器,是一种以获取最短回收停顿时间为目标的收集器,其缩写含义为 Concurrent Mark Sweep,Mark Sweep 指的是“标记-清除”算法,在互联网网站、B/S 架构的中常用的收集器就是 CMS,因为系统停顿的时间最短,给用户带来较好的体验。

CMS收集器的运作过程分为4个步骤,包括:

  • 初始标记(短暂),仅仅只是标记一下 GCRoots 能直接关联到的对象,速度很快;

  • 并发标记(和用户的应用程序同时进行),进行 GCRoots 追踪的过程,标记从 GCRoots 开始关联的所有对象开始遍历整个可达分析路径的对象。这个时间比较长,所以采用并发处理(垃圾回收器线程和用户线程同时工作);

  • 重新标记(短暂),为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短;

  • 并发清除(和用户的应用程序同时进行):垃圾回收

    整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

三、G1回收器

G1(Garbage-First)收集器是最前沿的成果之一,在Java7 update 4之后引入(Jdk7 的第 4 个版本),是一款面向服务端应用的垃圾收集器。在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。

G1 是一个分代的,增量的,并行与并发的“标记-复制”垃圾回收器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(pause time),同时兼顾良好的吞吐量。

G1收集器的优势:

  • 独特的分代垃圾回收器,分代GC: 分代收集器,同时兼顾年轻代和老年代;

  • 使用分区算法,不要求 eden,年轻代或老年代的空间都连续;

  • 并行性: 回收期间,可由多个线程同时工作,有效利用多核cpu资源;

  • 空间整理: 回收过程中,会进行适当对象移动,减少空间碎片;

  • 可预见性: G1 可选取部分区域进行回收,可以缩小回收范围,减少全局停顿。

G1收集器的阶段分以下几个步骤:

  • 初始标记(它标记了从GC Root开始直接可达的对象);

  • 并发标记(从GC Roots开始对堆中对象进行可达性分析,找出存活对象);

  • 最终标记(标记那些在并发标记阶段发生变化的对象,将被回收);

  • 筛选回收(首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间指定回收计划,回收一部分Region)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值