JVM Java 虚拟机
JVM:类加载、双亲委派、ThreadLocal、垃圾回收机制与收集器。
JVM Java虚拟机
元空间是方法区的实现。
- 线程私有区域
- 程序计数器(PC Register)
- Java虚拟机栈(JVM Stack) 存储栈帧 当程序调用函数的时候 形成栈帧 入栈
- 本地方法栈(Native Method Stack)
- 线程共享区域
- 堆(Heap) 字符串常量池在这 实例对象和数组都在堆
- 方法区(Method Area)(元空间实现 ) 运行时常量池在这 类的 “元数据” 和相关静态资源提供存储空间 已被 JVM 加载的类信息、常量、静态变量、即时编译器
什么是程序计数器
线程私有的,每个线程有一份,内部保存的字节码的行号。核心作用存放下一条将要执行的指令的内存地址。
类加载机制
家宴准备了西式菜 加载 验证 准备 解析 初始化
加载 -> 连接 -> 初始化

- 加载 读取Class文件,将其转化为某种静态数据结构存储在方法区,并在堆中生成一个便于用户调用的java.lang.Class类型的对象的过程。
- 连接
- 验证 确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全。
- 准备 为类中的静态变量分配内存,并设置默认的初始值,比如int类型初始值是0。被final修饰的static字段不会设置,因为final在编译的时候就分配了。
- 解析 解析阶段是虚拟机将常量池的「符号引用」直接替换为「直接引用」的过程。Java通过 动态解析(运行的时候来解析)实现后期绑定和多态。
- 初始化 不是构造函数 而是class层面 JVM 会执行类的
<clinit>()方法(类构造器方法) 如静态变量赋值 执行静态代码块
双亲委派
向上委托,向下加载
- Bootstrap ClassLoader
- Extension ClassLoader
- Application ClassLoader
作用:
- 避免重复加载 (当向上委托的时候会先查看自己有没有加载过)
- 保证Java核心类库绝对安全
Tomcat如何打破双亲委派的
WebAppClassLoader 重写了 ClassLoader 的 loadClass() 方法,并实现了与标准双亲委派相反的加载顺序,先在本地缓存中查找,再委派给ExtClassLoader看是不是基础安全类,如果不是则尝试自己加载。如果加载类是核心类就不会自己加载。
为了实现多个 Web 应用之间的应用隔离和热部署功能。
深拷贝和浅拷贝是什么
- 浅拷贝并不是真的拷贝,只是复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
- 深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
深拷贝如何实现
- 使用“拷贝构造函数” (Copy Constructor) 这是一种设计模式,你为你的类提供一个“拷贝构造函数”,它接收一个同类型的对象,然后手动(Manually)复制所有字段。
public User(User other) {
this.name = other.name;
// 关键:不是 this.address = other.address;
// 而是为 address 创建一个新副本
this.address = new Address(other.address);
}- 使用 JSON 序列化 (推荐的实用方案) 这是目前最流行、最简单的“偷懒”方式,利用第三方库(如 Jackson, Gson, Fastjson)将对象序列化为 JSON 字符串,然后再反序列化为新对象。
- 使用 Java 原生序列化 (Serializable) 不推荐
- 重写
clone()方法 (不推荐)
介绍一下四种引用
- 强引用
- 垃圾回收器不会回收被引用的对象,哪怕内存不足时,JVM 也会直接抛出 OutOfMemoryError,除非赋值为 null。
- 软引用
- 软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
- 弱引用
- 弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
- 虚引用
- 虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收
ThreadLocal
privatestatic ThreadLocal<String> threadLocal = new ThreadLocal<>();
每一个Thread内部有一个ThreadLocalMap ThreadLocalMap<ThreadLocal,T>
- Key :ThreadLocal本身 为一个弱引用
- Value :手动Set的Value 为强引用
ThreadLocal使用不当会造成内存溢出问题
ThreadLocal tl = new ThreadLocal() 这句代码所在的方法执行完毕,tl 这个栈引用被销-毁了),在下一次垃圾回收(GC)发生时,这个 ThreadLocal 对象就会被回收。
在使用完 ThreadLocal 后,务必手动调用其remove()方法。
- 如果 Key 是强引用: 只要线程不死(比如线程池),那么
ThreadLocal对象本身永远无法被回收,Value也永远无法被回收。这是 100% 的、绝对的、永久的内存泄漏。 - 因为 Key 是弱引用: 当外部不再使用
ThreadLocal时,下次 GC 至少能把ThreadLocal对象本身给回收掉。虽然 Value 暂时留下了,但泄漏的范围缩小了。
*垃圾回收
- minorGC 新生代垃圾回收
- majorGC 老年代垃圾回收 由于majorGC发生时通常伴随着minorGC,因此也被称为fullGC
搜集算法 判断垃圾的方法有哪些
- 引用计数法 已淘汰
- 每一个对象有一个引用计数器
- 缺点显著,两个对象可能相互引用
- 可达性分析算法 实际采用的方法
- 从根对象
GC Roots触发,向下搜素,走过的路径就叫引用链。
- 从根对象
哪些是对象是GC Roots
- 虚拟机栈 本地方法栈 中的对象
- 方法区中的 static 对象 final 对象
- 被同步锁持有的对象
- JVM内部引用 如类加载器
- 正在活跃的线程
回收算法
- 标记清除法 先标记哪些是垃圾再统一回收
- 会产生大量碎片
- 标记复制算法 两块区域 将原取余的非垃圾复制到新取余,清除原区域。
- 会浪费一半的内存空间 两个幸存者区就是这样
- 标记整理算法 FullGC时候会执行碎片整理 将非垃圾往前挪
- 性能较差 适合老年代
- 分代回收算法:分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数 -XX:MaxTenuringThreshold 来设定)后,如果对象还存活,那么该对象会进入老年代。
垃圾回收器
堆是如何分布的
- 新生代
- Eden区
- Survivor区
- 老年代
CMS - 老年代垃圾回收器
Concurrent Mark Sweep 内存小 对延迟极其敏感
核心:它是以获取最短回收停顿时间(STW)为目标的收集器。
当老年代内存不足的时候触发
- 初始化标记 将整个年轻代都认为是GC Roots(广义的GC Root) STW
- 并发标记(递归标记 三色标记法) GC线程和用户线程并发
- 这时候新产生的或发生变化的对象会被标记为Dirt Card
- 重新标记 标记Dirty Card STW
- 并发清除 重新初始化CMS中的数据 为下一次CMS做处理
CPU处理核数 = (处理器核心数+3)/ 4 至少占用25%
致命缺陷:
- CPU使用频率高
- 标记-清除算法,会产生大量内存碎片
- 会产生浮动垃圾:在“并发清除”阶段,业务线程还在运行,这期间产生的新垃圾,CMS 这次是收不掉的,只能等下次 GC,这就是浮动垃圾
- 停顿时间有非常苛刻的要求
- 堆内存不够
G1 - Gabage-First - JDK9开始使用
用户可以指定期望的 GC 停顿时间,G1 会根据这个时间,优先回收那些垃圾最多、收益最大的 Region(这就是 Garbage-First 名字的由来)。
采用标记复制算法
把堆分成2048个Region
还是分了Egen、S0、S1、老年代,额外的Humongous大对象区
- 初始标记 从GC Root出发 MaxGCPauseMillis STW
- 并发标记
- 最终标记 SATB(Snapshot-At-The-Beginning)算法 STW
- 筛选回收 STW
G1垃圾回收器 1.9后的默认回收器
- 大内存
- 内存碎片敏感
- 性能平衡 停顿时间和吞吐量平衡