JVM快速入门

发布于:2025-08-05 ⋅ 阅读:(15) ⋅ 点赞:(0)

1.什么是JVM

Java虚拟机,

JRE(Java Runtime Environment)的一部分,安装了JRE其实也就是相当于安装了JVM

JVM是运行在操作系统之上的,它与硬件没有直接的交互

JVM的作用加载并执行Java字节码文件(.class) - 加载字节码文件、分配内存(运行时数据区)运行程序

JVM的特点:一次编译到处运行、自动内存管理、自动垃圾回收

  • 类加载器子系统:将字节码文件(.class)加载到内存中的方法区

  • 运行时数据区:

    • 方法区:存储已被虚拟机加载的类的元数据信息(元空间)。也就是存储字节码信息。

    • 堆:存放对象实例,几乎所有的对象实例都在这里分配内存。

    • 虚拟机栈(java栈):虚拟机栈描述的是Java方法执行的内存模型每个方法被执行的时候都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息

    • 程序计数器:当前线程所执行的字节码的行号指示器

    • 本地方法栈:本地方法栈则是记录虚拟机当前使用到的native方法

  • 本地方法接口:虚拟机使用到的native类型的方法,负责调用操作系统类库。(例如Thread类中有很多Native方法的调用)

  • 执行引擎:包含解释器、即时编译器和垃圾收集器 ,负责执行加载到JVM中的字节码指令。

2.执行引擎Execution Engine

JVM执行引擎通常由两个主要组成部分构成:解释器即时编译器(Just-In-Time Compiler,JIT Compiler)。

  1. 解释器:当Java字节码被加载到内存中时,解释器逐条解析和执行字节码指令。解释器逐条执行字节码,将每条指令转换为对应平台上的本地机器指令。由于解释器逐条解析执行,因此执行速度相对较慢。但解释器具有优点,即可立即执行字节码,无需等待编译过程。

  2. 即时编译器(JIT Compiler):为了提高执行速度,JVM还使用即时编译器。即时编译器将字节码动态地编译为本地机器码,以便直接在底层硬件上执行。即时编译器根据运行时的性能数据和优化技术,对经常执行的热点代码进行优化,从而提高程序的性能。即时编译器可以将经过优化的代码缓存起来,以便下次再次执行时直接使用

3.本地方法接口Native Interface

内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies

本地方法栈存储了从Java代码中调用本地方法时所需的信息。是线程私有

4.PC寄存器(程序计数器)

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码用来存储指向下一条指令的地址,即 将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。

5.类加载器ClassLoader

加载 Java 类的字节码( .class 文件)到 JVM 中。

类加载的过程

  • 加载过程:加载->链接->初始化

  • 链接过程又可分为三步:验证->准备->解析

加载

主要完成下面 3 件事情:

  1. 通过全类名获取定义此类的二进制字节流

  2. 字节流所代表的静态存储结构转换方法区的运行时数据结构

  3. 内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

  • 每个 Java 类都有一个引用指向加载它的 ClassLoader

  • 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。

类加载器分类

  • 启动类加载器(BootstrapClassLoader):由C++实现。主要用来加载 JDK 内部的核心类库。

  • 扩展类加载器(ExtClassLoader/PlatformClassLoader):由Java实现,派生自ClassLoader类。主要用来加载 JDK 内部的扩展类库。

  • 应用程序类加载器(AppClassLoader):也叫系统类加载器。由Java实现,派生自ClassLoader类。面向我们用户的加载器,负责加载当前应用 classpath 的所有 jar 包和类。

  • 自定义加载器 :程序员可以定制类的加载方式,以满足自己的特殊需求。派生自ClassLoader类。就比如说,我们可以对 Java 类的字节码( .class 文件)进行加密,加载时再利用自定义的类加载器对其解密

6.双亲委派模型

加载一个类的时候,具体是哪个类加载器加载

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上

总结双亲委派模型的执行流程

  • 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。

  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。

  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。

7.方法区Method Area

方法区是被所有线程共享

存储:已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存

8.虚拟机栈stack

栈溢出

栈也叫栈内存

每个线程都有自己的栈,它的生命周期是跟随线程的生命周期,线程结束栈内存也就释放,是线程私有的

线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)

  • JVM对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”或者“后进先出”原则。

  • 一个线程中只能由一个正在执行的方法(当前方法),因此对应只会有一个活动的当前栈帧

栈存储

栈中的数据都是以栈帧(Stack Frame)的格式存在

查看局部变量: javap

动态链接(Dynamic Linking)

知道当前帧执行的是哪个方法指向运行时常量池中方法的符号引用

方法返回地址(Return Address)

知道调用完当前方法后,上一层方法接着做什么即“return”到什么位置去。存储当前方法调用完毕后下一条指令的地址

9.堆heap

HotSpot是使用指针的方式来访问对象

  • 内存用于存放对象和数组

  • 中会存放指向对象类型数据的地址

  • 中会存放指向中的对象的地址

堆空间

  • 一个Java程序运行起来对应一个进程,一个进程对应一个JVM实例,一个JVM实例中有一个运行时数据区

  • 堆是Java内存管理的核心区域,在JVM启动的时候被创建,并且一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。

堆空间划分

堆内存内部空间所占比例:

  • 新生代与老年代的默认比例: 1:2

  • 伊甸园区与幸存者区的默认比例是:8:1:1

分代空间工作流程

存储在JVM中的Java对象可以被划分为两类:

  • 一类是生命周期较短的对象,创建在新生代,新生代中被垃圾回收

  • 一类是生命周期非常长的对象,创建在新生代,老年代中被垃圾回收,甚至与JVM生命周期保持一致。

  • 几乎所有的对象创建在伊甸园区,绝大部分对象销毁在新生代,大对象直接进入老年代。

GC总结

  • 频繁回收新生代

  • 很少回收老年代

  • 几乎不在永久代/元空间回收

查看堆内存大小

  • -Xms表示堆的起始内存,等价于-XX:InitialHeapSize,默认是物理电脑内存的1/64。

  • -Xmx表示堆的最大内存,等价于-XX:MaxHeapSize,默认是物理电脑内存的1/4。

  • -Xmn 表示新生代堆大小,默认新生代占堆的1/3空间,老年代占堆的2/3空间

OOM异常

JVM启动时,为堆分配起始内存,当堆中数据超过-Xmx所指定的最大内存时,

Java heap space 异常,此时说明Java虚拟机堆内存不够。

原因有二:

(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。

(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

10.垃圾回收GC =Garbage Collection

内存管理

Java中简化对象的释放,引入了自动的垃圾回收(Garbage Collection简称GC)机制。

通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器负责上的内存进行回收

线程不共享的程序计数器、虚拟机栈、本地方法栈中没有垃圾回收。

方法区的垃圾回收

判定一个类可以被卸载

1、此类所有实例对象没有在任何地方被引用,在堆中不存在任何该类的实例对象以及子类对象

2、该类对应的 java.lang.Class 对象没有在任何地方被引用

3、加载该类的类加载器没有在任何地方被引用

如何判断堆上的对象可以回收?

Java中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还

在使用,不允许被回收。

特殊情况:循环引用的对象也可以被回收

如何判断堆上的对象没有被引用?

常见的有两种判断方法:引用计数法可达性分析法。

引用计数法(Reference-Counting)

引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。

基本思路:

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;

任何时刻计数器为0的对象就是不可能再被使用的。

优点:

  • 简单,高效,现在的objective-c、python等用的就是这种算法。

缺点:

  • 引用和去引用伴随着加减算法,影响性能

  • 很难处理循环引用,相互引用的两个对象则无法释放。

因此目前主流的Java虚拟机都摒弃掉了这种算法

可达性分析算法

实现简单,执行高效,解决引用计数算法中循环引用的问题,是Java和C#选择的算法。

基本思路:

可达性分析将对象分为两类垃圾回收根对象(GC Root)和普通对象,对象与对象之间存在引用关系

将一系列GC Root的集合作为起始点,按照从上至下的方式搜索所有能够该合集引用到的对象(是否可达),并将其加入到该和集中,这个过程称之为标记(mark),被标记的对象是存活对象。 最终,未被探索到的对象便是死亡的,是可以回收的,标记为垃圾对象

GC Root的对象包括

  1. 静态变量(Static Variables):静态变量属于类,而不是实例对象。它们在整个程序执行期间都存在,并且被认为是 GC Root 对象。

  2. 活动线程(Active Threads):正在运行的线程也被视为 GC Root 对象。因为线程是程序执行的控制流,如果一个线程还在运行,那么它引用的对象也应该被保留。

  3. 栈帧(Stack Frames)中的局部变量和输入参数:栈帧中的局部变量和输入参数也是 GC Root 对象。它们在方法调用期间创建,并且随着方法的结束而销毁。

  4. JNI 引用(JNI References):通过 Java Native Interface (JNI) 在 Java 代码和本地代码之间传递的对象也被视为 GC Root 对象。这些对象的生命周期由本地代码管理。

垃圾回收算法-清除已死对象

当成功区分出内存中存活对象和死亡对象后,执行垃圾回收。

Stop-the-World:

JVM由于要执行GC而停止了应用程序的执行

除了GC所需的线程以外,所有线程都处于等待状态直到GC任务完成

GC优化 :减少Stop-the-world发生的时间,从而使系统具有高吞吐 、低停顿的特点。

吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC时间)。吞吐量数值越高,垃圾回收的效率就越高。

标记清除(Mark-Sweep)

(1)标记:使用可达性分析算法,标记出可达对象。

(2)清除:对堆内存从头到尾进行线性便遍历,如果发现某个对象没有被标记为可达对象,则将其回收。

缺点:

  • 效率问题(两次遍历)

  • 空间问题(标记清除后会产生大量不连续的碎片。JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。)

复制算法(Copying)

1.将堆内存分割成两块From空间 To空间,对象分配阶段,创建对象。

2.GC阶段开始,将GC Root搬运到To空间

3.将GC Root关联的对象,搬运到To空间

4.清理From空间,并把名称互换

优点:

  • 实现简单

  • 不产生内存碎片

缺点:

  • 将内存缩小为原来的一半,浪费了一半的内存空间,代价太高;如果不想浪费一半的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

  • 如果对象的存活率很高,我们可以极端一点,假设是100%存活,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变的不可忽视。 所以从以上描述不难看出,复制算法要想使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要克服50%内存的浪费。

标记压缩(Mark-Compact)

标记-清除法的一个改进版。同样,在标记阶段,该算法也将所有对象标记为存活和死亡两种状态;不同的是,在第二个阶段,该算法并没有直接对死亡的对象进行清理,而是通过所有存活对像都向一端移动,然后直接清除边界以外内存

优点:

标记整理算法不仅可以弥补标记清除算法中,内存区域分散的缺点,也消除了复制算法当中,内存减半的高额代价。

缺点:

如果存活的对象过多,整理阶段将会执行较多复制操作,导致算法效率降低

分代收集算法(Generational-Collection)

内存效率:

复制算法 > 标记清除算法 > 标记整理算法

内存整齐度:

复制算法 > 标记整理算法 > 标记清除算法。

内存利用率:

标记整理算法=标记清除算法>复制算法。

分代回收算法实际上是复制算法和标记整理法、标记清除的结合

分为老年代(Old Generation)和年轻代(Young Generation)

老年代就是很少垃圾需要进行回收的,年轻代就是有很内存空间需要回收,所以不同代就采用不同的回收算法,以此来达到高效的回收算法。

年轻代(Young Gen)

年轻代特点是区域相对老年代较小,对像存活率低。

这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对像大小有关,因而很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。

老年代(Tenure Gen)

老年代的特点是区域较大,对像存活率高。

这种情况,存在大量存活率高的对像,复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现

四种引用

强引用 不回收

只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用 内存不足即回收

SoftReference 类实现软引用。在系统要发生内存溢出(OOM)之前,才会将这些对象列进回收范围之中进行二次回收如果这次回收还没有足够的内存,才会抛出内存溢出异常软引用可用来实现内存敏感的高速缓存

弱引用 发现即回收

WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集(GC)之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。

虚引用 对象回收跟踪

PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。它主要用于执行一些清理操作或监视对象的回收状态

11.垃圾收集器

JVM中的默认垃圾收集器

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:Serial Old、Parallel Old、CMS

整堆收集器:G1

Serial/Serial Old收集器

只使用一个线程去回收。

作过程:

新生代(Serial)使用复制算法、老年代(Serial Old)使用标记整理算法

参数控制:

-XX:+UseSerialGC 串行收集器(单线程收集器)

ParNew 收集器

Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The world、对象分配规则、回收策略等都与Serial收集器完全一样,实现上这两种收集器也共用了相当多的代码。

工作过程:

新生代并行,老年代串行;新生代使用复制算法、老年代使用标记整理算法

参数控制:

-XX:+UseParNewGC ParNew收集器 -XX:ParallelGCThreads 限制线程数量

Parallel / Parallel Old 收集器

Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例。

**工作过程:**

新生代使用复制算法、老年代使用标记整理算法。

**参数控制:**

 `-XX:+UseParallelGC` 使用Parallel收集器

CMS收集器(老年代)

CMS(Concurrent Mark Sweep)“标记-清除”算法

获取最短回收停顿时间为目标的收集器

重视服务的响应速度

优点:

并发收集、低停顿

缺点:

产生大量空间碎片、并发阶段会降低吞吐量

G1收集器(区域化分代式)

1. 并行与并发G1能充分利用CPU、**多核环境下**的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

2. **分代收集:**分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能**独立管理整个GC堆**,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。**也就是说G1可以自己管理新生代和老年代了**。

3. 空间整合:由于G1使用了**独立区域**(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。

4. **可预测的停顿**:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为**M毫秒的时间片段内**,消耗在**垃圾收集上的时间不得超过N毫秒**。

垃圾回收器选择策略 :

- 客户端程序 : Serial + Serial Old;
- **吞吐率优先**的服务端程序(比如:计算密集型) : Parallel Scavenge + Parallel Old;
- **响应时间优先**的服务端程序 :ParNew + CMS。
- G1收集器基于标记整理算法实现的,不会产生空间碎片,可以精确地控制停顿,将堆划分为多个大小固定的独立区域,并跟踪这些区域的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(Garbage First)


网站公告

今日签到

点亮在社区的每一天
去签到