1. 什么是强软弱虚
强软弱虚是指四种引用级别,
强: 只要强引用存在, 被引用的对象就不会被GC清除, JVM内存管理器, 从根节点出发遍寻所有可达对象的路径, 当到达某对象的任意路径都不含邮引用对象时
弱: 当垃圾回收器进行垃圾回收时, 引用对象就会被回收, 不管内存是否充足
软: 只有内存不足时, 才会被回收的对象
虚: 并不会决定对象的生命周期, 也无法通过虚引用获取对象实例, 在任何时候都有可能会被垃圾回收器回收, 主要用来跟踪对象被垃圾回收器回收的过程
2. java 中的集合
java 中的集合是collection 接口下的实现, 主要包含有list、set、和 queue
list 下主要有 arrayList 和 LinkedList
array List 底层为数组, 通过下标确认数据所在位置, 因此其查询速度以及修改速度相较于linked更快, 新增数据时可能会触发自动扩容, 自动扩容为创建新数组, 拷贝老数组
linked List 底层为双向链表, 其内部使用node, 包含有前后指针, 可以指向前后元素, 所以其增删速度相较于array更快, 同时其实现了queue, 所以也可以看作是栈和队列, 可以先进先出
queue 是队列接口, 其下有双向队列deque, 推荐使用arrayDeque, 阻塞队列什么的也都是属于queue 的实现
ArrayDeque 是一个双端队列, 基于动态数组实现, 既可以作为队列, 也可以作为栈, 拥有动态扩容机制, 即每次插入都会判断容量是否足够, 不足就会触发2倍扩容
set 下的话主要有treeSet 和 hashSet. 而如果要说这两个就得先说treeMap 和hashMap,因为这两个set 都是使用的map 两个实现的key 集合, 其value 是一个present 的obj 对象
hashMap 使用的是node, 底层为数组+链表+红黑树, treeMap 使用entry 存储, 底层为红黑树, 自带排序, 因为其直接实现是SortMap 下的navigableMap
hashMap 在1.8 采用尾插法, 避免了链表死循环, 同时优化了hash算法,将其改为前后16位的异或计算hash, 为了保证hash的散列度, 其保留了key 的高低16位特性. 引入红黑树, 当链表长度达到8并且元素个数达到64时, 会将链表转为红黑树, 当树大小变成6, 又会转为链表结构.
hashMap 为懒创建容器,即只有第一次向其put元素时, 才会初始化容器, 默认大小为16, 负载因子0.75
treeMap 抛弃了数组和链表, 直接使用红黑树
3. jvm 内存模型
JVM 包含有两个子系统和两个组件, 两个子系统时Class Loader 以及 执行引擎
两个组件是 运行时数据区和本地接口
ClassLoader会根据给定的全限定类名来装载class文件在运行时数据区, 即runtime data area 中的method area, 即方法区
执行引擎会去执行classes 中指令
本地接口 与 native libraries 进行交互, 是其他编程语言交互的接口
Runtime data area 就是常说的jvm 内存
首先通过编译器吧java代码转成字节码文件, 类加载器再把字节码加载到内存中, 将其放在运行时数据区的方法区内, 而字节码只是JVM的一套指令集规范, 并不能直接交给底层操作系统去执行, 因此需要特定的命令解析器即执行引擎, 将字节码文件翻译成底层系统指令, 在交由CPU执行, 而这个过程需要调用其他语言的本地库接口来实现整个程序的过程
3.1 JVM 内存模型
JVM内存模型通常分为堆栈方法区, 本地方法栈, 程序计数器等.
首先是堆, 所有线程共享, 几乎所有对象实例和数组都在堆上分配内存, 静态对象
然后是栈,分为虚拟机栈和本地方法栈,都是属于线程私有, 每个方法的执行叫做入栈, 同时会创建一个栈帧用于保存局部变量表, 操作数栈, 动态链接、方法出口等, 每个方法的返回叫做出栈. 本地方法栈服务于本地方法
方法区: 1.8 之前叫永久代, 1.8 叫元空间, 存储被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据
程序计数器, 当前线程所执行的字节码的行号指示器, 线程私有
4. 堆栈的区别
物理地址:
堆的物理地址分配是不连续的, 因此性能较慢, 在GC时要考虑到不连续的分配, 所以有各种算法, 比如标记-清除, 复制,标记压缩,分代(新生代使用复制, 老年代使用标记压缩)
栈中使用的是数据接口的栈, 先进先出的原则,物理地址分配是连续的, 所以性能快
内存分别:
堆因为是不连续的, 所以分配的内存是在运行期确认的, 因此大小不固定, 一般远大于栈
栈是连续的, 所以分配的内存大小在编译期确认, 大小是固定的
存放的内容:
堆存放的是对象的实例和数组, 因此更关注数据的存储
栈存放的是局部变量、操作数栈、返回结果等,更关注程序方法的执行
静态变量存放在方法区, 静态对象存在在堆
程序可见性:
堆对于整个应用程序都是共享可见的
栈对于线程是可见的, 即属于线程私有, 生命周期和线程相同
5. JVM 类加载机制的三种特性
1. 全盘负责: 当一个类加载起负责加载某个类时, 该class所依赖的和引用的其他class也将由该类加载器负责载入, 除非显示使用另一个类加载器
2. 父类委派: 双亲委派是指子类加载器如果没有加载过该目标类,就委托父类加载器加载该目标类, 只有在父类加载器找不到字节码文件的情况下,才从自己的类路径中查找并装载目标类
3. 缓存机制: 保证所有加载过的class都将在内存中缓存, 当程序中需要使用某个class时, 类加载器先从内存的缓存区寻找该class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成class对象,存入缓存区, 这就是为什么修改了Class后, 必须重启JVM, 程序的修改才会生效. 对于一个类加载器实例来说, 相同全名的类只加载一次, 即loadClass方法不会被重复调用
5.1 双亲委派机制加载class过程
1. ClassLoader 先判断该Class是否已加载, 如果已加载, 则返回Class对象, 如果没有则委托给父类加载器
2. 父类加载器判断是否加载过该Class, 如果已加载, 返回class对象, 否则委托组父类
3. 以此类推, 直到始祖类加载器(引用类)
4. 始祖类加载器判断是否加载过该Class, 如果已加载, 则返回class对象,如果没有则尝试从其对于的类路径下寻找class字节码文件并载入, 如果载入成功, 则返回class对象,如果载入失败,则委托给始祖类加载器的子类加载器
5. 始祖找子, 成功返回 否则找孙
6. 以此类推, 直到源ClassLoader
7. 源ClassLoader 载入成功返回, 载入失败抛异常
5.2 破坏双亲委派
双亲委派只是java推荐的机制, 但是并不是强制机制
可以继承ClassLoader , 实现自己的类加载器, 如果想保持双亲委派, 就重写findClass 方法, 如果想破坏, 就重写loadClass 方法
6. 常量池分类
6.1 静态常量池
静态常量池是相对于运行时常量池来说的, 属于描述class文件结构的一部分
由 ‘字面量’ 和 ‘符号引用’ 组成,在类被加载后会将静态常量池加载到内存中, 也就是运行时常量池
字面量: 文本,字符串,以及final 修饰的内容
符合引用: 类、接口、方法、字段等相关的描述信息
直接引用: 符号引用已经具体的落地到了内存,有了自己的地址
6.2 运行时常量池
当静态常量池被加载到内存后就会变成运行时常量池
也就是真正的把文件内容落地到JVM内存中了
6.3 字符串常量池
字符串作为最常用的数据类型, 为减小内存的开销, 专门为其开辟了一块内存区域用于存放
1.6 存放于永久代, 即当前的方法区
1.7 以后存放于堆中
7. 对象的生命周期
对象的生命周期主要分为创建阶段、应用阶段、不可见阶段、不可达阶段、收集阶段、终结阶段、以及空间重分配阶段
1. 首先是对象的创建阶段, 为对象分配存储空间, 开始构造对象, 从超类到子类对static 成员进行初始化
2. 然后超类成功变量按顺序初始化, 递归调用超类的构造方法, 子类成员变量按顺序初始化, 子类构造方法调用, 并且一旦对象被创建, 并被分配给某些变量赋值, 这个对象就切换到了应用阶段
3. 应用阶段时, 系统至少维护着对象的一个强引用, 所有对该对象的引用全都是强引用, 除非显示使用软弱虚
4. 然后到不可见阶段, 对象在虚拟机的根引用集合中再也找不回直接或者间接的强引用, 最常见的就是线程或者函数的临时变量, 程序不在持有对象的强引用
5. 然后到不可达阶段, 指对象不再被任何强引用持有, GC发现该对象不可达
6. 收集阶段, 垃圾回收器发现对象处于不可达状态, 会将其标记为可回收, 并在合适的时机进行回收, 会判断对象有没有必要执行finalize 方法
7. 在回收对象之前, 可能会调用对象的finalize 方法, 给对象最后一次自我拯救机会, 如果在finalize 方法中,重新让对象可达,对象将不会被回收, 否则对象进入终结阶段
8. 对象被回收后,其所占用的内存空间可以被重新分配给新创建的对象使用
8. 对象模型的对齐填充设计原因
对齐填充的意义是, 提高CPU访问数据的效率, 主要针对会存在 该实例对象数据跨内存地址区域存储的情况
如果对象的字段没有按照合适的边界对齐, 可能会导致多次内存访问来获取完整的数据, 降低了内存访问的效率, 通过填充可以确保对象的字段按照最优的字节边界对齐, 从而实现一次内存访问就能获取所需的数据,t提高了程序的运行速度
同时通过对齐填充, 可以使对象在各种平台上都能以一种高效的方式进行内存操作,增强了程序的可移植性
9. 对象的垃圾回收流程
一般情况下,新创建的对象都会分配在eden 区, 一些特殊的大的对象会直接分配到old区,首先Eden 区如果放不下, 会触发一次MGC, 如果触发之后还放不下, 会将其放入到老年代中,
eden 区满了之后, 会触发MGC, 将存活对象放入到S0区, 然后持续放入对象, 直到S0满了, 会将这部分对象放入到S1区, 同时对这部门对象进行标记
然后重复S0-S1 直到对象标记达到18, 就会将这个对象放入到老年代中, 如果老年代也满了, 就会触发一次FGC, 如果触发完FGC之后, 还是无法放入对象,
那么就会OOM
10. 为什么需要两个S区
最大的好处就是解决了碎片化
如果只有一个S区, 则新建对象, 触发MGC, eden 中的存活对象移动到S1区, 一直循环, 直到eden 又满了, 再次进行MGC, 此时eden 区和 S1区各有存活对象,
如果将eden 区的存活对象硬放到S1 区, 则这两部分对象所占有的内存是不连续的, 就会导致内存碎片化, 两个S区就可以保证永远有一个S区是空的, 另一个非空的无碎片
11. 栈帧的动态链接
栈帧中的动态链接是指在运行时将符号引用转换为直接引用的过程.
当一个方法调用另一个方法时, 通过在运行时查找符号表等数据结构, 将符号引用解析为直接引用, 从而能够正确的挑战到被调用方法的位置执行
动态链接, 允许程序在运行时进行模块的加载和更新, 增强了程序的可扩展性和可维护性
在运行时, 通过动态链接, 虚拟机能够根据当前的运行环境和类的加载情况, 找到方法的实际内存地, 从而完成方法的调用
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!