JVM相关

JVM的组成

JVM包含两个子系统与两个子组件,四个部分组成:

  • 两个子系统:类加载器 Class Loader,执行引擎Execution engine
  • 两个子组件:运行时数据区 Runtime data area,本地接口Native interface
  • 类加载器:根据给定的类全名来装载class文件到运行时数据区的方法区
  • **执行引擎:**执行class中的指令
  • 运行时数据区:常说的jvm内存
  • 本地接口:是其他编程语言的交互接口

**运行流程:**首先通过编译器把java文件转换为字节码文件,类加载器把字节码文件加载到内存中,将其放在运行时数据区的方法区内。字节码文件是JVM的一套指令集规范,不能直接交给底层的操作系统来执行,因此需要特定的命令解析器执行引擎Execution engine,将字节码翻译成底层指令,再交给CPU去执行,而这个过程需要用到其他语言的本地库接口Native Interface来实现整个功能。

所有线程共享的区域:方法区、堆

线程私有区域:虚拟机栈、本地方法栈、程序计数器

JVM的运行时数据区

Java虚拟机在执行Java程序的过程中会把它所管理的内存区域分成几个不同的数据区域,这些区域各自有各自的用途,以及创建与销毁的时间。

  • 方法区:用于存储已被虚拟机加载的类信息,常量,静态变量,即编译后的代码等数据
  • :Java虚拟机中内存最大的一块,是被所有线程共享的,几乎所有对象的实例都在这里.如(成员变量)
  • Java栈:每个方法在执行的同时都会在Java虚拟机栈中创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息 栈帧就是Java虚拟机栈中的一个单位
  • 本地方法栈 **:**与虚拟机栈的作用是一样的,只是Java栈是存储Java方法的,本地方法栈是虚拟机调用native方法的 Native 关键字修饰的方法是看不到的,Native 方法的源码都是 C 和C++ 的代码
  • 程序计数器:当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要程序计数器

为什么要线程计数器?因为线程是不具备记忆功能

详细的介绍下程序计数器 重点理解

1、程序计数器是一块较小的内存,可以看作是:保存当前线程所在执行字节码指令的地址(行号)

2、由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之前程序计数器互不影响,独立存储。称之为线程私有的内存,程序计数器内存区域是虚拟机中唯一没有规定OutOfMemory的区域

总结:也可以把它叫做线程计数器

详细介绍下Java虚拟机栈 重点理解

1、Java虚拟机栈是线程私有的,它的生命周期和线程相同

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

3、解释:虚拟机栈是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中要存储局部变量表,操作数栈,动态链接,方法出口等

详细的介绍Java堆吗?重点理解
  • Java堆是Java虚拟机所管理内存最大的一块区域,是被所有线程所共享的一块区域,在虚拟机启动时创建。此内存区域的唯一目的是存放对象实例
  • 在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配
  • Java堆是垃圾回收管理的主要区域,因此也被称之为"Gc堆"
  • 从垃圾回收的角度看,堆分为新生代与老年代
  • 从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
  • 根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
简单解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法

1、通常定义一个基本数据类型的变量,一个对象的引用,还有函数调用的保存都是放在栈空间。

2、new关键字与构造器创建的对象放在堆空间,堆是垃圾回收器管理的主要区域。由于现在垃圾回收器都采用分代收集算法,所以堆空间还可以细分为新生代和老年代

3、方法区与堆是线程共享是区域,用于存储已经被JVM加载的类信息、常量、静态变量、编译器编译后的代码等数据

4、程序中的字面量如直接书写的100、"hello"和常量都是放在常量池中,常量池是方法区的一部分

5、栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过JVM的启动参数来进行调整,栈空间用光了会引发StackOverflowError,而堆和常量池空间不足则会引发OutOfMemoryError

String str = new String("hello");

上面的语句中变量str放在栈上,用new创建出来的字符串对象放在堆上,而"hello"这个字面量是放在方法区的。

对象分配原则
  • 对象优先分配在Eden区,如果Eden区没有足够的空间,虚拟机执行一次MonitorGC
  • 大对象之间进入老年代,大对象是指需要大量连续内存空间的对象,这样做的目的是避免在Eden区与Survivor区之间发生大量拷贝
  • 长期存活的对象进入老年代。
  • Eden,To survivor,From survivior 的比例是8:1:1,虚拟机为每个对象定义了一个年龄计数器,每发生一次MonitorGc后年龄+1,到达阈值15时进入到老年代
  • 动态判断对象的年龄。如果survivor区相同年龄的所有对象大小的总和大于survivor空间的一半,年龄大于等于该年龄的对象可以直接进入到老年代
  • 空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
什么是类的加载

类的加载是指将类的.class文件中的二进制数据读入到内存中,将其放到运行时数据区的方法区。然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终结果是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并向Java程序员提供了访问方法区内的数据结构的接口.

类加载器

类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。

JVM里的有三种classloader(类加载器),为什么会有多种?

1、 BootstrapClassLoader:

主要加载JVM自身工作需要的类,这个classLoader完全由JVM自己控制,需要加载哪个类,怎么加载都是由JVM自己控制,别人也访问不到这个类。比如目录下的%JAVA_HOME%/jre/lib/下的resources.jar;rt.jar等,该loader底层采用C++编写,自然你也就不能调用。

2、ExtClassLoader :

用于加载一些扩展类,系统变量为java.ext.dirs中的类。作用:加载开发者自己扩展类。

3、AppClassLoader:

用于加载用户类,这个就是java.class.path下的类,也就是我们自己编写出来的类。

其中这三个加载器顺序为

BootstrapClassLoader>ExtClassLoader>AppClassLoader,之所以这样设计,主要是为了扩展与安全。

首先你将BootstrapClassLoader作为一个核心类加载器,只加载核心类,不与其他耦合在一起。并且为何要设计这三个加载器,就应该和双亲委派机制放在一起了。

何为双亲委派机制:简单来说就是当你需要加载类的时候,必须从顶级父加载器先加载,如果父加载不了,则交给子加载器。就相当于小孩子要做决定的时候,要先问问父亲怎么做。

什么是双亲委派模型?

在介绍双亲委派模式之前介绍一下来加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一起确立在JVM中的唯一性。每一个类,都有一个独立的类空间命名。类加载器就是根据指定全限定名将class文件加载到JVM内存,然后在转化为class对象。

  • 双亲委派模型:如果一个类记载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父加载器区完成,每一层的类加载器都是这样。这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载器无法完成加载请求(它的搜索范围内没找到所需要的类时),子加载器才会尝试去加载类。

总结就是:当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。

为什么要有这种双亲委派机制?

1、避免类的重复加载, 保证唯一性。如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证.

2、保证安全:Java 核心类库(如 java.lang 包下的类)都是由启动类加载器加载的,其他的类都是由其它类加载器加载的。这样,我们就可以保证 Java 核心类库的安全性,因为不同的应用程序无法改变这些类的实现。另外,也可以在类加载过程中做一些安全性检查

什么情况下我们需要破坏双亲委派模型?
  1. 由于加载范围的限制,顶层的ClassLoader无法访问底层ClassLoader所加载的类,此时需要破坏双委派模型。

2、所谓的打破就是,只要我加载类的时候,不是从APPClassLoader->Ext ClassLoader->BootStrap ClassLoader 这个顺序找,那就算是打破了啊。实现就是自定义个ClassLoader,重写loadClass方法(不依照往上开始寻找类加载器,那就算是打破双亲委派机制了。

3、最常见的比如tomcat,在初学时部署项目,我们是把war包放到tomcat的webapp下,这意味着一个tomcat可以运行多个Web应用程序,那假设我现在有两个Web应用程序,它们都有一个类,叫做User,并且它们的类全限定名都一样,比如都是com.yyy.User。但是他们的具体实现是不一样的,那么Tomcat是如何保证它们是不会冲突的呢?答案就是,Tomcat给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找

为什么tomcat要打破双亲委派机制?
Tomcat除了WebAppClassLoader其他的类加载器?

并不是Web应用程序下的所有依赖都需要隔离的,比如Redis就可以Web应用程序之间共享(如果有需要的话),因为如果版本相同,没必要每个Web应用程序都独自加载一份。

做法也很简单,Tomcat就在WebAppClassLoader上加了个父类加载器(SharedClassLoader),如果WebAppClassLoader自身没有加载到某个类,那就委托SharedClassLoader去加载

JDBC你不是知道吗,听说它也是破坏了双亲委派模型的,你怎么理解的

JDBC定义了接口,具体实现类由各个厂商进行实现嘛(比如MySQL)

类加载有个规则:如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。

我们用JDBC的时候,是使用DriverManager进而获取Connection,DriverManager在java.sql包下,显然是由BootStrap类加载器进行装载

当我们使用DriverManager.getConnection()时,得到的一定是厂商实现的类。

但BootStrap ClassLoader会能加载到各个厂商实现的类吗?

显然不可以啊,这些实现类又没在java包中,怎么可能加载得到呢

DriverManager的解决方案就是,在DriverManager初始化的时候,得到「线程上下文加载器」

去获取Connection的时候,是使用「线程上下文加载器」去加载Connection的,而这里的线程上下文加载器实际上还是App ClassLoader

所以在获取Connection的时候,还是先找Ext ClassLoader和BootStrap ClassLoader,只不过这俩加载器肯定是加载不到的,最终会由App ClassLoader进行加载

JVM加载class文件的原理机制

1、Jvm的类加载是由类加载器ClassLoader和它的子类来实现的,类加载器负责运行时查找和装入类文件中的类。

2、编译后的Java文件并不是一个可执行程序,而是一个或者多个类文件。当Java程序需要使用到某个类时,JVM会确保这个类已经被加载连接 (验证、准备、解析)和初始化

3、类的加载是指把类的.class文件中的数据读入到内存中,然后产生与所加载类对应的Class对象

4、类的加载有两种方式:

  • 隐式装载:程序在运行过程中碰到了通过new等方式生成对象时,隐式调用类加载器加载到对应的类到JVM中
  • 显示装载:通过class.forName()等方法,显式的加载所需要的类

5、当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤

6、Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销

7、最后JVM对类进行初始化,包括:

  • 1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;
  • 2)如果类中存在初始化语句,就依次执行这些初始化语句。
类的生命周期(类装载的执行过程)

类的生命周期包括:加载、连接、初始化、使用和卸载

其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段

1、加载:查找并加载类的二进制数据,在Java堆中也创建一个java.lang.Class类的对象

2、连接:连接又包含三部分内容,验证、准备、解析

  • 验证,文件格式、元数据、字节码、符号引用验证
  • 准备,为类的静态变量分配内存,并将其初始化为默认值
  • 解析,把类中的符号引用转换为直接引用

3、初始化:为类的静态变量赋予正确的初始值

4、使用:new出对象程序中使用

5、卸载:执行垃圾回收

在类加载的五个过程中,加载、验证、准备、初始化这四个阶段的顺序是确定的。而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定。另外注意这里的几个阶段是按顺序开始,而不是按照顺序进行或者完成。

Java的类加载初始化顺序

类的初始化遵从以下三个顺序,优先级依次递减

1、静态变量(对象)优先于非静态变量(对象)的初始化,静态变量只会初始化一次,非静态变量可能会初始化多次

2、父类优先于子类初始化

3、成员变量按照定义先后顺序进行初始化

例如:父类静态变量 > 父类静态代码块 > 子类静态变量 > 子类静态代码块 > 父类非静态变量 > 父类非静态代码块 > 父类构造函数 > 子类非静态变量 > 子类非静态代码块 > 子类构造函数。

如何判断对象可以被回收?

1、引用计数法

每个对象都有一个私有的引用计数属性,新增一个引用计数+1,释放一个引用时计数-1,引用计数为0则表示对象可被回收

2、可达性分析(java使用)

从GC Roots向下搜索,如果一个对象和GC Roots之间没有可达路径,则该对象是不可达的。

不可达对象不一定会成为可回收对象,被判定不可达的对象成为可回收对象至少要经历两次标记过程,如果两次标记过程中仍然没有逃脱成为可回收对象的可能,则基本上就成为可回收对象了。

3、该对象没有重写finalize()方法或者finalize()已经被执行过则直接回收

垃圾收集算法

GC最基础的算法有三种: 标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。

  • 标记 -清除算法,"标记-清除"(Mark-Sweep)算法,如它的名字一样,算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
  • 复制算法,"复制"(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
  • 标记-压缩算法,标记过程仍然与"标记-清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
  • 分代收集算法,"分代收集"(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
垃圾回收器
  • Serial收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
  • ParNew收集器,ParNew收集器其实就是Serial收集器的多线程版本。
  • Parallel收集器,Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。
  • Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和"标记-整理"算法
  • CMS收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
  • G1收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征
GC分哪两种Minor GC和Full GC有什么区别?
  • Minor GC
    • Minor GC指新生代GC,即发生在新生代(包括Eden区和Survivor区)的垃圾回收操作,当新生代无法为新生对象分配内存空间的时候,会触发Minor GC。因为新生代中大多数对象的生命周期都很短,所以发生Minor GC的频率很高,虽然它会触发stop-the-world,但是它的回收速度很快。
  • Major GC
    • Major GC清理Tenured区,用于回收老年代,出现Major GC通常会出现至少一次Minor GC。
  • Full GC
    • Full GC是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。Full GC不等于Major GC,也不等于Minor GC+Major GC,发生Full GC需要看使用了什么垃圾收集器组合,才能解释是什么样的垃圾回收。
什么情况下会触发Full GC?

1、调用 System.gc():只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

2、老年代空间不足:当老年代中没有足够的空间来分配一个大对象时,会先尝试进行Minor GC,如果仍然无法获得足够的空间,则会触发Full GC。

3、空间分配担保失败:分配担保失败:在Minor GC后,如果survivor区无法容纳所有幸存对象,那么就要将部分幸存对象转移到老年代。如果老年代剩余空间不足以容纳这些对象,就需要进行Full GC。

堆区为什么要分为新生代和老年代

提高效率。对象的寿命有长有短,寿命长的放在一个区,寿命短的放在另一个区。不同的区采用不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点。

1)为什么要有 Survivor 区

如果没有 Survivor 区,那么 Eden 每次满了清理垃圾,存活的对象被迁移到老年区,老年区满了,就会触发非常耗时的Full GC,解决办法:

①增加老年代内存,那么老年代清理频次减少,但清理一次花费时间更长。

②减少老年代内存,老年代一次 Full GC 时间更少,频率增加。
2)为什么要加两个 Survivor

避免产生内存碎片。为了不产生内存碎片,才用复制算法,将 Eden 区和 Survivor 区存活的对象整齐的放到一个空的内存。因为生命周期一般都比较短,所以在存活对象不多的情况下,复制算法效率还是比较高

GC的流程
JVM内存分配策略

1、对象优先在Eden分配

大多数情况下,对象在新生代Eden区分配,当Eden区空间不够,发起Minor GC

2、大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。

3、长期存活的对象进入老年代

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加1岁,增加到一定年龄则移动到老年代中。

-XX:MaxTenuringThreshold 用来定义年龄的阈值。

4、动态对象年龄判定

虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

5、空间分配担保

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。

什么情况下会触发Full GC?

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

1、调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

2、老年代空间不足

老年代空间不足的常见场景:大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。

除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

3、空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。

4、JDK 1.7 及以前的永久代空间不足

5、Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

如何判断对象是否存活

https://blog.csdn.net/qq_41574321/article/details/87278409

引用计数法会存在什么问题
JVM调优方法,用哪些参数

参考链接:https://github.com/Homiss/Java-interview-questions/blob/master/JVM/JVM%E9%9D%A2%E8%AF%95%E9%A2%98.md

https://juejin.cn/post/6844904125696573448?searchId=2023071510025577C23E7EF971CAAF33F0#heading-4

双亲委派模型

https://juejin.cn/post/7020935051860770853?searchId=202308062059028C2D02A9458F236F21BA

相关推荐
鸽鸽程序猿18 小时前
【JavaSE】简单理解JVM
java·jvm
小毛驴85018 小时前
JDK主流版本及推荐版本
jvm
没有bug.的程序员1 天前
微服务网关:从“必选项”到“思考题”的深度剖析
java·开发语言·网络·jvm·微服务·云原生·架构
tgethe1 天前
==和equals的区别
java·开发语言·jvm
步步为营DotNet1 天前
深度探索.NET 中 IAsyncEnumerable:异步迭代的底层奥秘与高效实践
java·jvm·.net
winfield8211 天前
GC 日志全解析:格式规范 + 问题分析 + 性能优化
java·jvm
无限进步_1 天前
C++多态全面解析:从概念到实现
开发语言·jvm·c++·ide·git·github·visual studio
懒惰蜗牛2 天前
Day66 | 深入理解Java反射前,先搞清楚类加载机制
java·开发语言·jvm·链接·类加载机制·初始化