JVM笔记

文章目录

JVM

https://www.processon.com/view/link/5eea141cf346fb1ae56a44e7

image-20220413155647402

1.类加载子系统

1.类加载过程

一个类,一个.class文件。

1.加载:

加载成方法区的运行时数据结构

加载方式:

本地系统,网络,数据库,压缩包,加密文件。最多的场景:动态代理技术

image-20220413132145721

2.链接

验证、准备、解析

3.初始化

类构造器方法<clinit>只针对类变量静态代码块中的语句。它与类的构造器<init>是不同的。

虚拟机必须保证一个类的<clinit>方法在多线程下被同步加锁。()

2.类的加载器

1.启动类加载器

image-20220413142911871

2.扩展类加载器

识别ext目录地下的class文件。

image-20220413143717500

3.应用程序加载器(系统类加载器)

image-20220413144712247

4.获取类加载器

public static void main(String[] args) {
    try {
        //1.
        ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
        System.out.println(classLoader);
        //2.
        ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
        System.out.println(classLoader1);

        //3.
        ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
        System.out.println(classLoader2);

    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

3.双亲委派机制

image-20220413152108312

接口是由引导类加载器加载的,而接口的具体的实现类是由线程的上下文类加载器加载的。

image-20220413151826629

对类的加载器的引用

image-20220413155000054

2.运行时数据区

image-20220413160641412

jvm支持多线程

image-20220413165956381

1.程序计数器

每个线程都有一份。

任何时间一个线程只有一个方法在执行,也就是当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;native方法则是undefined。

image-20220413173543461

问题:

image-20220413174647376

2.虚拟机栈

出现的背景:跨平台性,不同cpu架构不同,所以不能设计为基于寄存器。

优点:跨平台,指令集小,编译器容易实现。

缺点:性能下降,需要更多的指令。

生命周期:一个线程一个虚拟机栈,生命周期与线程一样。

作用:保存方法的局部变量、部分结果,并参与方法的调用和返回。

* 存储单位:栈帧

栈是运行时单位,堆是存储的单位。

栈帧:一个栈帧是一个方法。

当前正在执行的方法:当前方法,当前栈帧,当前类。

image-20220413203938391

image-20220413203224415

1.局部变量表

保存方法的局部变量。(vs 成员变量)

存储基本数据类型,或者引用数据类型在堆空间中的地址。

slot(变量槽):最基本的存储单元。32位以内的类型(包括returnAddress类型)占用一个槽,64位的类型(long,double)占用两个槽。

image-20220413213221069

image-20220413213655855

image-20220413214251904

image-20220413220243566

各种变量的赋值

按类型:①基本数据类型;②引用数据类型

按位置:

  • 成员变量:(在使用前,都经历过默认初始化赋值)

    • 类变量:

      linking的prepare阶段:默认赋值 --> initial阶段:显示赋值(静态代码块中的赋值)。

    • 实例变量:

      随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值。

  • 局部变量:(在使用前必须进行显示赋值!否则,编译不通过)

在java方法执行时,虚拟机使用局部变量表来完成方法的传递。

局部变量表中的变量也是重要的垃圾回收根结点,只要被局部变量表中直接或间接引用的对象都不会被回收。

2.操作数栈(表达式栈)

operand

栈:可以用数组或链表来实现。

在每一个方法刚开始执行的时候,一个空的操作数栈就随之被创建出来。

代码追踪

image-20220414172133525

image-20220414172453868

如果被调用方法有返回值,该返回值会被压入到当前栈帧中的操作数栈中。

3.动态链接(指向运行时常量池的方法引用)

作用:将class文件常量池中的符号引用转换为调用方法的直接引用。

image-20220414185304880

常量池的作用:提供一些符号和常量,便于指令的识别

静态链接和动态链接

image-20220414191147645

image-20220414191452410

动态类型语言和静态类型语言

image-20220414201739686

方法重写的本质

image-20220414203316883

虚方法

image-20220414215913697

image-20220414220038686

image-20220414203623204

4.方法返回地址

存放PC寄存器的值。

3.本地方法栈

本地方法是C语言实现的。

本地方法栈管理本地方法的调用。具体做法是Native Method Stack中登记native方法,在执行引擎执行时加载。

本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。甚至可以直接使用本地处理器中的寄存器。直接从本地内存中的堆中分配任意数量的内存。

image-20220415101542457

4.堆

java虚拟机规范规定:堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

所有的线程共享java堆,但还可以划分线程私有的缓冲区(TLAB)

“几乎”所有的对象实例都在这里分配内存。

image-20220415125546088

image-20220415133816014

image-20220415133656838

对象提升(promotion)规则

image-20220415182106460

TLAB

image-20220415183537754

image-20220415183712120

image-20220415184006633

/**
 * 测试堆空间常用的jvm参数:
 * -XX:+PrintFlagsInitial : 查看所有的参数的默认初始值
 * -XX:+PrintFlagsFinal  :查看所有的参数的最终值(可能会存在修改,不再是初始值)
 *      具体查看某个参数的指令: jps:查看当前运行中的进程
 *                             jinfo -flag SurvivorRatio 进程id
 *
 * -Xms:初始堆空间内存 (默认为物理内存的1/64)
 * -Xmx:最大堆空间内存(默认为物理内存的1/4)
 * -Xmn:设置新生代的大小。(初始值及最大值)
 * -XX:NewRatio:配置新生代与老年代在堆结构的占比
 * -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
 * -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄
 * -XX:+PrintGCDetails:输出详细的GC处理日志
 * 打印gc简要信息:① -XX:+PrintGC   ② -verbose:gc
 * -XX:HandlePromotionFailure:是否设置空间分配担保
 */

image-20220415215922217

逃逸分析、栈上分配、同步省略、标量替换

同步省略:不会体现在字节码中,在加载到内存后才会发生。

image-20220416125454693

标量替换

image-20220416130143368

5.方法区

堆、栈、方法区的交互关系

image-20220416132010415

image-20220416140828678

元空间与永久代的区别

  • 最大的区别是,元空间不在虚拟机设置的内存中,而是使用本地内存。更不容易出现oom。

  • 存储结构也发生了一定的变化。

存储的内容

方法区用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

image-20220416142712017

image-20220416142739125

image-20220416142825761

class文件中常量池的理解

constant pool table

image-20220416144939614

image-20220416145212862

运行时常量池的理解

runtime constant pool

字节码文件中的常量池加载到方法区后,就称为运行时常量池。image-20220416145726006

方法区演进细节

image-20220416152731201

image-20220416155631667

jdk8及之后,类型信息、字段、方法、常量保存在本地内存的元空间。但字符串常量池,静态变量仍在堆。

为什么要用元空间替换方法区?

image-20220416160316078

image-20220416160730246

image-20220416162705248

6.对象的内存布局

对象的实例化

创建对象的方式

image-20220416192829789

创建对象的步骤

image-20220416193637902

对象的内存布局

image-20220416214852613

image-20220416215451436

对象访问定位

image-20220416215846004

句柄访问

image-20220416215908017

直接指针(HotSpot访问)

image-20220416220247309

3.直接内存

不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域;

直接内存是在java堆外的,直接向系统申请的内存区间;

来源于NIO,通过存在堆中的DirectByteBuffer操作Native内存;

通常,访问直接内存的速度会优于Java堆,即读写性能高;

也可能导致OOM异常;

缺点:

分配回收成本较高;

不受JVM内存回收管理

4.执行引擎

image-20220417103114335

执行引擎的工作过程

image-20220417104042031

image-20220417104406595

什么是解释器,什么是JIT编译器?

image-20220417104606063

机器语言,指令集,汇编语言

机器语言:0101

指令集:MOV, INC等指令,构成指令集。使用硬件实现对应的指令操作。

汇编语言:使用助记符(Mnemonics)代替指令操作码,用地址符号(Symbo1)或标号(Labe1)代替指令或操作数的地址。

【CPU只认指令码,汇编语言也需要翻译成机器指令码才能被计算机识别和执行】

高级语言:C,C++,java,等

image-20220417110233553

JIT即时编译

image-20220417121425951

image-20220417121139435

image-20220417122721328

image-20220417123402752

image-20220417124216579

image-20220417131534867

image-20220417133121711

5.StringTable

// jdk1.8
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char[] value;
}

// jdk1.9
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    @Stable
    private final byte[] value;
}

image-20220417135855483

字符串拼接操作

image-20220417144614522

image-20220417145239562

image-20220417145642184

new String(“a”) + new String(“b”)创建了几个对象?

  • StringBuilder.toString()没有在字符串常量池中存放字面量。
 /** 题目:
 * new String("ab")会创建几个对象?看字节码,就知道是两个。
 *     一个对象是:new关键字在堆空间创建的
 *     另一个对象是:字符串常量池中的对象"ab"。 字节码指令:ldc
 *
 *
 * 思考:
 * new String("a") + new String("b")呢?
 *  对象1:new StringBuilder()
 *  对象2: new String("a")
 *  对象3: 常量池中的"a"
 *  对象4: new String("b")
 *  对象5: 常量池中的"b"
 *
 *  深入剖析: StringBuilder的toString():
 *      对象6 :new String("ab")
 *       强调一下,toString()的调用,在字符串常量池中,没有生成"ab"
 */
  • 在jdk8中,字符串常量池和对象都放在堆中,为了节省空间。当运行s3.intern()时,字符串常量池中直接存放了一个指向堆空间中String(“11”)对象的地址引用
public class StringIntern {
    public static void main(String[] args) {

        String s = new String("1");
        s.intern();//调用此方法之前,字符串常量池中已经存在了"1"
        String s2 = "1";
        System.out.println(s == s2);//jdk6:false   jdk7/8:false


        String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11")
        //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?
        // 答案:不存在!!
        s3.intern();//在字符串常量池中生成"11"。
        // 如何理解:jdk6:创建了一个新的对象"11",也就有新的地址。
        //         jdk7:此时常量中并没有创建"11",而是创建一个指向堆空间中new String("11")的地址
        String s4 = "11";//s4变量记录的地址:使用的是上一行代码代码执行时,在常量池中生成的"11"的地址
        System.out.println(s3 == s4);//jdk6:false  jdk7/8:true
    }
    
}
  • 新创建一个字符串时,先在字符串常量池中寻找是否存在该字面量或者该字面量的引用。
public class StringIntern1 {
    public static void main(String[] args) {
        //StringIntern.java中练习的拓展:
        String s3 = new String("1") + new String("1");//new String("11")
        //执行完上一行代码以后,字符串常量池中,是否存在"11"呢?答案:不存在!!
        String s4 = "11";//在字符串常量池中生成对象"11"
        String s5 = s3.intern();
        System.out.println(s3 == s4);//false  s3是对象应用,s4是字面量引用
        System.out.println(s5 == s4);//true  s5,s4都是字面量引用
    }
}

例1:

public class StringExer1 {
    public static void main(String[] args) {
        // String x = "ab";
        String s = new String("a") + new String("b");//new String("ab")
        //在上一行代码执行完以后,字符串常量池中并没有"ab"

        String s2 = s.intern();//jdk6中:在串池中创建一个字符串"ab"
                               //jdk8中:串池中没有创建字符串"ab",而是创建一个引用,指向new String("ab"),将此引用返回

        System.out.println(s2 == "ab");//jdk6:true  jdk8:true
        System.out.println(s == "ab");//jdk6:false  jdk8:true
    }
}

例2:

public class StringExer2 {
    public static void main(String[] args) {
        String s1 = new String("ab");//执行完以后,会在字符串常量池中会生成"ab"
//        String s1 = new String("a") + new String("b");执行完以后,不会在字符串常量池中会生成"ab"
        s1.intern();
        String s2 = "ab";
        System.out.println(s1 == s2); // jdk8: false;
    }
}

总结:jdk8中,intern()方法总会往字符串常量池中放一个东西。字面量or引用,然后返回放的这个东西。

image-20220417164510206

6.垃圾回收

标记阶段与清除阶段。

标记阶段

引用计数算法和可达性分析算法。

  • 引用计数算法

优点:实现简单,判定效率高

缺点: 增加计数器存储开销。计数器加减时间开销,无法处理循环引用的问题

  • 可达性分析算法

image-20220417192326838

finalization

三个状态:可触及,可复活,不可触及。

一个对象完成垃圾收集要经过2个标记过程。

image-20220418133709586

GC roots

image-20220417192554858

image-20220417192640607

清除阶段

  • 标记-清除算法(Mark-Sweep)

1.标记可达对象;

2.对堆内存中所有对象进行线性遍历,清除没有标记到的对象。

缺点:

效率不高,需要遍历整个堆空间;需要停止用户线程;产生内存碎片,需要维护空闲列表。【清除:将地址空间放在空闲列表中】

  • 复制算法(Copying)

1.内存空间分成两份;

2.将存活对象复制到另一半空间中。

优点:

没有标记和清除过程,高效;不产生内存碎片。

缺点:

需要两倍的内存空间,内存利用率低;对于G1这种分成大量region的垃圾回收器,维护对象间的引用关系成本;使用场景在存活对象数量少(死亡对象多)的情况下适用。

  • 标记-压缩算法(标记整理算法)(Mark-Compact)

1.标记可达对象。

2.整理内存空间,移动对象,清除不可达的对象。

优点:

不产生内存碎片;只需要维护内存可用空间起始地址,不需要维护空闲列表。

缺点:

效率相对较低。

  • 三种算法对比,各有优势。

image-20220417214624613

分代收集算法

针对不同生命周期的对象采用不同的收集方式,以提高回收效率。

新生代:生命周期短,存活率低,回收频繁。

老年代:区域大,对象生命周期长,回收频率不高。

增量收集算法

让垃圾收集线程和应用程序交替执行。每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

缺点:

由于在垃圾回收过程中,间断性的执行了应用程序代码,所以能减少系统的停顿时间。但是因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

相关知识点

1.System.gc()

通过System.gc()或者Runtime.getRuntime().gc(),会显式触发Full GC。同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。

【提醒jvm的垃圾回收器执行gc,但是不能确保一定会马上执行。可以通过调用System.runFinalization()方法,强制执行】

2.内存溢出与内存泄漏

内存溢出:没有空闲内存,并且垃圾收集器也无法提供更多内存。

内存泄漏:严格来说,对象不会再被程序用到,但是GC又回收不了,最终会导致oom;宽泛意义上,由于大量的不必要的生命周期过长的对象导致的oom,也可以叫memory leak。

3.STW

在GC过程中,产生的用户线程停顿,stw会使所有用户线程被暂停,没有任何响应。

why?

可达性分析算法需要在一个能确保一致性的快照中进行,否则如果出现分析过程中对象引用关系还在不断变化,则分析的准确性无法保证。

4.并行与并发

操作系统层面

并发:一个时间段内,才有多个进程同时运行。【在一个cpu时间片切换】

image-20220418130932693

并行:每个时间点,都有多个进程同时运行。【多个cpu核心,进程互不影响】

image-20220418131141063

image-20220418131513536

垃圾回收器层面

image-20220418131850448

image-20220418131831135

5.安全点与安全区域

GC发生在安全点上,用户线程采用主动式中断到达安全点。

安全区域:指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。离开safe region时,需要检查jvm是否已经完成gc。

6.引用(强,软,弱,虚)

StrongRef:只要强引用关系存在,对象就不会被回收。

SoftRef:将要发生oom之前,将这些对象进行回收。回收后内存仍然不够,报oom。

WeakRef:对象只能存活到下一次垃圾回收时。

PhantomRef:不会会对象生存时间产生影响,也不能获得实例。唯一目的:在对象

image-20220418134707086

image-20220418135118603

image-20220418135653188

垃圾回收器

1.GC评估性能指标

image-20220418140736593

image-20220418140933085

吞吐量和运行时间是矛盾的。

image-20220418141611223

image-20220418141808406

2.CMS

Concurrent-Mark-Sweep,采用标记-清除算法。

低延迟。

第一款真正意义上的并发收集器,实现了用户线程和垃圾回收线程的同时工作。

image-20220418155957610

image-20220418160342025

image-20220418160220669

image-20220418161630854

优点:

由于并发标记和并发清理两个耗时较长的过程都能够与用户线程并发执行,不会stw。所以整个回收过程是低停顿的。

缺点:

当堆内存使用率达到一定阈值的时候就要开始回收,因为在垃圾回收过程中,需要保证用户线程还有足够的使用空间。若无法满足此条件,会出现“Concurrent Mode Failure”失败,这时会启动后备预案,使用Serial Old收集器来进行老年代回收,导致停顿时间很长。

会产生内存碎片。

image-20220418161257530

JDK14已被完全抛弃。

3.G1

Garbage First。全功能收集器。

为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量。

官方目标:在延迟可控的情况下获得尽可能高的吞吐量。

名字:后台维护一个优先列表,每次根据收集时间,优先回收价值最大的Region。

image-20220418164225526

image-20220418163730418

image-20220418164258439

image-20220418164130828

JDK9成为默认的垃圾回收器。

image-20220418165158311

image-20220418165347920

image-20220418165632551

image-20220418165906213

记忆集

image-20220418184353146

G1详细的回收过程

image-20220418184732152

image-20220418192030806

image-20220418193424841

image-20220418193512270

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值