程序计数器
指向当前线程正在执行的字节码指令的地址
虚拟机栈
存储当前运行方法所需的数据、指令、返回地址
栈帧
- 局部变量表
- 操作数栈
- 动态链接
- 完成出口
大小限制
-Xss
本地方法栈
方法区
- 类信息(类加载器加载在
运行时数据区的方法区) - 常量
- 静态变量
- 即时编译期编译后的代码
Java堆
- 实例对象(几乎所有实例对象)
- 数组
Java堆的大小参数设置
- Xmx 堆区内存可被分配的最大上限
- Xms 堆区内存初始内存分配的大小
方法区和Java堆都是线程共享的,为什么不用一份,而是要区分方法区和Java堆?
从线程共享区域需要存放的数据的特性考虑,类信息、常量、静态变量、编译后的代码等等这些数据属于使用频次高、变更较少且回收效率低,难度大;而实例对对象和数组,大部分生命周期较短,频繁在创建和销毁;综上线程共享区域采用了动静分离的思想,拆分出了方法区和Java堆,便于垃圾回收的高效
JDK 1.7 之前 方法区称为永久代 JDK 1.8 之后 方法区称为元空间
从底层深入理解运行时数据区
- 申请内存(初始化Java栈、Java堆、方法区的的大小和内存分配)
- 类加载(class进入方法区)
- 常量、静态变量等(常量声明、静态常量等进入方法区)
- 虚拟机栈(入栈帧)
- 对象创建和内存分配
Java栈的作用
以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量以及对象引用的变量,变量出了作用域就会自动释放
Java堆的作用
堆内存用于存储Java中的对象,无论是成员变量、局部变量还是类变量,它们指向的对象都存储在堆内存中
线程共享还是线程独享
-
栈内存是归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存
-
堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问
空间大小
- 栈的内存空间要远远小于堆内存,栈的深度是有限制的,可能发生StackOverFlowError问题
内存溢出
- 栈溢出
- 堆溢出
- 方法区溢出
- 本机直接内存溢出
虚拟机优化技术
编译优化技术
- 方法内联 减少虚拟机栈帧入栈和出栈
栈的优化技术
- 栈帧之间数据共享
类加载以后在什么时候卸载和回收?
- 类的所有实例都已回收
- 加载该类的类加载器已被回收
- 该类的class对象没有任何地方被引用,且无法通过反射访问该类的方法
同时满足以上条件,该类达到可被回收的前置条件,最后还需要JVM参数控制配合(-Xnoclassgc 禁用类的垃圾收集参数没有配置)
垃圾回收器
内存分配:
常规内存模型
- 新生代(包含Eden[8]、From[1]、To[1]三部分):占堆空间的1/3
- 老年代:占堆空间的2/3
G1内存模型
将内存划分为若干等大小的块,并将每个或某几个块分别标记为以下不同的区域进行对象的储存
- Eden
存放新创建的对象 - Old
存放内存中存活时间较长的对象 - Survior
存放前几次GC后存活的对象 - Humongous
存放大对象
为什么要将对空间划分为新生代、老年代或者按块分割?
其目的是为了方便垃圾回收,提高垃圾回收效率
对象分配原则(一般对象)
- 对象优先在Eden区分配
- 空间分配担保
- 大对象直接进入老年代
- 长期存活的对象进入老年代
- 动态对象年龄判断
几乎所有对象都是在堆中分配,但也不全是
也有分配在栈中的对象(逃逸分析)
虚拟机栈中分配的对象,不需要被GC,因为栈中元素执行完成以后就会自动释放栈用的内存,所以栈中运行的效率非常高,可以提高JVM的运行效率(逃逸分析的作用)
逃逸分析的条件(对象创建时) 没有方法逃逸(不会存活到方法之外) 没有线程逃逸(不会存活到线程之外) 没有设置关闭逃逸参数 在栈上分配
new 一个对象
↓
是否栈上分配(逃逸分析判断) →是→ 分配到栈上
↓否
本地线程分配缓冲 →是→ 分配到堆中Eden区(TLAB)
↓否
是否是大对象 →是→ 分配到老年代
↓否
分配到堆中Eden区
GC的类型
- Minor(次要)/Young GC:回收新生代内存空间
- Major/Old GC:回收老年代内存空间
- Full GC:回收整个堆空间内存和方法区
分代收集理论
- 绝大部分对象都是朝生夕死
(新生代) - 对象熬过了多次垃圾回收就越难回收
(老年代)
垃圾回收算法
复制算法
优点:实现简单、运行高效,内存复制、没有内存碎片
缺点:利用率只有一半
对于复制算法的改进:
根据研究和分析,绝大部分(98%)新生代的对象的都是会被即时回收的,所以,可以将新生代的内存空间氛围3个部分,即Eden(占新生代的80%),From(也叫幸存区1,占新生代的10%),To(幸存区2,占新生代20%)
发生垃圾回收时,将Eden区中可存活的对象复制到From区或者To区当中,From和To同一时间只会启用其中一片区域用于储存存活下来的对象(第一次垃圾回收,Eden存活对象全部进入了From区,To区格式化并闲置;第二次垃圾回收,Eden存活对象和From区存活对象全部进入To区,From区格式化并闲置;如此循环交替),当From或者To区内存占用满了,则直接进行空间分配担保,将新生代存活对象全部存储到老年代
标记-清除算法(Mark-Sweep)
特点:执行效率不稳定,内存碎片容易导致提前GC
适用于老年代小范围内存回收
标记-整理算法(Mark-Compact)
特点:对象移动,引用更新,用户线程暂停,没有内存碎片,效率偏低
JVM中常见的垃圾收集器
- 单线程垃圾收集器
- 多线程并行垃圾收集器
- 多线程并发垃圾收集器
| 收集器 | 收集对象和算法 | 收集器类型 |
|---|---|---|
| Serial | 新生代,复制算法 | 单线程 |
| Par New | 新生代,复制算法 | 并行的多线程收集器 |
| Parallel Scavenge | 新生代,复制算法 | 并行的多线程收集器 |
| Serial Old | 老年代,标记整理算法 | 单线程 |
| Parallel Old | 老年代,标记整理算法 | 并行的多线程收集器 |
| CMS | 老年代,标记清除算法 | 并行与并发收集器 |
| G1 | 新生代和老年代,标记整理+化整为零 | 并行与并发收集器 |
CMS垃圾回收器的工作原理
- 初始标记(需要暂停所有用户线程,占用一个用户线程来执行GC,标记所有与GC ROOT直接相关的对象)
耗时短 - 并发标记(用户线程和GC同时运行,根据初始标记来标记所有相关的对象)
耗时长 - 重新标记(需要暂停所有用户线程,因为并发标记过程中可能会有新对象产生,故重新标记)
耗时短 - 并发清理(用户线程和GC同时运行)
耗时长 - 重置线程(将GC占用的线程替换回用户线程)
优点:不需要全部暂停用户线程
缺点:
· CPU敏感
GC会占用用户线程并发执行任务,如果CPU核心数<4,对用户影响较大
· 浮动垃圾
并发清理过程中可能会产生新的垃圾无法处理,只能等待下一次GC
· 内存碎片
因为CMS使用的是标记清除算法,所以可能会产生较多内存碎片,标记清除以后,如果碎片较多,则可能会再次启动或者切换只Serial Old进行一次整理
G1 (Garbage First)
- 追求停顿时间
- Region区
- 筛选回收
- 可预测停顿
- 复制和标记整理算法
常量池(存在于方法区)
静态常量池
- 字面量
- 符号引用
- 类、方法的信息
运行时常量池
在对象创建过程中,符号引用需要转化成直接引用,这个直接引用就放在运行时常量池当中,如:Person person = new Person() 此时的person就是对实例化Person对象的直接引用,存放在运行时常量池当中,存放的是该对象对象头的hash值(内存地址)
虚拟机中对象的创建过程
类加载
检查加载
分配内存
划分内存方式
- 指针碰撞
适用于内存空间比较规整的情况
- 空闲列表
适用于内存空间比较零散,有较多碎片的情况
指针碰撞效率高于散列表,因为散列表需要多访问一次表结构
但是指针碰撞只适用于内存比较规整的情况,
而垃圾回收大多时候都是标记清除(因为更快,更少的暂停),
所以具体使用哪种方式由当时的内存情况来决定
分配内存时,JVM会根据算法判断当前堆空间内存占用情况,以及内存的规整情况,如果内存规整则使用指针碰撞进行空间分配,如果不规整则使用空闲列表进行分配
并发安全问题
-
CAS(Compare and sweep) 比较交换
乐观锁机制
-
TLAB 线程分配缓冲
在Eden区给每个线程指定划分一块区域用于存储对象,即减少线程并发申请内存空间时的碰撞问题
内存空间初始化
将分配的内存空间设置为初始值(不是赋值),如:int类型设置为:0,boolean类型设置为:false,String类型设置为:null等等
设置
设置对象头
对象初始化
此时才在决定调用构造方法
对象的内存布局
- 对象头
1 存储对象自身的运行时的数据
``` 哈希吗 GC年龄分代 锁状态标识 线程持有的锁 偏向线程ID 偏向时间戳 ```2 类型指针
指向该对象从属的类(Class)
3 若为对象数组,还又记录数组长度的数据
- 实例数据
- 对齐填充
JVM规范中规定:对象占用空间应为8字节的整数倍,便于寻址(内存地址查找),同时也是为了提高JVM运行的效率(减少同步和缓存)
对象的访问定位
- 使用句柄 在堆空间中划分一块区域作为句柄池
指向对象的指针
好处是当对象实例数据的地址变更后((GC整理后)),
引用不需要发生变更,
只需要变更指针指向的对象地址
多一次指针定位的开销
- 直接指针
引用直接指向对象的内存地址
当对象实例数据的地址变更后,
需要重新定位和更新引用指向的地址
判断对象的存活
- 引用计数法
直接,简单,快速
对GC root不可达的循环引用无法判别
- 可达性分析(根可达)
准确,高效
效率相较引用计数慢一点
主流虚拟机使用的是根可达算法
判断一个对象是否应该被回收 首先是根据可达性分析算法判断该对象是否应该被回收 其次还会再次判断该对象是否还有其他引用(finalize方法的自我拯救) 最后才能判断是否回收该对象
Finalize()方法的作用 当一个对象被销毁时,可能会调用Finalize()方法,且最多只会触发一次
为什么是可能?
因为 finalize() 方法的调用时在一个专门的Finalize线程中,该线程的优先级很低,不能确保会在对象销毁之前能够立即执行
各种引用
- 强引用
- 软引用
SoftReference
垃圾回收时,不一定会回收软引用持有的对象
只有堆空间不够时(即将OOM),才会回收
- 弱引用
WeakReference
只能存活于下一次垃圾回收之前
只要发生垃圾回收,必定回收弱引用持有的对象
- 虚引用
PhantomRefrence
随时会被回收
主要用于JVM内部,监控垃圾回收器是否正常工作