文章目录
- 对象的结构
-
-
-
- 对象头:
- 判断对象是否存活的算法:
-
- 引用计数算法:
- 可达性分析算法
- 对象引用的分类
-
- 强引用
- 软引用
-
- 弱引用
- 虚引用
-
-
- JVM内存区域
-
-
- 1. 程序计数器
- 2. Java栈(虚拟机栈)
- 3.本地方法栈
- 4.Java堆
- 5.方法区
- PS:JDK8后为什么用元空间替代永久代?
-
- 垃圾回收算法
-
-
- 标记-清除算法
- 复制算法
- 标记-整理算法
- 分代收集算法
- 垃圾回收过程
-
- 思考:对象如何晋升到老年代?
- 常见的调优参数
- 触发Full GC的条件
-
- 常见的垃圾收集器
-
- 年轻代常见的垃圾收集器
-
- Serial收集器(-XX:+UseSerialGC,复制算法)
- ParNew收集器(-XX:+UseParNewGC,复制算法)
- Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)
- 老年代常用的垃圾收集器
-
- Serial Old收集器(-XX:+UseSerialOldGC,标记-整理算法)
- Parallel Old收集器(-XX:+UseParallelOldGC,标记-整理算法)
- CMS收集器(-XX:+UseConcMarkSweepGC,标记清除算法)
- G1收集器(-XX:+UseG1GC,复制+标记-整理算法,同时适用于年轻代和老年代)
- JDK11:Epsilon GC 和 ZGC(比G1效率更高)
对象的结构
在HotSpot虚拟机中,对象在内存中的布局可以分为3块区域:对象头,实例数据,对齐填充
对象头:
包括两部分信息
第一部分**(Mark Word标记字段)**:哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定(确定对象的内存地址) |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录的信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
另一部分(klass point):类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针
补充:如果对象是一个Java数组,在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象大小
判断对象是否存活的算法:
引用计数算法:
- 给对象添加一个引用计数器,每当有一个地方引用它时,计数器加一;当引用失效时,计数器减一;任何时刻计数器为0 的对象就是不能再被使用的。
优点:实现简单、效率高
缺点:难以解决对象之间的循环引用的问题(A对象引用着B对象,B对象引用着A对象)
可达性分析算法
- 通过一系列的“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(也就是GC Roots到这个对象不可达)时,则证明此对象是不可用的。
可作为GC Roots的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用对象
- 方法区中常量引用的对象
- 本地方法栈中JIN(就是Native方法)引用的对象。
- 活跃线程引用的对象
对象引用的分类
强引用
- 指在程序代码中普遍存在的,类似
这类的引用,只要强引用还存在,垃圾回收器永远不会回收被引用的对象。Object obj = new Object()
软引用
- 用来描述一些还有用但是非必要的对象。软引用关联的对象,在系统要OOM之前,会把这些对象列为回收范围中进行二次回收。(JDK1.2之后提供了
类来实现软引用)。SoftReference
弱引用
- 也是用来描述一些还有用但是非必要的对象,但强度比软引用更弱,被弱引用关联的对象只能生存到下次垃圾回收之前,在下次垃圾回收时,无论此时是否正在被引用都会回收弱引用的对象。(JDK1.2后提供了
类来实现弱引用)。WeakReference
虚引用
- 是最弱的一种引用关系,一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用获取一个对象的实例。(JDK1.2之后提供了
类来实现虚引用)PhantomReference
JVM内存区域
1. 程序计数器
- 线程私有的,可以看作是当前线程说执行的字节码的行号指示器, 保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址) ,保证每个线程都在线程切换后能够恢复在切换之前的程序执行位置 。
- 在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值为空是(undefined)。由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。
2. Java栈(虚拟机栈)
- 线程私有的,描述的是Java方法执行的内存模型:每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 局部变量表:存放了编译期可知的各种基本类型(int,char,long,booblean,double,byte,short,float)、对象引用(reference类型,可能是一个指向对象起始地址的引用指针,也可能是指向一个对象的句柄或其他与此对象相关的位置)、returnAddress类型(指向了一条字节码指令的地址)。
3.本地方法栈
和虚拟机栈区别就是,虚拟机栈是为Java的方法(就是字节码)服务的,而本地方法栈是为虚拟机用到的Native方法服务的。
4.Java堆
- 线程共享的,Java堆是所有线程间共享的一块内存区域,在虚拟机启动时就创建。用于存放对象的实例,几乎所有对象实例都在这里分配内存,但随着JIT编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换(11章)等优化技术导致所有对象都分配在堆上的结论也不是那么绝对了。
- Java堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器基本都采用分代收集算法,所以可细分为新生代和老年代,再细一点可分为:Eden区、From Survivor区、To Survivor区和老年代。
补充:通过-Xms(最小)和-Xmx(最大堆)可控制堆的大小。
5.方法区
- 线程共享的,各个线程线程间共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 运行时的常量池是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等信息,还有常量池:用与存放编译期生成的各种字面量和符号引用。
PS:JDK8后为什么用元空间替代永久代?
元空间和永久代的区别:元空间是使用的是操作系统的内存,永久代使用的是JVM的内存。
为什么要在直接内存里拿出来一块内存作为元空间取代永久代呢?主要的说法有以下几个:
(1)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
(2)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
即方便分配管理,因为直接内存空间比较充足;便于回收,因为永久代本来回收垃圾的事件发生概率很低,直接从拿到系统内存中可以提高回收效率。
方法区与永久代的关系
- 很多文章里喜欢把方法区等同与永久代,永久代既然没了,方法区也就没了。但我认为方法区只是一种逻辑上的概念,永久代指物理上的堆内存的一块空间,这块实际的空间完成了方法区存储字节码、静态变量、常量的功能等等。既然如此,现在元空间也可以认为是新的方法区的实现了。
垃圾回收算法
标记-清除算法
算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有标记的对象。他的标记过程就是使用了“GC Roots(可达性分析算法)”,其他垃圾回收算法就是针对其缺点进行优化的。
缺点:
- 1.效率不高,标记和清楚两个阶段的效率都不高
- 2.空间问题,使用标记清楚后会产生大量不连续的内存碎片,空间碎片可能会导致程序运行过程中需要分配大对象时,无法找到连续的内存空间而再次触发垃圾回收。
复制算法
将可以的内存分为大小相等的两块,每次只使用其中一块。当这一块用完了,就将还存活的对象复制到另一块上面,然后把已使用过的内存一次清理掉。
优点:
- 1.解决了碎片化的问题:因为是直接对整半个区域进行回收,不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可
- 2.实现简单、运行高效
缺点:
- 内存相当于直接减少了一半,代价太高。
优化方案:
因为新生代中98%的对象都是“朝生夕死”,所有并不需要按1:1来划分内存空间,而是将内存划分为一块较大的Eden区和两块较小的Survivor区(细分为From区和To区,默认分配大小为8:1:1)。当回收时,Eden和Survivor中还存活的对象一次性地复制到另外一个Survivor空间上,最后清理Eden和刚才用过的Survivor区。
标记-整理算法
算法分为标记、整理两个部分,标记:从根集合进行扫描,对存活的对象进行标记;清除:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。
优点:
- 避免内存的不连续性
- 不用设置两块内存互换区域
- 适用于对象存活率较高的场景(老年代)
分代收集算法
主流虚拟机的算法,如HotSport将内存区域分为新生代和老年代(1.8后移除了永久代),不同的区域采用不同的垃圾回收算法,如新生代采用复制算法,老年代采用标记-整理算法。
- Minor GC 年轻代的垃圾回收
- Full GC 和 Major GC 老年代垃圾回收
- Yong和Old 的默认比例大约为:1:3
垃圾回收过程
1.对象创建首先在Eden区,当Eden区满会将存活的对象拷贝到From区(对象年龄+1),然后引发Minor GC回收Eden区;
2.当Eden区再次满了,JVM会将Eden区、From区存活的对象拷贝到To区(对象年龄+1),然后引发Minor GC回收Eden区和From区;
3.当Eden区再再次满了,JVM会将Eden区、To区存活的对象拷贝到From区(对象年龄+1),然后引发Minor GC回收Eden区和To区;
说明:当对象的分代年龄到达某个值(默认为15,可用-xx:MaxTenuringThreshold参数调整);老年代的担保机制:当大对象创建时,Eden区和Survivor区装不下时,会分配到老年代。
思考:对象如何晋升到老年代?
- 经历过一定的Minor GC次数依然存活的对象
- Survivor区中存放不下的对象
- 新生成的大对象(可通过:-XX:+PretenuerSizeThreshold,参数设置)
常见的调优参数
- -XX:SurvivorRatio :Eden和Survivor的比值,默认为8:1
- -XX:NewRatio :老年代和年轻代内存大小的比例
- -XX:MaxTenuringThreshold :对象从年轻代晋升到老年代经过GC次数的最大阈值
触发Full GC的条件
- 老年代空间不足
- 永久代空间不足(JDK1.7之前)
- Minor GC晋升到老年代的平均大小大于老年代剩余空间
- 程序调用System.gc()
- 使用RMI来进行RPC或管理的JDK应用,默认每小时执行1次Full GC
常见的垃圾收集器
分为年轻代收集器和老年代收集器,有连线表示两者可搭配使用
年轻代常见的垃圾收集器
Serial收集器(-XX:+UseSerialGC,复制算法)
- 单线程收集,进行垃圾回收时,必须暂停所有工作线程
- 简单高效,Client模式下默认的年轻代收集器
ParNew收集器(-XX:+UseParNewGC,复制算法)
- 多线程收集,其余行为、特点和Serial收集器一样
- 单核CPU下执行效率不如Serial,多核CPU下有很大优势,默认开启的回收线程数与CPU数量相同
- 除Serial,只要ParNew可以和CMS收集器配合工作
Parallel Scavenge收集器(-XX:+UseParallelGC,复制算法)
- 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
- 多线收集,比起关注用户线程停顿时间,更关注系统的吞吐量
- 多核下执行有优势,Server模式下默认的年轻代收集器
如果不知道用什么垃圾收集器,可以使用:-XX:UseAdaptiveSizePolicy 参数让JVM自适应调节垃圾收集器
老年代常用的垃圾收集器
Serial Old收集器(-XX:+UseSerialOldGC,标记-整理算法)
- Serial的老年代版本,单线程收集,收集时必须暂停所有工作线程
- 简单高效,Client模式下默认的老年代收集器
Parallel Old收集器(-XX:+UseParallelOldGC,标记-整理算法)
- 多线程,吞吐量优先,主要和Parallel Scavenge收集器组合使用
CMS收集器(-XX:+UseConcMarkSweepGC,标记清除算法)
当下主流的老年代垃圾收集器
- 停顿时间短,几乎可以和用户线程同时工作(仅初始标记和重新标记需要暂停)
回收过程:
- 初始标记:stop-the-world
- 并发标记:并发追朔标记(GC Roots),程序不会停顿
- 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
- 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
- 并发清理:清理垃圾对象,程序不会停顿
- 并发重置:重置CMS收集器的数据结构
缺点:
- 因为采用的是标记清除算法,所以无法避免内存空间碎片化的问题,当分配大对象没有连续的空间时,只能再次引发GC
G1收集器(-XX:+UseG1GC,复制+标记-整理算法,同时适用于年轻代和老年代)
- 并行和并发,使用多个CPU来缩短stop-the-world的时间,并发的进行垃圾清理
- 分代收集,既可以用于年轻代,又可以用与老年代,且不同代采用不同的垃圾回收算法
- 空间整合,采用了标记-整理算法,解决了内存碎片化的问题
- 可预测的停顿
原理:
- 将整个Java堆内存划分成多个大小相等的Region
- 虽然还有新生代、老年代的概念,但两者不再是“物理”上的隔离的了
JDK11:Epsilon GC 和 ZGC(比G1效率更高)
持续更新中……