面经:JVM学习笔记

文章目录

一、Java内存区域详解

1. 运行时数据区

  • JKD1.8之前
    在这里插入图片描述
  • JDK1.8之后
    在这里插入图片描述

2. 程序计数器 (线程私有)

  • 作用
    字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理
    多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了
  • 是否会出现OutOfMemoryError
    是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

3. Java虚拟机栈 (线程私有)

  • 生命周期
    Java 虚拟机栈(后文简称栈)也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。
  • 作用
    方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
  • 栈的构成
    栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。
    在这里插入图片描述
    局部变量表:主要存放了编译期可知的各种数据类型、对象引用
    操作数栈:用于存放方法执行过程中产生的中间计算结果
    动态链接:主要服务一个方法需要调用其他方法的场景,将符号引用转换为调用方法的直接引用
  • 栈的两种错误
  • StackOverFlowError 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时
  • OutOfMemoryError 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间
  • 补充
  • 垃圾回收是否涉及栈内存?
    答:不会,栈帧内存在每次方法结束后都会弹出栈,自动回收掉
  • 栈内存分配越大越好吗?
    答:不对,因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数目就会越少。
  • 方法内的局部变量是否线程安全?
    如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
    如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

4. 本地方法栈 (线程私有)

虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

5. 堆(线程共享)

  • 生命周期及作用
    在虚拟机启动时创建
    此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
    线程共享、考虑线程安全、有垃圾回收机制
  • 堆内存的划分
    在这里插入图片描述
    (1) JDK7版本之前
    新生代内存(Young Generation)、老生代(Old Generation)、永久代(Permanent Generation)
    下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。
    (2) JDK 8 版本之后 PermGen(永久) 已被 Metaspace(元空间) 取代,元空间使用的是直接内存
  • OutOfMemoryError错误
    java.lang.OutOfMemoryError: GC Overhead Limit Exceeded : 当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
    java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。

6. 方法区(线程共享)

方法区属于是 JVM 运行时数据区域的一块逻辑区域是各个线程共享的内存区域。到底要如何实现那就是虚拟机自己要考虑的事情了。也就是说,在不同的虚拟机实现上,方法区的实现是不同的。

  • 作用
    当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
    静态变量放在方法区,成员变量放在堆,局部变量放在栈。
    运行时常量池是方法区的一部分。
    方法区:类加载.class文件的相关信息
  • 方法区与永久代和元空间的关系
    在这里插入图片描述
    方法区和永久代以及元空间的关系很像 Java 中接口和类的关系,类实现了接口,这里的类就可以看作是永久代和元空间,接口可以看作是方法区,也就是说永久代以及元空间是 HotSpot 虚拟机对虚拟机规范中方法区的两种实现方式。并且,永久代是 JDK 1.8 之前的方法区实现,JDK 1.8 及以后方法区的实现变成了元空间。

7. 运行时常量池

常量池,就是一张表虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量
等信息
运行时常量池,常量池是 *.class 文件中的,当该类被加载,它的常量池信息就会放入运行时常量
池,并把里面的符号地址变为真实地址
比较抽象可以看黑马视频,很清晰
常量池-黑马

8. 字符串常量池(String Table)

  • 作用
    是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。本质是一个hashset,保存的是字符串对象的引用,字符串对象的引用指向堆中的字符串对象。
  • JDK1.7为什么要将字符串常量池移动到堆中
    JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。
    主要是因为永久代(方法区实现)的 GC 回收效率太低,只有在整堆收集 (Full GC)的时候才会被执行 GC。Java 程序中通常会有大量的被创建的字符串等待回收,将字符串常量池放到堆中,能够更高效及时地回收字符串内存。
  • 字符串拼接题目
    黑马
//s1 s2 s3都会被加载到字符串常量池里面去
//StringTable[a,b,ab] 
//s4是new出来的对象 放在堆里面
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;//拼接的原理是new stringbulider().append("a").append("b").toString() new String("ab")
String s5 = "a" + "b";//编译期间已确定是ab 直接在字符串常量池中找
String s6 = s4.intern();//主动将串池中还没有的字符串对象放入串池,1.8以后将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//true
System.out.println(s3 == s6);//true
String x2 = new String("c") + new String("d");//堆cd
String x1 = "cd";//字符串池cd
x2.intern(); 
// 问,如果调换了【最后两行代码】的位置呢
System.out.println(x1 == x2);//false
//调换后:true
  • 特性总结
    常量池中的字符串仅是符号,第一次用到时才变为对象
    利用串池的机制,来避免重复创建字符串对象
    字符串变量拼接的原理是 StringBuilder (1.8)
    字符串常量拼接的原理是编译期优化
    可以使用 intern 方法,主动将串池中还没有的字符串对象放入串池,1.8 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回

9. 直接内存

不是虚拟机的,是系统的内存
JDK1.8元空间使用直接内存

  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

10. 元空间和永久代

元空间在jdk7及以前是属于永久代的,元空间和永久代都是用来存储class相关信息,包括class对象的Method,Field等,元空间和永久代其实都是方法区的实现,只是实现有所不同,所以说方法区其实只是一种JVM的规范。
元空间与永久代区别是其内存空间直接使用的是本地内存,而metaspace没有了字符串常量池,而在jdk7的时候已经被移动到了堆中,MetaSpace其他存储的东西,包括类文件,在JAVA虚拟机运行时的数据结构,以及class相关的内容,如Method,Field道理上都与永久代一样,只是划分上更趋于合理,比如说类及相关的元数据的生命周期与类加载器一致,每个加载器就是我们常说的classloader,都会分配一个单独的存储空间。

11. 堆和栈的区别

堆:线程共享,存放对象实例,存储的单位(解决数据存储的问题)
栈:线程独享,存放基本数据类型和对象的引用,运行时的单位

二、Hotspot虚拟机对象

1. 对象创建整体流程

在这里插入图片描述

  • 类加载检查
    虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
  • 分配内存
    在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来
    分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
  • 初始化零值
    内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
  • 设置对象头
    初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
  • 执行init方法
    执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来

2.内存分配的两种方式

  • 指针碰撞
    适用场合 :堆内存规整(即没有内存碎片)的情况下。
    原理 :用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
    使用该分配方式的 GC 收集器:Serial, ParNew
  • 空闲列表
    适用场合 : 堆内存不规整的情况下。
    原理 :虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
    使用该分配方式的 GC 收集器:CMS

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的

3.内存分配并发问题

  • CAS+失败重试
    CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB Thread local allocation buffer线程本地分配缓存区
    为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

4. 对象的内存布局

  • 对象头
    存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等)
    类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
  • 实例数据
    对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容
  • 对齐填充
    不是必然存在的,也没有什么特别的含义,仅仅起占位作用。

5. 对象的访问定位

  • 句柄
    如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
    在这里插入图片描述
  • 直接指针
    如果使用直接指针访问,reference 中存储的直接就是对象的地址
  • 对比
    这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。

6. 对象的大小

在这里插入图片描述
任何对象大小都大于8byte

7.new一个对象

在这里插入图片描述

三、JVM垃圾回收详解

1. 内存分配和回收原则

1.1 三种收集器的内存分配策略

对象优先在Eden区分配
大对象直接进入老年代(大对象指需要大量连续内存的Java对象)
长期存活的对象进入老年代

1.2 空间分配担保

确保在 Minor GC 之前老年代本身还有容纳新生代所有对象的剩余空间。
就是怕Minor GC后进入老年代的对象太多了,老年代没有那么大空间,先提前检查一下,如果检查结果显示老年代确定装不下,那么这次Minor GC就得改成Full GC,如果Full GC完了老年代空间,OOM内存溢出。

2. Minor GC与Full GC对比

2.1 定义

Minor GC (新生代GC):发生在新生代的垃圾收集动作,发生频繁,速度快
Full GC/Major GC(老年代GC):发生在老年代,速度慢

2.2 什么时候触发

Minor GC:在新生代的Eden区满了会触发
Full GC: System.gc()方法调用、老年代空间不足、空间分配担保失败

2.3 为什么要减少Full GC

影响性能,导致STW(Stop the world),对外的表现是卡顿
STW: 说人话就是,清理垃圾的时候,线程要停下来

3. 死亡对象判断方法

  • 引用计数法
    引用加1,没引用减1。计数为0时 表示不再使用
    弊端: 无法解决对象相互循环引用的问题
  • 可达性分析算法
    根对象:永远不会被回收
    “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
  • 哪些对象可以作为 GC Roots 呢?
    虚拟机栈(栈帧中的本地变量表)中引用的对象
    本地方法栈(Native 方法)中引用的对象
    方法区中类静态属性引用的对象
    方法区中常量引用的对象
    所有被同步锁持有的对象

4. 引用类型总结

  • 强引用
    必不可少。垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
  • 软引用
    可有可无的生活用品。发生垃圾回收的时候,如果内存不足,就会被垃圾回收器回收
    可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
  • 弱引用
    可有可无的生活用品。发生垃圾回收的时候,就会被垃圾回收器回收
    可以和一个引用队列(ReferenceQueue)联合使用
  • 虚引用
    如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。必须配合引用队列使用,

5. 如何判断一个常量是废弃常量

运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?
假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池了

6. 如何判断一个类是无用的类

方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?

  • 三个条件:
    该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
    加载该类的 ClassLoader 已经被回收。
    该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

7. 垃圾收集算法

  • 标记-清除
    首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象在这里插入图片描述
    速度较快。适合存活对象多,需要回收的对象少的场景。
    会造成
    内存碎片
  • 标记-复制
    将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
    在这里插入图片描述
  • 标记-整理
    标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存在这里插入图片描述
  • 分代收集算法
    当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法
    比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择**“标记-清除”或“标记-整理”**算法进行垃圾收集。

8. 垃圾收集器

8.0 Serial 收集器

Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。

8.1 CMS(Concurrent Mark Sweep)收集器

  • 目的
    获取最短回收停顿时间为目标,是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。 牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器
  • 过程
    在这里插入图片描述
    初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
    并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
    重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
    并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
  • 浮动垃圾
    在并发标记阶段本来可达的对象,由于用户线程的作用变得不可达了,即产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终导致这些新产生的垃圾对象没有被及时回收。
    添加链接描述
  • 优缺点
    优点:并发收集、低停顿
    缺点:对 CPU 资源敏感;无法处理浮动垃圾;它使用的回收算法标记-清除算法会导致收集结束时会有大量空间碎片产生。

8.2 G1收集器

  • 概述
    G1 可谓博采众家之长,力求到达一种完美。
  • 它吸取了增量收集优点,把整个堆划分为一个一个等大小的区域(region)。内存的回收和划分都以region为单位
  • 同时,它也吸取了 CMS 的特点,把这个垃圾回收过程分为几个阶段,分散一个垃圾回收过程;
  • 而且,G1 也认同分代垃圾回收的思想,认为不同对象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。
  • 如何理解G1名称的由来:为了达到对回收时间的可预计性,G1 在扫描了 region 以后,对其中的活跃对象的大小进行排序,首先会收集那些活跃对象小的 region,以便快速回收空间(要复制的活跃对象少了),因为活跃对象小,里面可以认为多数都是垃圾,所以这种方式被称为 Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收。
  • 特点
    并行与并发、分代收集、空间整合、可预测的停顿
  • 步骤
    初始标记、并发标记、最终标记、筛选回收
  • 三色标记法
    在这里插入图片描述

8.3 ZGC收集器

添加链接描述

  • 内存布局
    在这里插入图片描述
  • 染色指针
    在这里插入图片描述
  • 读屏障
    在这里插入图片描述
  • 内存多重映射
    在这里插入图片描述
  • GC的过程
    在这里插入图片描述
    ZGC 采用标记 - 整理算法,算法的思想是把所有存活对象移动到堆的一侧,移动完成后回收掉边界以外的对象

9. 被标记为垃圾的对象一定会被回收吗?

即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡**,至少要经历两次标记**过程。

第一次标记:如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那它将会被第一次标记;

第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。在 finalize() 方法中没有重新与引用链建立关联关系的,将被进行第二次标记。第二次标记成功的对象将真的会被回收,如果对象在 finalize() 方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。

10. 垃圾回收是从哪里开始的?

在这里插入图片描述
是真正进行程序执行地方,所以要获取哪些对象正在被使用,则需要从 Java 栈开始。同时,一个栈是与一个线程对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。
除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。这样,以栈或寄存器中的引用为起点,我们可以找到中的对象,又从这些对象找到对堆中其他对象的引用,这种引用逐步扩展,最终以 null 引用或者基本类型结束,这样就形成了一颗以 Java 栈中引用所对应的对象为根节点的一颗对象树。
栈 / 寄存器 -------- 堆

11. 对finalize方法的理解

一个对象真正死亡,至少要经历两次标记过程:如果对象不可达,那它将会被第一次标记并且进行一次筛选。筛选的条件是此对象是否需要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。

如果对象被判定为有必要执行finalize()方法,会放置在F-Queue的队列之中,并稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,原因是如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。

finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那么在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那么基本上它就真的被回收了

四、类加载

1. 类的生命周期

2. 类的加载过程

在这里插入图片描述

  • 加载
    通过全类名获取定义此类的二进制字节流
    将字节流所代表的静态存储结构转换为方法区的运行时数据结构
    在内存中生成一个代表此类的Class对象,作为方法区这些数据的访问入口

  • 验证
    文件格式验证—元数据验证-----字节码验证-------符号引用验证
    在这里插入图片描述

  • 准备
    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

  • 解析
    解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。

  • 初始化
    初始化阶段是执行初始化方法 ()方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。
    在准备阶段,变量已经赋过一次系统要求的初始值了,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器 () 方法的过程。

  • 卸载
    该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
    该类没有在其他任何地方被引用
    该类的类加载器的实例已被 GC

3. 类加载器总结

  • BootstrapClassLoader(启动类加载器)
    最顶层的加载类,由 C++实现,负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。
  • ExtensionClassLoader(扩展类加载器)
    主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
  • AppClassLoader(应用程序类加载器)
    面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。

4. 双亲委派模型

4.1 定义

如果一个类加载器收到了类加载的请求,它首先不会自己尝试去加载这个类,而是把这个请求委派给父类加载器去完成,每一层次的类加载器都是这样,所以所有的加载请求最终都应该传送到顶层的启动类加载容器,只有当父类加载器无法完成加载,子加载器才会尝试自己去加载
在这里插入图片描述

4.2 好处

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

4.3 弊端

基础类如果想要调用用户的代码,那么就会出现问题,因为第三方的类不能被启动类所加载。

4.4 如何自定义类加载器?如果不想用双亲委派模型怎么办?

  • 自定义加载器
    需要继承 ClassLoader 。如果我们不想打破双亲委派模型就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。
  • 打破双亲委派模型
    自己写一个类加载器;
    重写 loadClass() 方法
    重写 findClass() 方法

4.5 代码实现

实现双亲委派的代码都集中在 java.lang.ClassLoader 的 loadClass() 方法中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的 loadClass() 方法,若父加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载失败,抛出 ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载。

4.6 打破双亲委派模型的场景

JNDI 服务,它的代码由启动类加载器去加载,但 JNDI 的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现并部署在应用程序的 classpath 下的 JNDI 接口提供者(SPI, Service Provider Interface) 的代码,但启动类加载器不可能“认识”之些代码,该怎么办?

为了解决这个困境,Java 设计团队只好引入了一个不太优雅的设计:**线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader() 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。有了线程上下文类加载器,JNDI 服务使用这个线程上下文类加载器去加载所需要的 SPI 代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。

Java 中所有涉及 SPI 的加载动作基本上都采用这种方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。

五、JVM主要组成部分及作用

在这里插入图片描述

  • 类装载系统(类加载器)
    加载类文件到内存
  • 执行引擎
    也叫解释器,负责解释指令,交由操作系统执行
  • 本地库接口
    与其他语言交互时使用
  • 运行时数据区
    JVM的内存区域
  • 工作原理
    编译器把java代码变成字节码,类加载器把字节码加载到内存,将其放在运行时数据区的方法区内,
    而字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作系统去执行,
    因此需要特定的命令解析执行引擎,将字节码翻译成底层系统指令,再交给cpu去执行,
    而这个过程需要调用其他语言的本地库接口来实现整个程序的功能

六. JVM调优部分

1.JVM调优步骤

在这里插入图片描述

2. 调优的目标及策略

  • 目标
    停顿时间(一般由垃圾回收引起),吞吐量(用户程序运行时间占总时间之比,可以理解为系统在特定时间内的最大工作量),内存占用
  • 调优策略:内存和垃圾回收器
    选择合适的垃圾回收器
    调整内存大小(垃圾收集频率非常频繁,如果是内存太小,可适当调整内存大小)
    调整内存区域大小比率(某一个区域的GC频繁,其他都正常。)
    调整对象升老年代的年龄(老年代频繁GC,每次回收的对象很多。)
    调整大对象的标准(老年代频繁GC,每次回收的对象很多,而且单个对象的体积都比较大。)
    调整GC的触发时机(CMS,G1 经常 Full GC,程序卡顿严重。)
    调整 JVM本地内存大小(GC的次数、时间和回收的对象都正常,堆内存空间充足,但是报OOM)

3. GC日志

在这里插入图片描述

4. 虚拟机监控命令

在这里插入图片描述
第一个指令是jps 不是jsp
jstack分析死锁和CPU飙高的实战

  • jps指令
    jps是JDK提供的一个可以列出正在运行的Java虚拟机的进程信息的命令行工具
  • jstack指令
    jstack是JVM自带的Java堆栈跟踪工具,它用于打印出给定的java进程ID、core file、远程调试服务的Java堆栈信息.

5. 调优参数

在这里插入图片描述
Xmx Java Heap最大值,默认值为物理内存的1/4,最佳设值应该视物理内存大小及计算机内其他内存开销而定,建议设置为可用内存的最大值的80%;
-Xms Java Heap初始值,默认值为物理内存的1/64,Server端JVM最好将-Xms和-Xmx设为相同值;
-Xmn Java Heap Young区大小,默认值为堆内存的3/8,对于最新的G1收集器,建议不要设置此参数,而由系统自动分配和管理;
-Xss 每个线程的Stack大小。

6. JDK可视化分析工具

在这里插入图片描述

7. CPU飙高,不断FULL GC,如何排查

top命令 + jstat + jstack
详细解答

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值