格式塔意识

格式塔意识

JVM

63
2021-02-06

类加载器分类

启动类加载器BootstrapClassLoad

  • 默认加载jre/lib下的类文件

  • 位于jvm环境中,无法在代码中获取,因为是由虚拟机底层提供的,使用C++编写的

通过启动类加载器去加载用户定义jar包:

  • 放入jre/lib目录下进行扩展,不推荐

  • 使用参数进行扩展,-Xbootclasspath/a:/jar(路径)

默认类加载器

  • 位于sun.misc.Launcher中,是一个静态内部类,继承自URLClassloader

粘贴的图像

扩展类加载器ExtensionClassLoader

  • /jre/lib/ext

  • 使用java编写的类加载器

通过扩展类加载器去加载用户jar包:

  • 放入/jre/lib/ext下进行扩展

  • 使用参数进行扩展,-Djava.ext.dirs=jar,会覆盖原始目录,用;/:分割

应用程序类加载器AppClassLoader

  • 加载classpath下

类加载器的双亲委派机制

如何主动加载一个类:

  • 使用Class.forName,使用当前类的类的类加载器

  • 获取类加载器,通过loadClass方法

    image-20231022161433797

解决的问题:

image-20231022161641128

打破双亲委派机制

自定义类加载器

  • 自定义类加载器并重写loadClass方法

  • tomcat使用自定义类加载器实现web应用之间的隔离,每一个应用会有一个独立的类加载器

双亲委派机制的核心代码位于loadClass方法中:

image-20231022170302249

自定义类加载器不指定父类会默认指向应用程序类加载器

只有相同类加载器+相同的类限定名才会被认为是一个类

正确的实现一个自定义类加载器的方法是重写findClass方法,这样不会破坏双亲委派机制

线程上下文类加载器

image-20231022173155220

jdbc的spi机制:jdk内置的服务提供发现机制

spi中使用了线程上下文中保存的类加载器进行类的加载,一般是应用程序类加载器

image-20231022173018018

jdbc只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,类的加载依然遵循双亲委派机制

OSGi框架的类加载器

OSGi使用了类加载器实现热部署

案例:使用arthas不停机解决线上问题

image-20231024211005078

jdk9之后的类加载器

jdk8之前的类加载器

位于rt.jar中的sun.misc.Launcher.java

jdk9之后的类加载器

引入了module的概念

  • 启动类加载器使用java编写,位于jdk.internal.loader.ClassLoaders类中。继承自BuiltinClassLoader实现从模块中找到要加载的字节码资源文件

  • 扩展类加载器替换为平台类加载器(PlatformClassLoader),继承关系从URLClassLoader变成了BuiltinClassLoader

运行时数据区

jvm运行java程序过程中管理的内存区域

image-20231024212847157

程序计数器

又叫PC寄存器,每个线程会通过程序计数器记录当前要执行的字节码指令的地址

image-20231024214412619

加载阶段:

image-20231024214733356

  • 程序计数器保存的是字节码指令的地址,可以控制指令的进行,实现分支跳转异常等

  • 多线程情况下,可以记录cpu切换前解释执行到哪一句指令

程序计数器只会存储定长的内存地址,不会发生内存溢出

Java虚拟机栈

FILO先进后出

随着线程的创建而创建,随线程的销毁而回收

栈帧(Frame)的组成

image-20231024220108869

局部变量表

存放方法执行中所有的局部变量

image-20231024220528896

栈帧中的局部变量表是一个数组,每一个位置是一个槽(slot),long和double类型占用两个槽,其他类型一个槽

image-20231024220712143

实例方法中的局部变量表的0号位存放的是this,存放该实例对象的地址

局部变量表保存的内容:

  1. 实例方法的this对象

  2. 方法的参数

  3. 方法体中声明的局部变量

局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以被再次使用

操作数栈

存放执行过程中的中间数据

在编译期就可以确定最大深度,从而在执行时分配内存大小

帧数据
动态链接

动态链接保存了符号引用(编号)到运行时常量池的内存地址的映射关系,保存了其他类的属性和方法

方法出口

在当前栈帧中存储的下一个栈帧的下一条指令的地址

方法结束时,当前栈帧会被弹出,程序计数器应该指向上一个栈帧中下一条指令的地址,当前栈帧中要存储此方法出口的地址

异常表

代码中异常的处理信息

try,catch跳转到的位置

image-20231024222912741

栈内存溢出

栈帧过多,占用内存超过栈内存可以分配的最大大小就会溢出

StackOverflowError

默认大小

Linux

BSD

Solaris

Windows

x86:1024kb;ppc:2048kb

x86:1024kb

64位:1024kb

基于操作系统默认值

设置大小

-Xss栈大小(单位)

单位:默认字节、k、m、g

  • 也可以用-XX:ThreadStackSize=1024

  • HotSpot JVM对栈大小的最大值和最小值有要求,win64下jdk8测试最小值为180k,最大值为1024m

  • 局部变量过多,操作数栈深度过大也会影响栈大小

一般情况下,栈深度最多也只是几百,所以可以指定为-Xss256k节省内存

本地方法栈

存储native方法的栈帧

在HotSpot中,和虚拟机栈使用同一个栈空间

image-20231024230632744

Java堆

属于java程序中内存最大的一块区域,创建出来的对象都位于堆

栈上的局部变量表,可以存放堆内存对象的引用。静态变量也可以存放堆内存的引用,所以可以实现对象在线程间共享

  • used

  • total

  • max

image-20231024231518082

默认max是系统内存的1/4,total是1/64

-Xmx(max,必须大于2m) -Xms(total,必须大于1m)

一般将两个参数值设为一样

方法区(Method Area)

存放基础信息的位置,线程共享

image-20231024233712742

类的元信息

一般称为InstanceKlass对象,在类的加载阶段完成

运行时常量池

字节码文件中通过编号查表找到常量,称为静态常量池。常量池加载到内存中,通过内存地址快速定位,叫做运行时常量池

  • Jdk7之前方法区存放在堆中的永久代

  • Jdk8之后方法区放在元空间,位于直接内存,使用-XX:MaxMetaspaceSize=256m进行限制

字符串常量池

image-20231026234757527

Jdk7之后的版本中,静态变量存放在堆中的Class对象中

直接内存

为了解决以下问题:

  • java堆中你的对象不使用会被回收,回收会影响对象的使用和创建。

  • IO操作中需要先把文件读入直接内存在复制到Java堆 。现在直接放入直接内存,java堆维护直接内存的引用。

image-20231027000601529

直接创建内存上的数据:ByteBuffer.allocateDirect(size)

手动调整:-XX:MaxDirectMemorySize=N

自动垃圾回收

线程不共享的区域,都是随着线程的创建而创建,线程的销毁而销毁。方法的栈帧在执行完方法后就会自动弹出栈并释放掉对应内存

方法区的回收

方法区中回收的是不再使用的类

判定一个类可以被回收,需要同时满足:

  • 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象

  • 加载该类的类加载器已经被回收

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

手动触发垃圾回收:System.gc()

堆内存的回收

如果java对象被引用了,说明该对象还在使用,不允许被回收

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

引用计数法

为每个对象维护一个引用计数器,当对象被引用时加1,取消引用时减1

缺点:

  1. 维护计数器对系统性能有一定影响

  2. 循环引用时会出现对象无法回收问题

查看垃圾回收日志:-verbose:gc

image-20231027233850638

可达性分析法

java中使用的是此方法

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

image-20231027235134703

那些对象称之为GC Root对象:

  • 线程Thread对象,引用线程栈帧中的方法参数,局部变量等

    每一个线程对象维护一个栈内存

    image-20231027235553064

  • 系统类加载器加载的java.lang.Class对象

    image-20231027235802095

  • 监视器对象,用来保存同步锁synchronized关键字持有的对象

    image-20231027235858687

  • 本地方法调用时使用的全局对象

查看GC Root:

  1. 使用arthas的heapdump命令将堆内存快照保存到本地磁盘中

  2. 使用MAT工具打开堆内存快照

  3. 选择GC Roots功能查看所有的GC Root

五种对象引用

强引用

GC Root对象对普通对象的引用关系

软引用

一个对象只有软引用时就会被回收。JDK1.2后提供了SoftReference类实现,软引用用于缓存中。

垃圾回收如果任然不能解决内存不足,就会回收软引用中的对象

image-20231028210951360

Caffeine框架:

Caffeine.newBuilder.softValues().build()

SoftReference对象如果需要被回收,如何确定被回收的是哪些呢:

image-20231028215134426

弱引用

类似软引用,但不管内存是否不足,都会被回收

WeakReference,主要在ThreadLocal中使用

虚引用

唯一用途:当对象被垃圾回收时可以接收对应的通知。

PhantomReference

直接内存和堆内存的关联

终结器引用

finalize方法

强引用自救

垃圾回收算法

如何回收:

  1. 找到内存中存活的对象

  2. 释放不再存活对象的内存,使得程序能再次利用这部分空间

历史和分类:

image-20231101221429913

垃圾回收启用单独的GC线程,称之为Stop The World,会停止所有用户线程,如果STW时间过长则会影响用户的使用

评价标准:

  1. 吞吐量

    吞吐量=执行用户代码的时间/(执行用户代码的时间+GC时间)

    吞吐量越高,效率越高

  2. 最大暂停时间

    STW时间最大值

  3. 堆使用效率

    使用完整的堆内存效率大于使用一半的堆内存

上述标准不可兼得

堆内存越大,最大暂停时间越长。减少最大暂停时间,就会降低吞吐量

标记清除算法
  1. 标记所有存活对象。使用可达性分析算法,从GC Root开始通过引用链标记

  2. 删除没有被标记的对象

优点:实现简单

缺点:

  1. 内存碎片化

  2. 分配速度慢

复制算法
  1. From、To空间,每次分配只能使用其中一块(From)

  2. GC阶段,将From存活对象复制到To

  3. 将两空间名字互换

image-20231101223349394

优缺点:吞吐量高、不会发生碎片化;内存使用效率低

标记整理算法/标记压缩算法

对标记清理算法中容易产生内粗碎片问题的一种解决方法

标记->整理(将存活对象移动到堆的另一端,清理掉存活对象的内存空间)

优缺点:内存使用效率高,不会发生碎片;整理阶段效率不高

分代GC算法

现代最优秀的垃圾回收算法

将内存区划为年轻代和老年代

image-20231101224642418

JDK8:-XX:+UseSerialGC 使用分代垃圾回收器

image-20231101225325992

老年代=堆大小-新生代大小

分代回收时,创建出来的对象首先会被放入Eden区,满了后会触发Minor GC或Young GC

根据可达性分析法把Eden中和From中需要回收的对象回收,把没有回收的放入To,接下来互换To,From,如果Eden满了,会继续Minor GC,此时会回收Eden和From中的对象,把存活的对象放入To

每次Minor GC后对象年龄+1,年龄到达阈值(最大15),对象就会晋升到老年代

老年代空间不足,先尝试Minor GC,如果还是不足就会Full GC

Full GC仍然无法回收掉,对象继续放入老年代时就会OOM

垃圾回收器

image-20231101235437159

垃圾回收器是垃圾回收算法的具体实现

image-20231101235915385

Serial垃圾回收器

单线程串行年轻代垃圾回收器

image-20231102000203202

image-20231102000217785

SerialOld垃圾回收器

如上,老年代版本

-XX:+UserSerialGC 新生代老年代都用串行回收器

image-20231102000427066

ParNew垃圾回收器

Serial在多CPU下的优化

-XX:+UseParNewGC

image-20231102000830511

image-20231102000900798

CMS垃圾回收器(Councurrent Mark Sweep)

允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间

image-20231102001108268

XX:+UseConcMarkSweepGC

image-20231102001406161

image-20231102001542100

image-20231102001647518

Parallel Scavenge垃圾回收器

PS是JDK8默认的年轻代垃圾回收器,多线程并行,具备自动调整堆内存大小的特点,关注吞吐量

image-20231102002044628

image-20231102002055383

PS允许手动设置最大暂停时间和吞吐量,官方建议使用这个组合时不要设置堆内存最大值

image-20231103000908525

Parallel Old垃圾回收器

PS的老年代版本

-XX:+UseParallelGC

-XX:+UseParallelOldGC

image-20231102002443751

-XX:PrintCommandLineFlags 启动时打印所有参数

G1垃圾回收器

JDK9之后的默认垃圾回收器(Garbage First)

优点:

  1. 支持巨大的堆空间回收,并有较高吞吐量

  2. 支持多CPU并行垃圾回收

  3. 允许用户设置最大暂停时间

G1内存结构:

  • 整个堆化为大小相等的区域,称之为Region,区域不要求是连续的。分为Eden、Survivor、Old区

  • 大小通过堆空间大小/2048

  • 也可以通过-XX:G1HeapRegionSize=32M指定

  • Region必须是2的指数幂,去值范围从1M到32M

image-20231105225600477

两种回收方式:

  • Young GC

  • Mixed GC

Young GC执行流程:

  1. 年轻代区不足(max默认60%),无法分配对象时需要执行回收

  2. 标记出Eden和Survivor区域中存活的对象

  3. 根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区(年龄+1),清空这些区域

    G1在进行Young GC的过程中会记录回收每个Eden区和Survivor区的平均耗时,作为下次回收时的参考依据,根据配置的最大暂停时间计算出本次回收最多能回收多少个Region

  4. 后续Young GC时与之前相同,只不过Survivor区中存活的对象会被搬运到另一个Survivor区

  5. 当某个存活对象的年龄达到阈值(默认15),将被放入老年代

  6. 部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。如果对象过大会横跨多个Region

  7. 多次回收后,会出现很多老年代区,总堆占有率达到阈值时(-XX:InitiatingHeapOccupancyParcent默认45%)会触发Mixed GC。回收所有年轻代和部分老年代的对象以及Humongous区。采用复制算法

image-20231105230931573

Mixed GC回收流程:

  • 初始标记、并发标记、最终标记、并发清理

  • 对老年代的清理会选择存活度最低的区域进行回收,可以保证回收效率最高

  • 清理阶段采用复制算法,不会产生内存碎片

image-20231105231729928

Full GC

image-20231105231927546

-XX:+UseG1GC 开启G1

-XX:MaxGCPauseMillis=毫秒值 最大暂停时间

image-20231105232258273

垃圾回收器组合的选择:

  • 0