JVM基础

发布于:2024-04-27 ⋅ 阅读:(27) ⋅ 点赞:(0)

JVM基础

字节码

使用javap -v <class位置> 即可查看类的字节码信息
主要包含了:类的基本信息、常量池、局部变量表、方法、字段
Classfile /E:/project_pro/study/java/demo/java/target/classes/com/tx/study/extendss/Client.class
  Last modified 2024-4-5; size 553 bytes
  MD5 checksum dc6c720e56c147e401834aaf581633d7
  Compiled from "Client.java"
//类信息
public class com.tx.study.extendss.Client
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
//常量池
Constant pool: 
   #1 = Methodref          #4.#23         // java/lang/Object."<init>":()V
   #2 = Methodref          #3.#24         // com/tx/study/extendss/Client.test:()V
 ....
{
  //方法
  public static void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
     //操作指令
    Code:
      stack=1, locals=2, args_size=0
         0: iconst_5
         1: istore_0
         2: iload_0
         3: iinc          0, 1
         6: istore_1
         7: return
      LineNumberTable:
        line 9: 0
        line 10: 2
        line 11: 7
       //局部变量表 
      LocalVariableTable:  
        Start  Length  Slot  Name   Signature
            2       6     0     i   I
            7       1     1     a   I
}
SourceFile: "Client.java"

i++与++i;

在这里插入图片描述

1、先把常量放入操作数栈
2、把操作数栈中的数据弹出,并存入局部变量表数组中索引为1的位置;
3、把局部变量表中索引为1的数据加入操作数栈中
4、把局部变量表中索引1的数据做加1操作
5、把操作数栈的数据弹出,存入局部变量表数组中索引为1的位置

在这里插入图片描述

1、先把常量放入操作数栈
2、把操作数栈中的数据弹出,并存入局部变量表数组中索引为1的位置;
3、把局部变量表中索引1的数据做加1操作
4、把局部变量表中索引为1的数据加入操作数栈中
5、把操作数栈的数据弹出,存入局部变量表数组中索引为1的位置

类的生命周期

类加载

1、加载阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。
2、类加载器在加载完类之后,java虚拟机会将字节码中的信息保存到方法区中,生成一个InstanceKlcass对象,保存类的所有信息,里面还包含是西安特定功能,比如多态的信息。
3、java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class对象。作用是在java代码中获取类的信息,以及存储静态字段的数据。
总结:将类的信息加载到内存中,java虚拟机在方法区,堆区中个分配一个对象保存内的信息,一般只使用堆中的class对象/

链接接阶段

验证:验证内容是否满足java虚拟机规范
1、文件格式验证:比如文件是否以0xCAFEBABE开头,主次版本号是否满足当前虚拟机版本要求。
2、元信息验证:例如类必须有父类(super不能null)
3、验证程序指令的语义:比如方法内的指令执行中跳转到不正确的位置,如goto跳转的位置必须有效。
4、符号引用验证:例如是否访问其他类中的private方法等
准备:准备阶段为静态变量分配内存并设置初始值。
1、如果在类中定义 static int value=1,那么在这阶段,value的值为0;
2、如果使用final修饰符一个基本数据类型的静态变量,准备阶段会将代码中的值进行赋值,如 public static final int value=1;,那么准备阶段,value的值为1
解析:将常量池中的符号引用替换成内存的直接引用(符号引用变为内存地址的引用)

初始化

初始化阶段会执行静态代码块中的代码,并为静态变量赋值
初始化阶段会执行字节码中的cinit部分的字节码指令,clinit方法中执行的顺序与java中编写的顺序一致
触发类的初始化:
    1、访问一个类的静态变量或则方法,注意变量是final修饰的且等号右边是常量不会触发初始化
    2、调用Class.forName(String className)
    3、new 一个该类的对象时
    4、执行Main方法的当前类
cinit不生效:
    1、无静态代码块,且无静态变量赋值语句
    2、有静态变量的声明,但是没有赋值语句
    3、静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。
有继承关系的类的初始化过程:
    1、直接访问父类的静态变量,不会触发子类的初始化
    2、子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。

总结

加载:根据类的全限定名把字节码文件的内容加载并转换成合适的数据放入内存中,存放在方法区和堆上。
链接-验证:魔数(文件开头是否包含java文件指定的头数据),版本号等验证。
链接-准备:为静态变量分配内存并设置初始值.static final修饰的变量,直接指定值。
链接-解析:将常量池中的符合(编号)引用替换为直接引用(内存地址)
初始化:执行静态代码块和静态变量赋值

类加载器

Bootstrap

1、启动类加载器是由Hostpot虚拟机提供,使用C++编写的类加载器
2、默认加载java安装目录。jre/lib下的类文件,比如rt.jar;
3、自己写的类使用启动类来加载:使用-Xbootclasspath/a:jar的目录。

Extension

1、扩展类加载器是JDK中提供的,使用Java编写的类加载器
2、默认加载Java安装目录/jre/ext下的文件
3、自己写的类使用扩展类加载来加载:-Djava.ext.dir=jar包目录;这种方式会覆盖原始目录,可以用;(windows):(linux)追加原始目录

Application

加载classpath下的类文件、自己编写的类文件、第三方jar中的类文件

双亲委派机制

 1、双亲委派机制的核心是解决一个类到底由谁加载的问题。
 2、双亲委派机制指的是:当一个类加载器接收到加载类任务时,会自底向上查找是否加载过,在由顶向下进行加载。
 
总结:
1、当一个类加载器去加载某个类时,会自底向上查找是否加载过,如果加载过就是直接返回,如果一直到顶层的类加载器都没加载过,在由顶向下进行加载。
2、应用程序类加载器的父类加载器是扩展加载器,扩展加载器的父类是启动类加载器
3、双亲委派机制的好处是:避免恶意代码替换JDK中的核心类库、避免类重复加载

运行时数据区域

程序计数器

每个线程会通过程序计数器记录当前要执行的字节码值的地址,程序计数器可以控制程序指令的进行,是西安分支、跳转、异常等逻辑。

虚拟机栈

Java虚拟机采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈帧来保存。

局部变量表

1、栈帧中的局部变量表是一个数组,数组中的每一个位子称之为槽(slot),long和double类型占两个槽位,其他类型占用一个槽。
2、 局部变量表保存的内容有:实例方法的this对象,方法参数,方法体中声明的局部变量。
3、一旦某一个局部变量不在生效,当前槽位可以在次被使用
操作数栈
操作数栈是栈帧中虚拟机在执行指令过程中用来存放临时数据的的一块区域
帧数据
帧数据:动态链接(用到其他方法中的变量)
方法出口:虚拟机中下一个方法执行到的指令地址
异常表:保存了异常捕获的范围以及异常发生之后要执行的代码位置
栈内存溢出
如果栈帧的占用内存操作分配的大小,就会出现内存溢出。
栈设置大小:
使用-Xss指定栈内存大小
限制:

创建出来的对象都存在于堆上。
栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量可以是西安对象在线程之间共享
堆空间有三个需要关注的值,used\total\max,used指的是当前已使用的堆内存,total是java虚拟机已经分配的可用堆内存,max是java虚拟机可以分配的最大堆内存。
1、随着堆中对象增多,当total可以使用的内存即将不足时,java虚拟机会继续分配内存给堆。
2、堆设置大小:
使用-Xmx:或-Xms设置,单位字节、KB、M、G
限制:Xmx必须大于2MB,Xms必须大于1M

方法区

主要存放类的元信息、运行时常量池、字符串常量池
1、运行时常量池:字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池,当常量池加载到内存中之后,可以通过内存快速定位到常量池中的内容,这种常量池称为运行时常量池。
2、字符串常量池:存储在代码中定义的常量字符串内容,比如“123”,这个123就会被放入字符串常量池

方法区溢出

JDK8之前,方法区存放在堆区域中的永久代空间中,堆的大小由虚拟机参数-XX:MaxPermSize=值 来控制。
JDK8之后方法区存放在元空间中,可使用-XX:MatespaceSize=值 将元空间大小进行限制

执行引擎

可达性分析算法

java中使用可达性分析算法来判断对象是否可以被回收,可达性分析算法分为两类:垃圾回收的根对象(GCRoot)和普通对象,对象与对象之间存在引用关系。

下图中A到B在到C和D,形成了一个引用链,可达性分析算法指的是如果从某个对象到GCRoot对象是可达的,对象就不可被回收。

在这里插入图片描述

判断一个对象是否是跟对象

GCRoot一共分为四大类,如果一个对象属于四个大类中的某一个大类,他就是跟对象。
1、线程Thread对象:创建一个线程之后,整个线程对象就被称为GCRoot对象,如:在 线程中创建对象,这些对象可以找到ThreadGCRott
2、系统类加载的java.lang.Class对象,引用类中的静态变量
3、监视器对象,用保存同步锁synchornized关键字持有的对象
4、本地方法调用时使用的全局对象

垃圾回收算法

标记清除算法
标记清除算法分为两个阶段:
标记阶段:将所有存活的对象进行标记。java使用可达性分析算法,从GCRoot开始通过引用链遍历出所有存活对象
清除阶段:从内存中删除没有被标记的对象(也就是非存活对象)
优点:实现简单
缺点:
 碎片化:由于内存是连续的,所以在对象被删除后,内存中会出现很多细小的可用内存单元。如果需要一个比较大的空间,很有可能这些内存单元大小无法进行分配。
 分配速度慢:由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次遍历到链表的最后才能获得合适的内存空间。
复制算法
1、准备两块空间FROM空间和TO空间,每次在对象分配阶段,只能使用其中一块空间(FROM空间)
2、在垃圾回收GC阶段,将FROM中的存活对象复制到TO区
3.将两块空间的FROM和TO名字互换。
优点:复制算法只需要遍历一次存活对象到TO区,不会发生碎片化
缺点:内存使用效率低,每次只能让一半的内存空间来创建对象使用
标记整理算法
标记阶段:将所有存活的对象进行标记。java使用可达性分析算法,从GCRoot开始通过引用链遍历出所有存活对象
整理阶段:将存活的对象移动到堆的一端,清理掉存活对象的内存空间。
优点:内存使用效率高,不会发生碎片化
缺点:整理阶段的效率不高
分带垃圾回收算法
分代垃圾回收将整个内存划分为年轻代(分为伊甸园区和两个幸存区)和老年代,新生代存放存活时间较短的对象,老年代存放存活时间较长的对象。

内存大小区域调整

在这里插入图片描述

每个幸存区计算公式:Xmn/(比例+2) 

算法:

分代回收时:
1、创建的的对象首先会被放入伊甸园区
2、随着对象在伊甸园区越来越多,如果伊甸园区满了,新对象的创建无法放入,就会触发年轻代的GC,被称为MinorGc或YoungGC,MinorGC会回收伊甸园区和Form需要回收的对象,把没有回收的对象放入To区
3、如果MinorGC后对象年龄达到阈值(默认15,默认值与垃圾回收器有关),对象就会被晋升到老年代
4、当老年代空间不足时,无法放入对象时,先尝试minorGC如果还是不足(当对象太大时会直接放入老年代,先minorGC看大对象是否可以放入年轻代),就会触发FULLGC,FULLGC会对整个堆进行垃圾回收
5、如果FullGC无法回收掉老年代对象,那么当对象继续放入老年代时,就会抛出Out of memoty异常

垃圾回收器

年轻代-Serial垃圾回收器
serial是一种单线程串行回收年轻代的垃圾回收器
回收年代和算法:年轻代、复制算法 
优点:单CPU处理器下吞吐量非常出色
缺点:多个CPU下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间等待
使用场景:java编写的客户端程序或则硬件配置有限的场景
老年代-SerialOld垃圾回收器
serialOld是Serial垃圾回收器的老年代版本,采用单线程串行回收,-XX:+useSerialGC新生代、老年代都使用串行回收器。
回收年代和算法:老年代、标记-整理算法 
优点:单CPU处理器下吞吐量非常出色
缺点:多个CPU下吞吐量不如其他垃圾回收器,堆如果偏大会让用户线程处于长时间等待
使用场景:与Serial垃圾回收器搭配使用,或者在CMS特殊情况下使用
年轻代-ParNew垃圾回收器
ParNew垃圾回收器本质上是对Serial在多线程CPU下的优化,使用多线程进行垃圾回收,-XX:+UseParNewGC新生代使用ParNew回收器,老年代使用串行回收器。
回收年代和算法:年轻代、复制算法
优点:多CPU处理器下停顿时间较短
缺点:吞吐量和停留时间不如G1,所以在JDK9之后不建议使用
使用场景:JDK8及之前的版本中,与CMS老年代垃圾回收器搭配使用
老年代-CMS(Concurrent Mark Sweep)垃圾回收器
CMS垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾线程在某些步骤中同时执行,减少了用户线程的等待时间,-XX:+UseConcMarkSweepGC
回收年代和算法:老年代、标记清除算法
优点:系统由于垃圾回收出现的停顿时间较短,用户体验好
缺点:
1、使用的标记清除算法,所以有内存碎片问题,CMS会在FullGC时进行碎片整理,可使用CMSFullGCBeforeConpaction=n调整N次FullGC之后再整理。
2、退化问题(退化为单线程),如果老年代内存不足时,会退化成serialOld单线程回收老年代。
3、浮动垃圾问题,不能做到完全回收垃圾,比如在并发清理时有的对象变成了垃圾对象,需要等到下一次才能回收。
使用场景:
	大型的互联网系统中用户请求数据量大、频率高的场景,比如订单接口、商品接口等
执行步骤:
1、初始标记:用极短的时间标记出GCRoot能直接关联的对象。
2、并发标记:标记所有的对象,用户线程不需要暂停
3、重新标记:由于并发标记阶段有些对象发生了变化,存在漏标、标错等情况,需要重新标记。
4、并发清理:清理掉死亡的对象,用户线程不需要暂停

在这里插入图片描述

Paralle
年轻代-Parallel Scavenge
ParallelScaveng是JDK8默认的年轻代垃圾回收器,多线程并行回收,关注的是吞吐量,具备自动调整堆内存大小的特点。
回收年代和算法:年轻代、复制算法
优点:吞吐量高、而且手动可控,围栏提高吞吐量,虚拟机会动态调整堆的参数
缺点:不能保证单次的停顿时间。
使用场景:后台任务、不需要与用户交互,并且容易产生大量的对象。比如大数据处理,大文件导出,
注:当前用户请求时间/总时间=吞吐量,垃圾回收时,用户线程不能操作。
老年代-ParallelOld
ParallelOld是ParallelScavenge收集器设计的老年代版本,利用多线程并发收集。
参数:-XX:+UseParallelGC或-XX:+UseParallelOldGC可以使用ParallelScavenge+ParallelOld这种组合。
回收年代和算法:老年代、标记整理算法
优点:并发收集、在CPU下效率高
缺点:暂停时间会比较长
使用场景:与ParallelScavenge配套使用
最大暂停时间:-XX:MaxGCPauseMillis=n 设置每次垃圾回收时的最大停顿毫秒数
吞吐量:-XX:GCTimeRatio=n 设置吞吐量为n(用户线程执行时间=n/n+1)
自动调整内存大小:-XX:+UseAdaptiveSizePolicy设置可以让垃圾回收器根据吞吐量和最大停顿时间自动调整内存大小,默认开启
java -XX:+PrintCommandLineFlags -version 打印虚拟机启动参数
G1垃圾回收器
简介:JDK9之后默认的垃圾回收器时G1垃圾回收器,Parallel关注的是吞吐量,允许用户设置最大暂停时间,但是会减少年轻代空间的大小。CMS关注的是暂停时间,但是吞吐量会下降。G1是将上述两种垃圾回收器的优点融合:
1、支持巨大的堆空间回收,并有较高的吞吐量
2、支持多CPU进行垃圾回收
3、允许用户设置最大暂停时间
内存结构:
G1的堆空间会被划分成多个大小相同的区域,称为Region区域不要求是连续的。分为伊甸园、幸存、老年代。Region的大小通过堆空间/2048计算得到,也可以通过参数-XX:G1HeapRegionSize=32m指定(其中32m指定region大小为32M),RegionSize必须是2的指数幂,取值范围从1M到32.

堆的内存空间
在这里插入图片描述

年轻代回收

回收伊甸园和幸存区中不用的对象,会导致STW,G1中通过参数-XX:MaxGCPauseMillis=n(默认200)设置每次垃圾回收时最大暂停时间毫秒数,G1垃圾回收器尽可能的保证暂停时间

执行流程

1、新创建的对象会被放入伊甸园区,当G1判断年轻代区不足(max默认60%),无法分配对象时会执行youngGC.
2、标记出伊甸园和幸存区中存活的对象
3、根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的幸存区中(对象年龄+1),清空这些区域
4、G1在进行youngGC过程中会记录每次回收时每个伊甸园和幸存区的耗时,以作为下次回收时的参考一句,这样就可以根据配置的最大暂停时间计算出本次回收时最多能回收多少个Region区域了。
比如-XX:MaxGCPauseMillis=n(默认200),每个Region回收耗时40ms,那么这次回收最多只能回收4给Region
5、后于youngGC时与之前相同,只不过幸存区存活对象会被搬运到另外一个幸存区中。
6、当某个存活对象年龄达到阈值(默认15),将会放入老年代
7、部分对象如果大小超过Region的一半,会直接放入老年代,比如堆内存时4G,每个Region是2m,只要有一个大对象超过1M就会被放入老年代中,如果对象过大会横跨多个Region
8、多次回收之后,会出现很多老年代,此时堆占有率达到阈值时(-XX:InitiatingGeapOccupancyPercent 默认45%)会触发混合回收(MixedGC),回收年轻代和部分老年代的对象以及大对象区,采用复制算法完成。

混合回收

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

回收年代和算法:年轻代+老年代,复制算法
优点:对比较大的堆如果超过6个G的堆回收时,延迟可控,不会产生内存碎片、并发标记的算法(SATB)效率高
垃圾回收器总结
jdk8之前:
ParNew+CMS(关注暂停时间)、Parallel(关注吞吐量)、G1(jdk8之前不建议)
jdk9之后:
 G1

JVM总结

1、Java中有那几块内存需要进行垃圾回收:

在这里插入图片描述

线程不共享不需要垃圾回收,跟着线程的生命周期随着线程回收而回收。
方法区:一把不需要回收,JSP等技术会通过回收类加载器区回收方法区中的类
堆:由垃圾回收器负责进行回收

2、常用的引用类型

强引用:最常见的引用方式,有可达性分析算法判断
软引用:对象在没有强引用的情况下,内存不足时会回收
弱引用:对象在没有强引用的情况下,直接回收
虚引用:通过虚引用知道对象被回收了
终结器引用:对象回收时需要自救,不建议使用

3、常用的垃圾算法

标记清除:标记之后在清除,容易产生内存碎片
复制:从一个区域复制到另外一个区域,容易造成只能使用一部分内存
标记整理:标记之后将存活的对象推到一百年,对象会移动,效率不高
分代GC:将整块内存区域划分为年轻代、幸幸存区、老年代回收,可使用多种回收算法

4、常见的垃圾回收器

serial和serialOld:单线程回收,主要用于单核CPU场景
ParNew和CMS:暂停时间较短,使用于大型互联网应用中与用户交互的部分
ParallelScavenge和ParallelOld:吞吐量高,适用于后台进行大量数据操作
G1:适用于较大的堆,具有可控的暂停时间

工具下载

https://arthas.gitee.io/doc/

网站公告

今日签到

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