java面经03-虚拟机篇-jvm内存结构&垃圾回收、内存溢出&类加载、引用&悲观锁&HashTable、引用&finalize

虚拟机篇

1. JVM 内存结构

要求

  • 掌握 JVM 内存结构划分
  • 尤其要知道方法区、永久代、元空间的关系

结合一段 java 代码的执行理解内存划分

在这里插入图片描述


在这里插入图片描述

  • 执行 javac 命令编译源代码为字节码
  • 执行 java 命令
    1. 创建 JVM,调用类加载子系统加载 class,将类的信息存入方法区
    2. 创建 main 线程,使用的内存区域是 JVM 虚拟机栈,开始执行 main 方法代码
    3. 如果遇到了未见过的类,会继续触发类加载过程,同样会存入方法区
    4. 需要创建对象,会使用内存来存储对象
    5. 不再使用的对象,会由垃圾回收器在内存不足时回收其内存
    6. 调用方法时,方法内的局部变量、方法参数所使用的是 JVM 虚拟机栈中的栈帧内存
    7. 调用方法时,先要到方法区获得到该方法的字节码指令,由解释器将字节码指令解释为机器码执行
    8. 调用方法时,会将要执行的指令行号读到程序计数器,这样当发生了线程切换,恢复时就可以从中断的位置继续
    9. 对于非 java 实现的方法调用,使用内存称为本地方法栈(见说明)
    10. 对于热点方法调用,或者频繁的循环代码,由 JIT 即时编译器将这些代码编译成机器码缓存,提高执行性能 (否则每次执行相同的代码,都要解释器重复地将字节码指令解释为机器码执行,相当于对字节码指令做了缓存)

方法区:存放类的相关信息(类的名称、继承关系、引用的其他类的符号、成员变量、方法的字节码、类和方法和成员变量上加的注解等等)
堆: 存放new出来的对象
JVM 虚拟机栈:存放方法内的局部变量和方法参数 java实现的普通方法变量都存在这里,以前需要和os交互的特殊方法需要到本地方法栈去执行,但是现在Oracle公司的 Hotspot 虚拟机实现已经不再使用本地方法栈,或者说两个栈合二为一了,所有方法需要的变量内存都在JVM 虚拟机栈中

说明

  • 加粗字体代表了 JVM 虚拟机组件
  • 对于 Oracle 的 Hotspot 虚拟机实现,不区分虚拟机栈和本地方法栈

会发生内存溢出的区域

内存溢出: 该区域内存耗尽了,报错了

内存泄漏:垃圾回收器无法回收某部分内存,这种现象就叫做内存泄漏;

上图中5块内存区域,除了程序计数器,都会产生内存溢出

  • 不会出现内存溢出的区域 – 程序计数器
  • 出现 OutOfMemoryError 的情况
    • 堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收
    • 方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类
    • 虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时
  • 出现 StackOverflowError 的区域
    • JVM 虚拟机栈,原因有方法递归调用未正确结束、反序列化 json 时循环引用 (线程内方法不断调用,而每个线程内的1M内存消耗掉,就会报StackOverflowError)

方法区、永久代、元空间

  • 方法区是 JVM 规范中定义的一块内存区域,用来存储类元数据方法字节码即时编译器需要的信息等
  • 永久代是 Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)
  • 元空间是 Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间

方法区只是 JVM 规范中的一种定义 (你得有,怎么实现我不管)
永久代和元空间才是对规范的物理实现

在这里插入图片描述

从这张图学到三点

类元数据: 描述类的数据 (哪些成员,什么类型,长度多少…) 存储在元空间(方法区的物理实现)
类名.class 字节码对象,既然是对象,自然就存储在堆中了
类的原始信息(类元数据)存储在元空间中,无法直接访问,得通过java对象访问,这个对象就是字节码对象

  • 当第一次用到某个类时,由类加载器将 class 文件的类元信息读入,并存储于元空间
  • X,Y 的类元信息是存储于元空间中,无法直接访问
  • 可以用 X.class,Y.class 间接访问类元信息,它们俩属于 java 对象 (字节码对象),我们的代码中可以使用

在这里插入图片描述

从这张图可以学到

  • 堆内存中:当一个类加载器对象,这个类加载器对象加载的所有类对象,这些类对象对应的所有实例对象都没人引用时,GC 时就会对它们占用的堆内存进行释放
  • 元空间中:内存释放以类加载器为单位,当堆中类加载器内存释放时,对应的元空间中的类元信息也会释放

一般系统类加载器不会被释放,我们自定义的类加载器不再使用时会被释放( 释放啥? 元空间内存啊 )

2. JVM 内存参数

要求

  • 熟悉常见的 JVM 参数,尤其和大小相关的

提问: 在这里插入图片描述

堆内存,按大小设置

在这里插入图片描述

解释:

  • -Xms JVM最小内存(包括新生代和老年代)
  • -Xmx JVM最大内存(包括新生代和老年代)
  • 通常建议将 -Xms 与 -Xmx 设置为大小相等,即不需要保留内存,不需要从小到大增长,这样性能较好
  • -XX:NewSize 与 -XX:MaxNewSize 设置新生代的最小与最大值,但一般不建议设置,由 JVM 自己控制
  • -Xmn 设置新生代大小,相当于同时设置了 -XX:NewSize 与 -XX:MaxNewSize 并且取值相等
  • 保留是指,一开始不会占用那么多内存,随着使用内存越来越多,会逐步使用这部分保留内存。下同

从年代角度,JVM将内存划分为新生代和老年代
-Xmn的n就是new 新生代

堆内存,按比例设置

下图的 new 就是新生代,新生代内存可以进一步划分为eden和Survivor,Survivor又可以细分为:from,to
old 自然就是老年代内存

在这里插入图片描述

解释:

  • -XX:NewRatio=2:1 表示老年代占两份,新生代占一份
  • -XX:SurvivorRatio=4:1 表示新生代分成六份,伊甸园占四份,from 和 to 各占一份
  • (注意1:默认8:1 也就是8:1:1) (注意2:上面的4:1指的是eden:from=eden:to=4:1 因为from和to总是相等的)

元空间内存设置

在这里插入图片描述

解释:

  • class space 存储类的基本信息,最大值受 -XX:CompressedClassSpaceSize 控制
  • non-class space 存储除类的基本信息以外的其它信息(如方法字节码、注解等)
  • class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制

注意:

  • 这里 -XX:CompressedClassSpaceSize 这段空间还与是否开启了指针压缩有关,这里暂不深入展开,可以简单认为指针压缩默认开启

代码缓存内存设置

JIT即时编译器,将热点代码编译成机器码后缓存起来,就存放在CodeCache 代码缓存区

在这里插入图片描述

解释:

  • 如果 -XX:ReservedCodeCacheSize < 240m,所有优化机器代码不加区分存在一起
  • 否则,分成三个区域(优化代码细分3份)(图中笔误 mthod 拼写错误,少一个 e)
    • non-nmethods - JVM 自己用的代码 (JIT编译器自己的代码)
    • profiled nmethods - 部分优化的机器码
    • non-profiled nmethods - 完全优化的机器码

线程内存设置

也就是JVM虚拟机栈的内存
-Xss 设置每个线程占用的内存
不设置,linux系统默认1MB, 也就是每个线程默认占用1MB内存

在这里插入图片描述

官方参考文档

3. JVM 垃圾回收

要求

  • 掌握垃圾回收算法
  • 掌握分代回收思想
  • 理解三色标记及漏标处理
  • 了解常见垃圾回收器

eg: 内存中一些对象,已经没有任何内存中引用指向它,GC就可以将它回收了

三种垃圾回收算法

标记清除法

在这里插入图片描述

解释:

  1. 找到 GC Root 对象,即那些一定不会被回收的对象,如正执行方法内局部变量引用的对象、静态变量引用的对象
  2. 标记阶段:沿着 GC Root 对象的引用链找,直接或间接引用到的对象加上标记
  3. 清除阶段:释放未加标记的对象占用的内存

局部变量正在引用或者说使用的对象一定不能回收,可以作为根对象
静态变量得一直存在,不能被回收,可以作为根对象

要点:

  • 标记速度与存活对象线性关系
  • 清除速度与内存大小线性关系
  • 缺点是会产生内存碎片 (未标记的内存极大概率都是不连续的,会产生大量内存碎片 所以基本上已经被弃用了)

标记整理法

在这里插入图片描述

解释:

  1. 前面的标记阶段、清理阶段与标记清除法类似
  2. 多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片产生

特点:

  • 标记速度与存活对象线性关系

  • 清除与整理速度与内存大小成线性关系

  • 缺点是性能上较慢

标记复制法

在这里插入图片描述

解释:

  1. 将整个内存分成两个大小相等的区域,from 和 to,其中 to 总是处于空闲,from 存储新创建的对象
  2. 标记阶段与前面的算法类似
  3. 在找出存活对象后,会将它们从 from 复制到 to 区域,复制的过程中自然完成了碎片整理(复制完后from区全部都可以清除了)
  4. 复制完成后,交换 from 和 to 的位置即可 (两个区域交替使用,永远不会产生内存碎片问题,多好啊)

特点:

  • 标记与复制速度与存活对象成线性关系
  • 缺点是会占用成倍的空间

GC 与分代回收算法

GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度

GC 要点:

  • 回收区域是堆内存,不包括虚拟机栈 (方法栈中内存,在方法调用结束会自动释放方法占用内存)
  • 判断无用对象,使用可达性分析算法三色标记法标记存活对象,回收未标记对象
  • GC 具体的实现称为垃圾回收器
  • GC 大都采用了分代回收思想
    • 理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收
    • 根据这两类对象的特性将回收区域分为新生代老年代新生代采用标记复制法老年代一般采用标记整理法
  • 根据 GC 的规模可以分成 Minor GCMixed GCFull GC

新生代:垃圾对象比较多 (方法内经常new的局部对象)
老年代: 存活对象比较多,很难回收,或者说不需要经常回收,整理也不会特别耗时 (eg: 静态对象,框架里面长期使用的对象) (老年代存活对象多,标记复制法也会极其浪费内存)
可达性分析算法:找到GC Root 打上标记 (先找到一定不会被回收的对象,然后沿着其引用链再找,再标记)
三色标记法:见下文
垃圾回收器有很多种,见下文
.
Minor GC:新生代的垃圾回收,小范围垃圾回收,暂停时间短,对系统影响小
Full GC: 新生代和老年代都发生内存不足了,来了一次全面的垃圾回收,暂停时间长,明显感到系统卡顿,一般是不愿意看到Full GC的
Mixed GC: 位于以上二者之间,指的是:新生代发生了垃圾回收,部分的老年代也发生了垃圾回收,一种混合垃圾回收,G1垃圾回收器独有的回收方式

个人再整理一下GC和堆内存相关概念:
GC只是回收堆内存
new出来的对象,都放在堆内存
堆内存划分:
从年代角度,JVM将堆内存划分为新生代和老年代
新生代内存又可以分为: eden和Survivor,Survivor又可以细分为:from,to
先总览一下,有个大致框架: 再慢慢看下面详细过程
在这里插入图片描述
图中黄色空闲,白色是已分配
打标记可以用一句话概括: 寻找有没有被根对象直接或者间接引用到的

分代回收

  1. 伊甸园 eden,最初对象都分配到这里,与幸存区 survivor(分成 from 和 to)合称新生代,

在这里插入图片描述

  1. 当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象

在这里插入图片描述

  1. 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放

在这里插入图片描述

  1. 将 from 和 to 交换位置

在这里插入图片描述

  1. 经过一段时间后伊甸园的内存又出现不足

在这里插入图片描述

  1. 标记伊甸园与 from(现阶段没有)的存活对象

在这里插入图片描述

  1. 将存活对象采用复制算法复制到 to 中

在这里插入图片描述

  1. 复制完毕后,伊甸园和 from 内存都得到释放

在这里插入图片描述

  1. 将 from 和 to 交换位置

在这里插入图片描述

  1. 老年代 old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升

幸存区不足: to不够复制的,肯定把已经在to的给移到老年代 (to很大的,不足肯定是有之前熬过了回收的对象存在的) 提前竞升也是没有办法的事情
大对象:每次GC都要复制来复制去的,太消耗了,不如提前竞升为老年代

GC 规模

  • Minor GC 发生在新生代的垃圾回收,暂停时间短

  • Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有

  • Full GC 新生代 + 老年代 完整(全面) 垃圾回收,暂停时间长,应尽力避免

三色标记

即用三种颜色记录对象的标记状态

  • 黑色 – 已标记
  • 灰色 – 标记中
  • 白色 – 还未标记

黑色 – 已标记: 沿着根对象的引用链,已经找到这个对象了,且此对象内部的其他引用也已经处理完成了
灰色 – 标记中:沿着根对象的引用链,已经找到这个对象了,但这个对象内部的其他引用还没有处理完
白色 – 还未标记: 就是标记完最终剩下的对象了

  1. 起始的三个对象还未处理完成,用灰色表示

在这里插入图片描述

  1. 该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色

将其直接引用标记为灰色,就认为他的引用处理完成了,就可以直接标记为黑色了

在这里插入图片描述

  1. 依次类推

在这里插入图片描述

  1. 沿着引用链都标记了一遍

在这里插入图片描述

  1. 最后未标记的白色对象,即为垃圾

在这里插入图片描述

并发漏标问题

前面的GC是非并发的,GC在工作时,用户线程就暂停了,因此用户线程不会对GC线程造成影响
也即GC在打标记时,用户线程暂停了,不会对打标记产生任何影响(不会修改引用链)
非并发GC效率低,并发GC,也即并发标记,肯定是需要的
那么GC在打标记时,用户线程还在工作,万一打标过程中,用户线程修改了引用关系,很容易导致漏标啊

比较先进的垃圾回收器都支持并发标记,即在标记过程中,用户线程仍然能工作。但这样带来一个新的问题,如果用户线程修改了对象引用,那么就存在漏标问题。例如:

  1. 如图所示标记工作尚未完成

在这里插入图片描述

  1. 用户线程同时在工作,断开了第一层 3、4 两个对象之间的引用,这时对于正在处理 3 号对象的垃圾回收线程来讲,它会将 4 号对象当做是白色垃圾

这个时候回收3其实也是合理的
但是万一他断开后又被别的对象引用了呢(不是我们不用了,而是我给别人用了) 就不能回收了呀(见下)

在这里插入图片描述

  1. 但如果其他用户线程又建立了 2、4 两个对象的引用,这时因为 2 号对象是黑色已处理对象了,因此垃圾回收线程不会察觉到这个引用关系的变化从而产生了漏标

在这里插入图片描述

  1. 如果用户线程让黑色对象引用了一个新增对象,一样会存在漏标问题

黑色对象已经处理过了(被标记为黑色的,会认为已经处理过了),已经处理过的对象,不会再去处理他的(不会再重复地找他的直接引用然后标记为灰色)

在这里插入图片描述

因此对于并发标记而言,必须解决漏标问题,也就是要记录标记过程中的变化。有两种解决方法:

解决漏标,核心就是:记录标记过程中的变化+二次处理

  1. Incremental Update 增量更新法,CMS 垃圾回收器采用
    • 思路是拦截每次赋值动作,只要赋值发生,被赋值的对象就会被记录下来,在重新标记阶段再确认一遍
  2. Snapshot At The Beginning,SATB 原始快照法,G1 垃圾回收器采用
    • 思路也是拦截每次赋值动作,不过记录的对象不同,也需要在重新标记阶段对这些对象二次处理
    • 新加对象会被记录
    • 被删除引用关系的对象也被记录

上图红箭头 黑->白 黑色对象就是被赋值对象(把白色对象赋值给黑色对象)

在这里插入图片描述

垃圾回收器 - Parallel GC

  • eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程

  • old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程

  • 注重吞吐量 (响应时间、暂停时间慢点没关系,但是总体上暂停时间短一点就ok了)

Parallel GC: 实际上由2个垃圾回收器组成,一个工作在新生代,一个工作在老年代
Minor GC 仅仅新生代垃圾回收器工作
Full GC 时,新生代和老年代垃圾回收器都会工作
标记复制和标记整理(慢)都不会有内存碎片

垃圾回收器 - ConcurrentMarkSweep GC

  • 它是工作在 old 老年代,支持并发标记的一款回收器,采用并发清除算法

    • 并发标记时不需暂停用户线程 (可能导致漏标)
    • 重新标记时仍需暂停用户线程 (处理漏标时用户线程不能再并发了,得暂停,否则没完没了了)
  • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC

  • 注重响应时间 (也就这一个好处 响应时间很快 不需要等很久)

ConcurrentMarkSweep GC 这是一个老年代垃圾回收器,
ConcurrentMarkSweep GC 简称 CMS垃圾回收器
Concurrent:并发 Mark:标记 Sweep: 扫描,打扫
并发就意味着GC时用户线程暂停时间很短,可以并发执行嘛
标记指的是标记为黑、灰、白三色,清除指的是清除回收白色垃圾对象
然而正因为人家采用的是 标记清除法,有内存碎片问题,因此最新的JDK已经将其标记为废弃了

STW(Stop The World)
在这里插入图片描述

垃圾回收器 - G1 GC

  • 响应时间与吞吐量兼顾
  • 划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
  • 分成三个阶段:新生代回收、并发标记、混合收集
  • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC

G1 GC 读作:G one 垃圾回收器
humongous: 巨大无比的

总览:
在这里插入图片描述

G1也有保底策略:回收速度<新对象创建速度 也就是并发失败 : FailBack Full GC 整体进行一次回收,暂停时间会比较长

G1 回收阶段 - 新生代回收

  1. 初始时,所有区域都处于空闲状态

在这里插入图片描述

  1. 创建了一些对象,挑出一些空闲区域作为伊甸园区存储这些对象

在这里插入图片描述

  1. 当伊甸园需要垃圾回收时,挑出一个空闲区域作为幸存区,用复制算法复制存活对象,需要暂停用户线程
    (新生代采用标记复制法,复制时要STW, 非并发的)
    (eden区所有存活对象复制到一个幸存区(to区 然后to和from区互换地位 ))

在这里插入图片描述

  1. 复制完成,将之前的伊甸园内存释放

在这里插入图片描述

  1. 随着时间流逝,伊甸园的内存又有不足

在这里插入图片描述

  1. 将伊甸园以及之前幸存区中的存活对象,采用复制算法,复制到新的幸存区,其中较老对象晋升至老年代

(eden区和幸存from区中的对象全部复制到新的幸存区(类似to))

在这里插入图片描述

  1. 释放伊甸园以及之前幸存区的内存

在这里插入图片描述

G1 回收阶段 - 并发标记与混合收集

前提,老年代内存快不足了,才需要开始回收老年代,老年代标记策略是:并发标记

  1. 当老年代占用内存超过阈值后,触发并发标记,这时无需暂停用户线程

在这里插入图片描述

也不是直接回收所有的老年代区域,而是挑选几个回收价值高的老年代区域(存活对象很少)先进行回收

  1. 并发标记之后,会有重新标记阶段解决漏标问题,此时需要暂停用户线程。这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是 Gabage First 名称的由来)。

在这里插入图片描述

混合收集,不仅收集挑选出来的回收价值高的老年代(上图红色),还收集新生代(eden+survivor)

  1. 混合收集阶段中,参与复制的有 eden、survivor、old,下图显示了伊甸园和幸存区的存活对象复制

在这里插入图片描述

  1. 下图显示了老年代和幸存区晋升的存活对象的复制

在这里插入图片描述

  1. 复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集

在这里插入图片描述

4. 内存溢出

内存溢出: 该区域内存耗尽了,报错了

要求

  • 能够说出几种典型的导致内存溢出的情况

典型情况

  • 1)误用线程池导致的内存溢出
    • 参考 day03.TestOomThreadPool
      在这里插入图片描述
      LinkedBlockingQueue就是一种无界队列 (Interger类型不溢出,他就不会溢出)

在这里插入图片描述
上图代码不断创建新的现场并提交,由于每个线程都要阻塞30ms,阻塞队列越来越大,无限制增长,就会导致内存爆

在这里插入图片描述
在这里插入图片描述

  • 2)查询数据量太大导致的内存溢出
    • 参考 day03.TestOomTooManyObject

数据库条目太多了,你findAll, 一次查可能就100w条,就是100w个很普通的Product商品POJO集合,也要占用363MB的内存, 服务器内存再大,也经不起这么造啊, 10个用户就得占用3G内存呀
所以后端开发千万不要findAll( 自己不要写,也不要调用)
以后写代码,sql查询一定要加limit (光有条件都不行,条件可能失效啊)

这些错误在测试环境下是测不出来的,生产环境下才有百万级别的数据,才会暴露出来的问题

所以项目做完后,做一下压力测试也是很有必要的,面试会问到

  • 3)动态生成类导致的内存溢出
    • 参考 day03.TestOomTooManyClass
      在这里插入图片描述
      在这里插入图片描述

5. 类加载

要求

  • 掌握类加载阶段
  • 掌握类加载器
  • 理解双亲委派机制

类加载过程的三个阶段

  1. 加载

    1. 将类的字节码载入方法区,并创建类.class 对象
    2. 如果此类的父类没有加载,先加载父类
    3. 加载是懒惰执行 (真的用到此类时才加载)

类.class对象里面有一系列反射方法,可以获知类的所有信息:有哪些成员,有哪些方法
类.class对象存放在堆里面

  1. 链接

    1. 验证 – 验证类是否符合 Class 规范,合法性、安全性检查
    2. 准备 – 为 static 变量分配空间,设置默认值 (但是手动写了赋值语句此时是不会执行的,会在初始化阶段执行,这里其实只是给静态变量分配空间 (final变量是例外,会在此时赋值))
    3. 解析 – 将常量池的符号引用解析为直接引用
  2. 初始化

    1. 静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个 <cinit> 方法,在初始化时被调用
    2. static final 修饰的基本类型变量赋值,在链接阶段就已完成
    3. 初始化是懒惰执行 (真正要用到该类时才会初始化 懒惰执行,化整为零,多好)

验证手段

  • 使用 jps 查看进程号
  • 使用 jhsdb 调试,执行命令 jhsdb.exe hsdb 打开它的图形界面
    • Class Browser 可以查看当前 jvm 中加载了哪些类
    • 控制台的 universe 命令查看堆内存范围
    • 控制台的 g1regiondetails 命令查看 region 详情
    • scanoops 起始地址 结束地址 对象类型 可以根据类型查找某个区间内的对象地址
    • 控制台的 inspect 地址 指令能够查看这个地址对应的对象详情
  • 使用 javap 命令可以查看 class 字节码

代码说明

  • day03.loader.TestLazy - 验证类的加载是懒惰的,用到时才触发类加载
  • day03.loader.TestFinal - 验证使用 final 修饰的变量不会触发类加载

字节码对象确实在堆空间(eden区域),不在方法区

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

  • 初始化方法(静态成员(非final普通类型)和静态代码块)

会将静态成员和静态代码快里的语句,整合在一起,变成一个方法(cinit方法),在类初始化时调用这个方法
注意:final static 非引用类型 的变量在类加载时(创建字节码对象时)就会初始化好的,这里不需要整合了

在这里插入图片描述

  • 使用 final 修饰的非引用类型变量不会触发类加载

前两个打印语句,看起来使用了类,Student.c和Student.m 实际上并没有真正使用到类,因此此时类并没有被加载,内存中并没有类,充分证明了类的加载是懒加载
此时类加载完成了,可以看到类的字节码信息了(类的结构:哪些成员、哪些方法)

在这里插入图片描述
当一个类A使用另一个类B的final static 普通类型 变量,实际上是常量,这个时候类A直接将该类B常量复制一份到自己类中,根本不会真的用到另一个类B

如果常量数值比较小,那么直接就写死在方法里
如果数值比较大,超过了short的最大范围(>32767) 就会放到常量池子中,需要用到时到常量池中拿就好了
也即是: 数值较大,会复制到类A自己的常量池中,每个类都有自己的常量池(一个常量列表,且1,2,3,… 地给每个常量编好了号,给出编号,直接到常量池中取那个常量的值)

在这里插入图片描述

解析:符号引用-》直接引用 随着代码的执行不断进行的过程,不是一次性就能完成的
类的static成员变量的引用,都是放在常量池的
没有给静态成员赋值时,常量池中就没有直接引用,只有符号引用(空指针 只知道要指向什么类型,但是并没有真的内存)

jdk 8 的类加载器

名称 加载哪的类 说明
Bootstrap ClassLoader JAVA_HOME/jre/lib 无法直接访问
Extension ClassLoader JAVA_HOME/jre/lib/ext 上级为 Bootstrap,显示为 null
Application ClassLoader classpath 上级为 Extension
自定义类加载器 自定义 上级为 Application

像String.class, Application和Extension类加载器中都没有,无法加载,这个时候必须向上询问Bootstrap启动类加载器,让他加载,然后下级都可见 (String类型是jdk的,上层都需要用到,所有都可见也是合理的)
像自己写的类Student.class, 也会遵循规则先逐级向上询问,上层加载器都没有这个类,Application类加载器才有了加载Student.class的资格,进行加载(上层类加载器不可见,也不需要可见,这种屏蔽很合理)

双亲委派机制

所谓的双亲委派,就是指优先委派上级类加载器进行加载,如果上级类加载器

  • 能找到这个类,由上级加载,加载后该类也对下级加载器可见
  • 找不到这个类,则下级类加载器才有资格执行加载

双亲委派的目的有两点

  1. 让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到 jdk 提供的核心类 (反之不行:jdk肯定不需要依赖你自己写的类)

  2. 让类的加载有优先次序,保证核心类优先加载

上级类加载器中的类对下级可见
但是下级类加载器中的类对上级不可见

对双亲委派的误解

下面面试题的回答是错误的

在这里插入图片描述

错在哪了?

  • 自己编写类加载器就能加载一个假冒的 java.lang.System 吗? 答案是不行。

  • 假设你自己的类加载器用了双亲委派,那么优先由启动类加载器加载真正的 java.lang.System,自然不会加载假冒的

  • 假设你自己的类加载器不用双亲委派,那么你的类加载器加载假冒的 java.lang.System 时,它需要先加载父类 java.lang.Object,而你没有用委派,找不到 java.lang.Object 所以加载会失败

  • 以上也仅仅是假设。事实上操作你就会发现,自定义类加载器加载以 java. 打头的类时,会抛安全异常,在 jdk9 以上版本这些特殊包名都与模块进行了绑定,更连编译都过不了 (实际操作,直接抛安全异常,或者编译不过,到不了假设那一步,jdk已经做了安全措施,防止你这么做了, 直接就不允许你重复写java.lang这重包名了)

代码说明

  • day03.loader.TestJdk9ClassLoader - 演示类加载器与模块的绑定关系 =》 结论:不准自己重复写jdk已经有的包名.类名

6. 四种引用

要求

  • 掌握四种引用

强引用

  1. 普通变量赋值即为强引用,如 A a = new A();

  2. 通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收

在这里插入图片描述

软引用(SoftReference)

  1. 例如:SoftReference a = new SoftReference(new A()); (中间有一个SoftReference对象做中转,a间接关联到对象new A())

  2. 如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象 (内存不足时会触发GC,第一次饶过你,第二次内存不足又触发了GC, 是会将软引用对象回收的(有强引用指向的对象GC无法回收))

  3. 软引用自身需要配合引用队列来释放(如下图,a对象是软引用,但是SoftReference自身还是强引用,GC无法回收软引用自身)

  4. 典型例子是反射数据(通过反射获取的数据都是软引用数据,如:类名.class=》获取的成员变量,方法等数据信息都是软引用)

在这里插入图片描述

弱引用(WeakReference)

  1. 例如:WeakReference a = new WeakReference(new A());

  2. 如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象

  3. 弱引用自身需要配合引用队列来释放 (同上)

  4. 典型例子是 ThreadLocalMap 中的 Entry 对象

在这里插入图片描述

虚引用(PhantomReference)

  1. 例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);

  2. 必须配合引用队列一起使用,当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理

  3. 典型例子是 Cleaner 释放 DirectByteBuffer 关联的直接内存

在这里插入图片描述

引用队列详解:如图,虚引用关联的对象a,b被释放内存后,虚引用本身会被放到引用队列里,由Reference Handler 线程专门负责回收他们,因为他们可能还关联了其他一些资源(不仅仅只是a对象和b对象)

代码说明

  • day03.reference.TestPhantomReference - 演示虚引用的基本用法
  • day03.reference.TestWeakReference - 模拟 ThreadLocalMap, 采用引用队列释放 entry 内存
String str = new String("hello"); // "hello"在堆内存中 (new出来的都在堆中)
String str = "hello"; // "hello" 在常量池中 

ThreadLocalMap 中的 Entry 对象,key是弱引用,value是强引用
在这里插入图片描述
上图就是一种典型的内存泄露
解决:使用引用队列,将Entry和某个引用队列关联上,当Entry的key被回收时,整个Entry对象会被放到引用队列里面去,然后直接将已经在引用队列中的Entry对象的Map引用去掉就行了(或者说看看当前Entry在不在Map中,在就将Map里面记录Entry的数组对应引用设置为null),没有引用指向它,下次回收时就会被回收了
jdk不是这么实现的,成本会比较高

★★★key就是ThreadLocal对象本身,线程运行时一定还被其他对象强引用,所以不怕他被设置为弱引用,线程没有结束前,key(有其他强引用)不会被释放。但是value一旦设置为弱引用,真的就只有这一个弱引用了,很可能线程还没结束,就被GC回收了。★★★

7. finalize

要求

  • 掌握 finalize 的工作原理与缺点

finalize

  • 一般回答:它是 Object 中的一个方法,如果子类重写它,垃圾回收时此方法会被调用,可以在其中进行资源释放和清理工作
  • 优秀回答:将资源释放和清理放在 finalize 方法中非常不好,非常影响性能,严重时甚至会引起 OOM(Out of Memory),从 Java9 开始就被标注为 @Deprecated,不建议被使用了

追问:为什么非常不好,非常影响性能?
见下面原理:

补:守护线程,在主线程已经结束时,守护线程就不会再执行了(即使有代码没执行完毕)

finalize 原理

  1. 对 finalize 方法进行处理的核心逻辑位于 java.lang.ref.Finalizer 类中,它包含了名为 unfinalized 的静态变量(双向链表结构),Finalizer 也可被视为另一种引用对象(地位与软、弱、虚相当,只是不对外,无法直接使用)
  2. 当重写了 finalize 方法的对象,在构造方法调用之时,JVM 都会将其包装成一个 Finalizer 对象,并加入 unfinalized 链表中 (表示这些对象的finalize方法还没有被调用哦,不要轻易释放它 (也是此引用链的作用))

在这里插入图片描述

  1. Finalizer 类中还有另一个重要的静态变量,即 ReferenceQueue 引用队列 (类似前面四种引用里面的引用队列,辅助释放引用对象本身(帮助释放关联的一些其他资源) 区别在于加入队列时关联的对象暂时不能被回收,因为要先调用 finalize 方法),刚开始它是空的。当狗对象可以被当作垃圾回收时,就会把这些狗对象对应的 Finalizer 对象加入此引用队列
  2. 但此时 Dog 对象还没法被立刻回收,因为 unfinalized -> Finalizer 这一引用链还在引用它嘛,为的是【先别着急回收啊,等我调完 finalize 方法,再回收】
  3. FinalizerThread 线程会从 ReferenceQueue 中逐一取出每个 Finalizer 对象,把它们从链表断开并真正调用 finallize 方法

在这里插入图片描述

  1. 由于整个 Finalizer 对象已经从 unfinalized 链表中断开,这样没谁能引用到它和狗对象,所以下次 gc 时就被回收了

finalize 缺点

  • 无法保证资源释放:FinalizerThread 是守护线程,代码很有可能没来得及执行完,线程就结束了
  • 无法判断是否发生错误:执行 finalize 方法时,会吞掉任意异常(Throwable try-catch给吞了)
  • 内存释放不及时:重写了 finalize 方法的对象在第一次被 gc 时,并不能及时释放它占用的内存,因为要等着 FinalizerThread 调用完 finalize,把它从 unfinalized 队列移除后,第二次 gc 时才能真正释放内存
  • 有的文章提到【Finalizer 线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐】这个显然是错误的,FinalizerThread 的优先级较普通线程更高(max-2=8 普通线程都才5),原因应该是 finalize 串行执行慢等原因综合导致(队列上取一个调用一个finalize )

代码说明

  • day03.reference.TestFinalize - finalize 的测试代码

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/hza419763578/article/details/130630961