# JVM

# 编译型和解释型

# 编译型

使用专门的编译器,针对特定的平台,将高级语言源代码一次性编译成可被该平台硬件执行的机器码,并被包装成该平台所能识别的可执行程序的格式

C,C++, Golang

  • 执行速度快,效率高
  • 依靠编译器,跨平台性较差

# 解释型

使用专门的解释器对源程序助航解释成特定平台的机器码并立即执行

代码在执行时才被解释器一行行动态翻译和执行,而不是在执行前就完成翻译

Python, Js

# java

java属于编译型+解释型的高级语言,JVM对class文件进行翻译,这个翻译大多数是解释的过程,但也存在编译的过程,称为运行时编译,即Just In Time。

# JRE JDK JVM

# JVM

Java Virtual Machine java虚拟机,是将class文件字节码转换成特定平台下的机器码,从而实现了java的跨平台,此外它还提供内存管理,垃圾回收等功能

# JRE

Java Runtime Environment java运行时环境,是运行java程序所需要的程序包,包括jvm以及标准类库,如,java.lang, java.util,java.io JRE可以让用户运行已经编译好的java程序,但是不能用与开发java程序

# JDK

Java Develop Kit java开发工具包,能够支持开发和运行,调试,打包java程序,不仅包含了jre,还包括一些开发工具,javac,javap,jar,javadoc

image-20231218201234027

# 引用类型

不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾收集的影响

  1. 强引用:大部分对象都是强引用类型,当内存不足时,jvm会抛出OOM错误,也不会回收强引用对象
  2. 软引用:当内存足够的时候,垃圾回收不会回收该对象;当内存不足的时候,进行垃圾回收会把软引用对象回收。这样既能满足一定的需求,又不会对主要业务造成影响。断路器,查询1000条数据,数据存储就用软引用对象
  3. 弱引用:更短的生命周期,只要被垃圾回收器发现就会被回收。ThreadLocalMap里面的k:ThreadLocal就是软引用对象
  4. 虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可 能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收, 然后我们就可以在引用的对象的内存回收之前采取必要的行动。主要就是为了跟踪对象的垃圾回收情况。数据库连接池
image-20231217160643585

当某个被引用的对象(referent)被回收的时候,JVM 会将指向它的引用(reference)加入到引用队列的队列末尾,这相当于是一种通知机制。这个操作其实是由 ReferenceHandler 守护线程来做的,这个守护线程是在 Reference 静态代码块中建立并且运行的线程,所以只要 Reference 这个父类被初始化,该线程就会创建和运行,它的运行方法中依赖了比较多的本地 (native) 方法:

# 类文件加载机制

概念:虚拟机把class文件加载到内存并对数据进行校验,转换解析和初始化,形成虚拟机直接使用的java类型

流程:==load -> link(verify、prepare、resolve) -> intialize -> use -> unload==

# Load

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将二进制字符流转化为方法区的运行时数据结构
  3. 在java堆中生成java.lang.Class对象

Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。在 Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

# Verify

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

# Prepare

为类的静态变量分配内存,并将其初始化为默认值 age=0

# Resolve

把类中的符号引用转换为直接引用

# Initialize

对类的静态变量,静态代码块执行初始化操作 age=10

# 类加载器ClassLoader

# 概念:load阶段通过类的全限定名去获取二进制字节流,装在Class文件

# 类型:

  1. Bootstrap ClassLoader: jre/lib/rt.jar里面的class或者xbootclasspath参数配置的jar包,c++实现,唯一一个不是classLoader子类,父类为null。
  2. Extension ClassLoader:加载拓展jar包, /jre/lib/*.jar或者-Djava.ext.dirs指定的jar包
  3. App ClassLoader: 加载classpath中指定的jar包或者Djava.class.path所指定的类的jar包
  4. Custom ClassLoader :ClassLoader抽象类子类自定义的加载器。tomcat,jboss
img

# 加载原则:双亲委派模型

img

自底向上,按照custom,app,extension,bootstrap的顺序去执行,当一个类加载器收到类加载请求,它不会立即去加载,而是调用委托给父类加载器去执行,相当于所有的请求都移交给Bootstrap来做;如果父类加载器没有找到请求的类的情况下,该类加载器才会去尝试加载。否则抛出ClassNotFound的异常

双亲委派模型可以避免类的重复加载,也保证了java核心api不会被篡改。

# 破坏双亲委派

继承ClassLoader类并且重写loadClass()方法即可,【只重写findClass()方法可以不破坏双亲委派】

子类加载器会先尝试加载类,之后给父类加载器;或者父类加载器返回后在去从其他地方加载这个类

具体示例:

  1. Tomcat
img

CommonClassLoader作为 CatalinaClassLoaderSharedClassLoader 的父加载器。CommonClassLoader 能加载的类都可以被 CatalinaClassLoaderSharedClassLoader 使用。因此,CommonClassLoader 是为了实现公共类库(可以被所有 Web 应用和 Tomcat 内部组件使用的类库)的共享和隔离。

CatalinaClassLoaderSharedClassLoader 能加载的类则与对方相互隔离。CatalinaClassLoader 用于加载 Tomcat 自身的类,为了隔离 Tomcat 本身的类和 Web 应用的类。SharedClassLoader 作为 WebAppClassLoader 的父加载器,专门来加载 Web 应用之间共享的类比如 Spring、Mybatis。

每个 Web 应用都会创建一个单独的 WebAppClassLoader,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 WebAppClassLoader。各个 WebAppClassLoader 实例之间相互隔离,进而实现 Web 应用之间的类隔。

  1. SPI机制。

SPI 中,SPI 的接口(如 java.sql.Driver)是由 Java 核心库提供的,由BootstrapClassLoader 加载。而 SPI 的实现(如com.mysql.cj.jdbc.Driver)是由第三方供应商提供的,它们是由应用程序类加载器或者自定义类加载器来加载的。默认情况下,一个类及其依赖类由同一个类加载器加载。所以,加载 SPI 的接口的类加载器(BootstrapClassLoader)也会用来加载 SPI 的实现。按照双亲委派模型,BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它无法委托给子类加载器去尝试加载。

再比如,假设我们的项目中有 Spring 的 jar 包,由于其是 Web 应用之间共享的,因此会由 SharedClassLoader 加载(Web 服务器是 Tomcat)。我们项目中有一些用到了 Spring 的业务类,比如实现了 Spring 提供的接口、用到了 Spring 提供的注解。所以,加载 Spring 的类加载器(也就是 SharedClassLoader)也会用来加载这些业务类。但是业务类在 Web 应用目录下,不在 SharedClassLoader 的加载路径下,所以 SharedClassLoader 无法找到业务类,也就无法加载它们。

3.OSGi 连接 (opens new window)

线程上下文类加载器(ThreadContextClassLoader

当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。还记得我上面说的吗?每个 Web 应用都会创建一个单独的 WebAppClassLoader,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 WebAppClassLoader。这样就可以让高层的类加载器(SharedClassLoader)借助子类加载器( WebAppClassLoader)来加载业务类,破坏了 Java 的类加载委托机制,让应用逆向使用类加载器。

线程线程上下文类加载器的原理是将一个类加载器保存在线程私有数据里,跟线程绑定,然后在需要的时候取出来使用。这个类加载器通常是由应用程序或者容器(如 Tomcat)设置的。

Java.lang.Thread 中的getContextClassLoader()setContextClassLoader(ClassLoader cl)分别用来获取和设置线程的上下文类加载器。如果没有通过setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器。

# 一个类的静态块是否可能被执行两次

如果一个类,被两个 osgi的bundle加载, 然后又有实例被初始化,其静态块会被执行两次

# 运行时数据区RunTime Data Areas

image-20231218203942026 img

# 程序计数器(Program Counter Register)

PC

Java虚拟机的多线程是通过==线程轮流切换==、==分配处理器执行时间==的方式来实现的。 一个内核任一时间只会执行一条线程中的指令。

每条线程都有一个==独立==的程序计数器,这类内存区域被称为“线程私有”的内存,目的是为了==线程切换后了能恢复到正确的执行位置==。 Java方法计数器记录的是虚拟机==字节码指令的地址==;==本地native方法值为空== Undefined

==内存情况:唯一一个没有规定任何OutOfMemoryError情况的区域==

# Java虚拟机栈(Java Virtual Machine Stack)

VMStack

==虚拟机栈是一个线程执行的区域,保存着一个线程中方法的调用状态==。换句话说,一个Java线程

的运行状态,由一个虚拟机栈来保存,所以虚拟机栈肯定是线程私有的,独有的,随着线程的创建而创建。

==线程私有==,其生命周期和线程相同 ==方法执行==时,JVM同步创建一个==栈帧==(Stack Frame)用与存储==局部变量表,操作数栈,动态连接,方法出口==

每一个被线程执行的方法,为该栈中的栈帧,即每个方法对应一个栈帧. 调用一个方法,就会向栈中压入一个栈帧;一个方法调用完成,就会把该栈帧从栈中弹出。

局部变量表存放了编译期可知的各种JVM虚拟机==基本数据类型,对象引用== (reference)和==returnAddress==(指向一条字节码指令的地址)(结果返回地址),数据类型在其中的存储空间以==局部变量槽Slot==,==double,long,占两个,其余占一个==。

虚拟机栈为虚拟机执行Java方法(字节码)服务

==局部变量表==:方法中定义的局部变量以及方法的参数存放在这张表中局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用

==操作数栈==:以压栈和出栈的方式存储操作数的

==动态链接==:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。

==方法返回地址==:当一个方法开始执行后,只有两种方式可以退出,==一种是遇到方法返回的字节码指令;一种是遇见异常==,并且这个异常没有在方法体内得到处理

# JAVA堆 (Java Heap)

JavaHeap

虚拟机管理内存最大的一块,==所有线程共享==,虚拟机启动时创建,唯一目的==存放对象实例==

==新生代,老年代,永久代,Eden空间,From Survivor空间,To Survivor空间==

从内存分配的角度看,所有线程共享的Java堆中可以划分出许多线程私有的==分配缓冲区TLAB==(Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。 JVM默认为每个线程在==Eden上开辟一个buffer区域==,用来加速对象的分配,称之为TLAB,全称:==Thread Local Allocation Buffer==。 ==对象优先会在TLAB上分配==,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。

# 本地方法栈(Native Method Stacks)

NativeStack

本地方法栈为虚拟机使用到本地方法服务 作用与java虚拟机栈所发挥的作用相似

# 方法区(Method Area)

MethodArea

各个==线程共享==的内存区域,存储JVM加载的==类型信息,常量,静态变量,即时编译器编译后的代码缓存==

方法区8之后已经是概念,真正存在本地内存中。MetaSpace

# 运行时常量池(Runtime Contstant Pool)

ConstantPool

==方法区的一部分==,Class文件常量池表(Constant Pool Table)用于存放编译期生成的各种==字面量和符号引用==,类加载后存放到方法区的运行时常量池中

字面量:java代码在编译过程中是无法构建引用的,字面量就是在编译时对于数据的一种表示:

int a=1;//这个1便是字面量
String b="iloveu";//iloveu便是字面量
1
2

符号引用:由于在编译过程中并不知道每个类的地址,因为可能这个类还没有加载,所以如果你在一个类中引用了另一个类,那么你完全无法知道他的内存地址,那怎么办,我们只能用他的类名作为符号引用,在类加载完后用这个符号引用去获取他的内存地址。

例子:我在com.demo.Solution类中引用了com.test.Quest,那么我会把com.test.Quest作为符号引用存到类常量池,等类加载完后,拿着这个引用去方法区找这个类的内存地址。

动态性:既有预置入Class文件中常量池的内容,也可以在运行期间将新的常量放入池中(String类中的intern()方法)

# 直接内存(Direct Memory)

直接内存并不是虚拟机运行时数据区的一部分。

JDK1.4新加入NIO(New Input/Output)类,引入了一种基于通道 (Channel)与缓冲区(Buffer)的I/O方式,它可以直接使用Native函数库直接分配堆外内存,通过Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作

优点:某些场景显著提高性能,避免Java堆和Native堆来回复制数据

# 内存情况

线程私有 程序计数器 Java虚拟机栈 本地方法栈 线程共享 Java堆 方法区 运行时常量池

# 内存异常

OutOfMemoryError:内存,堆,栈拓展失败 Java虚拟机栈 本地方法栈 Java堆 运行时常量池 直接内存 StackOverFlowError:栈深度超出Max允许申请 Java虚拟机栈 本地方法栈

error

# 几种常见的指向关系

==栈指向堆:== 如果一个栈帧中有一个变量,类型为引用类型,这就是典型的栈中元素指向堆的中的对象

img

==方法区指向堆==:方法区会存放静态变量,常量等数据,下面情况就是典型的方法区中的元素指向堆中的对象

private static Object obj=new Object();
1
img

==堆指向方法区==: 方法区中会包含类的信息,堆中会有对象,对象头中会包含类元信息的指针,如下二图

img

# Java内存模型JMM

# Java对象内存模型

java对象包含三个部分:==对象头,实例数据,对齐填充==

img

# 概念

JVM运行时数据区是一种规范,而JVM内存模式是对该规范的实现

# 图形展示

一块是非堆区,一块是堆区 堆区分为两大块,一个是Old区,一个是Young区 Young区分为两大块,一个是Survivor区(S0+S1),一块是Eden区 S0和S1一样大,也可以叫From和To

img

# 对象创建过程

一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大对象会字节分配到老年区

img

我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了 挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor 区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的 时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的

# 常见问题

  • 如何理解Minor/Major/Full GC

Minor GC:新生代

Major GC:老年代

Full GC:新生代+老年代

  • 为什么需要Surivior区?只有Eden区不行吗?

如果没进行一次Minor GC,存货的对象就会进入老年代,这样的化老年代空间很快就被填满触发Major GC(Major GC一般伴随着Minor GC,可以看作是Full GC),老年代空间较大,进行一次Major GC时间很长,频发的GC会影响大型程序的执行和响应速度.

假如增加老年代空间,更多存活对象才能填满老年代。虽然降低Full GC频率,但是随着老年代空间加大,一 旦发生Full GC,执行所需要的时间更长。 假如减少老年代空间,虽然Full GC所需时间减少,但是老年代很快被存活对象填满,Full GC频率增加。

所以Surivoir区就是为了减少老年区的对象,从而减少Full GC的频率, 提高系统的执行效率

  • 为什么需要两个Survivor区(默认空间比例 8:1:1)

    解决碎片化空间. 可以保证永远有一个空的Survivor和一个没有碎片化空间的survivor区. 当Eden发生Minor GC后,存货下来的对象会进入S0,当下一次Eden区发生Minor GC后, 存货下来的对象和S0中存活的对象就会被复制到S1, 如果一个对象复制次数达到16次就会被转移到老年代.

  • 堆内存中都是线程共享的区域吗?

JVM默认为每个线程在Eden上开辟一个buffer区域,用来加速对象的分配,称之为TLAB,全称:Thread Local Allocation Buffer。 对象优先会在TLAB上分配,但是TLAB空间通常会比较小,如果对象比较大,那么还是在共享区域分配。

# 垃圾回收GC

# 如何判断一个对象需要被回收

  1. 引用计数法: ==当一个对象被引用计数就+1,引用被释放就-1==,当计数为0就说明没有地方使用这个对象,那么就是垃圾需要被回收, 但是这种方法在两个对象相互引用的情况下存在弊端:==两个对象都持有对方的引用,导致无法被识别回收.==
  2. 可达性分析法: 根据GC ROOT的对象,开始向下寻找,看某个对象是否可达. 即==判断某个对象到GC Root是否存在一个可达的引用链.== GCRoot: ==类加载器, Thread, static 成员, 常量引用, 本地方法栈的变量, 虚拟机栈的本地方法表.==
img

# 垃圾回收的时机

  1. Eden区不够用
  2. Surivior区不够用
  3. Old区不够用
  4. 方法区不够用
  5. System.gc()

# 常见的垃圾回收算法

  • 标记-清除: 找出堆内存中所有需要清除的对象进行标记,然后进行回收,释放占有的内存

    标记清除后会产生大量的内存碎片 ; 标记和清除这两个过程都比较耗时

  • 标记-复制: 将内存分为两块,每次只用其中的一块, 当一块内存使用完, 就将上面的存活对象复制到空白的区域. 可用内存只有原来的一半 ; 在对象存活率较高时就要进行较多的复制操作,效率将会变低

  • 标记-整理: 先进行扫描标记处需要回收的对象, 然后将所有的存活的对象向一端移动,然后清除到边界以外的内存 多了整理这一步,因此效率也不高,适合老年代这种垃圾回收频率不是很高的场景

  • 分代收集: 将堆内存分为两大块, Old区和Young区, Young区又分为Eden区和两个Surivor区, 根据区域的不同采用不同的回收算法进行回收,以此达到高效率执行. Young区一般采用标记-复制法, Old区一般采用标记-清除或者标记-整理法 扬长避短

# 垃圾收集器

img

# Serial

==单线程收集器==, 只会使用一个收集线程或者CPU去完成垃圾收集工作, 更要命的是垃圾回收期间必须==暂停其他线程==

优点:==简单高效,拥有很高的单线程收集效率==

缺点:收集过程需要暂停所有线程

算法:==标记-复制算法==

适用范围:新生代

应用:==Client模式==下的默认新生代收集器

img

# Serial Old

==单线程收集器==, 只能使用一个线程或者一个CPU区执行收集工作, 需要Stop The World. 但是采用的是==标记-整理==方法

img

# ParNew

Serival收集器的多线程版本

优点:在多CPU时,比Serial效率高。

缺点:收集过程暂停所有应用程序线程,单CPU时比Serial效率差。

算法:标记-复制算法

适用范围:新生代

应用:运行在Server模式下的虚拟机中首选的新生代收集器

img

# Parallel Scavenge

和ParNew差不多, 但是更加关注系统的吞吐量:(运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)

-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间

-XX:GCRatio直接设置吞吐量的大小。

# Parallel Old

parallel Scavenge的老年版本, 标记-整理,也关注系统的吞吐量

# CMS

CMS(Concurrent Mark Sweep)收集器是一种以==获取最短回收停顿时间==为目标的收集器。 采用的是"==标记-清除算法==",整个过程分为4步

  1. ==初始标记== CMS initial mark 标记GC Roots直接关联对象,不用Tracing,速度很快(找出GC Root根对象)
  2. ==并发标记== CMS concurrent mark 进行GC Roots Tracing (依根寻果,链路追踪可达对象即活的对象)
  3. ==重新标记== CMS remark 修改并发标记因用户程序变动的内容(查漏补缺,并发标记过程中状态变化对象)
  4. ==并发清除== CMS concurrent sweep 清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为 ==浮动垃圾==

优点:并发收集、低停顿

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

img

# G1

使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个 大小相等的独立区域(==Region==),虽然还保留有新生代和老年代的概念,但新生代和老年代不再 是物理隔离的了,它们都是一部分Region(不需要连续)的集合。 每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是==2的n次幂== 如果对象太大,一个Region放不下[超过Region大小的50%],那么就会直接放到==H==中

设置Region大小:-XX:G1HeapRegionSize=M

所谓Garbage-Frist,其实就是==优先回收垃圾最多的Region区域==

(1)==分代收集==(仍然保留了分代的概念)

(2)==空间整合==(整体上属于“==标记-整理==”算法,不会导致空间碎片)

(3)可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒

img
  1. ==初始标记==(Initial Marking) 标记以下GC Roots能够关联的对象,并且修改TAMS的值,需要暂停用户线程
  2. ==并发标记==(Concurrent Marking) 从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行
  3. ==最终标记==(Final Marking) 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程
  4. ==筛选回收==(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划
img

# ZGC

JDK11新引入的ZGC收集器,不管是物理上还是逻辑上,ZGC中已经不存在新老年代的概念了,会分为一个个page,当进行GC操作时会对page进行压缩,因此没有碎片问题 只能在64位的linux上使用,目前用得还比较少

(1)可以达到10ms以内的停顿时间要求

(2)支持TB级别的内存

(3)堆内存变大后停顿时间还是在10ms以内

# 垃圾回收期分类

  • ==串行收集器: Serial , Serial Old==
  • ==并行收集器: Parallel Scanvenge, Parallel Old , PreNew==
  • ==并发收集器: CMS, G1, ZGC==

# 常见问题

  • ==吞吐量和停顿时间==(==垃圾收集器性能的评判标准==) 吞吐量: 程序运行时间 / 程序运行时间+垃圾回收时间 停顿时间: 垃圾收集器进行垃圾回收终端应用执行响应的时间 停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验; 高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互 的任务。
  • 如何选择合适的垃圾收集器 优先调整堆的大小让服务器自己来选择 如果内存小于100M,使用串行收集器 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选 如果允许停顿时间超过1秒,选择并行或JVM自己选 如果响应时间最重要,并且不能超过1秒,使用并发收集器
  • 如何开启需要的垃圾收集器 (1)串行 -XX:+UseSerialGC -XX:+UseSerialOldGC (2)并行(吞吐量优先): -XX:+UseParallelGC -XX:+UseParallelOldGC (3)并发收集器(响应时间优先) -XX:+UseConcMarkSweepGC -XX:+UseG1GC
img