Java内存区域与垃圾回收
1,Java内存区域
1.1,运行时数据区域
java虚拟机在java的执行过程中将其管理的内存划分若干区域,有的随虚拟机进程的启动而一直存在,有的则依赖线程的启动和结束而建立和销毁。
1.1.1,程序计数器----线程私有
- 可以看作当前线程所执行的字节码的行号指示器
- 其工作:通过改变计数器的值来选取下一条需要需要执行的字节码指令,是程序的控制流指示器,分支、循环、跳转、异常处理、线程恢复等都依赖指示器完成。
- 程序计数器是此内存区域唯一一个在《java虚拟机规范》中没有任何OutOfMemoryError情况的区域。
1.1.2,Java虚拟机栈----线程私有--与线程生命周期一样
- 每个方法被执行时,Java虚拟机都会创建一个栈帧,用于储存局部变量表(形参,类型)、操作数栈(实参)、动态连接、方法返回地址等、
- 每一个方法的调用与执行完毕,都对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
- StackOverflowError,线程请求的栈深度大于虚拟机所允许的栈深度。OutOfMemoryError,当栈扩展时无法申请到足够内存。
1.1.3,本地方法栈----线程私有
- 与虚拟机栈功能类似,虚拟机栈为虚拟机执行Java方法(字节码)服务,本地方法栈为虚拟机使用到的本地(Native)方法服务。
- 什么是native方法: 什么是native方法
<1>一个Native方法就是一个Java调用非java代码的接口,
<2>当java需要和Java外面的环境交互,如操作系统或某些硬件交换信息。Thread类就用了很多Native方法,(Thread.currentThread(),
start()函数调用 native函数 start0(),)Java线程是直接映射在操作系统内核线程上的,一对一线程模型
<3>JVM部分是由C写的,我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。
<4>的解释器是由C实现的
解释器与编译器:
当程序需要迅速启动和执行时,解释器首先发挥作用,省去编译时间立即运行。
随时间推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,提高效率。
解释器:节约内存
编译器:提高效率
1.1.4,Java堆----线程共享
- Java世界中几乎所有的对象实例都在这里分配,(并不是所有的,--逃逸分析等)
- Java堆是垃圾收集管理的内存区域-GC堆(Garbage Collection)。(Eden区,fromSurvivor,ToSurvivor,新生代,老年代,永久代......,默认8:1:1,
也可以提过参数改)
- 参数-Xmx,-Xms,可以调节堆的大小。
1.1.5,方法区----线程共享--逻辑区域
- 主要储存虚拟机加载的类型信息,常量,静态变量,即时编译后的代码缓存等数据。
- 运行时常量池,Class文件(.java程序编译后生成.Class文件)中,除了有类的版本、字段、方法、接口等描述信息外,还有常量池表
用于存放编译期生成的各种字面量与符号引用,这部分内容在类加载(后面会讨论)后存放到方法区的运行时常量池中。
- 内存回收主要目标是,常量池回收和对类型的卸载。
1.1.6,直接内存
- 并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域,但也被频繁使用,可能导致,OutOfMemory异常,
- JDK1.4新加的NIO类,引入一种基于通道与缓存区的I/O方式,它可以使用Native函数之间分配堆外内存,然后通过一个存储在 Java 堆中的
DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
2,HotSpot虚拟机对象探秘
2.1,对象的创建
①类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否
已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
②分配内存:将对象所需空间从堆中划分出来
内存分配方法:
- 指针碰撞:适用于堆内存规整(即没有内存碎片,用过的内存被规整到一边,没用过的在一边中间放着一个指针指示分界)分配时将指针移动对象所需大小。
GC收集器:Serial,ParNew
- 空闲列表:堆内存不规整,使用内存与空闲内存交错存在,这时需要维护一个空闲列表,记录空闲内存的起始地址和大小。分配时找一个足够大的内存分配给对象。
GC收集器:CMS
内存分配并发问题:
- CAS加失败重试的方式保证更新操作的原子性
- TLAB(Thread Local Allocation Buffer) 每个线程在Java堆中预先分配一小块内存称为TLAB,线程给对象分配内存时,先在TLAB中进行,本地缓存用完了才去
同步锁定分配内存。
③初始化零值,内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中
可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
④设置对象头(什么是对象头,在并发synchronized原理里介绍过,后面也会介绍),对象头里储存着,这个对象是哪个类的实例、如何找到类的元数据信息、
对象的哈希码,GC年龄等。还有对象锁状态信息,
⑤执行init方法,完成上面工作,从虚拟机角度对象已创建,但从Java角度,所有字段都是零值,对象的其他资源和状态信息还没有构造好,
执行Class文件中<init>()方法,按代码(程序员意愿)对对象初始化,这样才创建完毕。
2.2,对象的内存布局
- 对象在堆内存的储存布局可以划分为三个部分:对象头(Header),实例数据(Instance Data),对齐填充(Padding)
- 对象头(就像数据报,帧格式里的那个头):分为自身运行时数据Mark Word,类型指针Class Metadata Address;
(如果是数组对象,还有数组长度项Array length)。
Mark World:
哈希码,GC分代年龄,锁状态标志,线程持有的锁、偏向线程ID、偏向时间戳等,
3,垃圾回收
- 哪些内存需要回收?(什么是垃圾)
- 什么时候回收?
- 如何回收?(回收策略)
在JVM内存区域各部分中,程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭,这几个内存区域的分配与回收都是确定的,不需要过多考虑回收问题,
堆区和方法区,则充满不确定性,只要运行时才直到要创建多少对象,对象的作用域,生命周期,如何分配与回收。
3.1,对象已死?(对象不会再被使用,超出作用域,则其占用的内存需要回收,即‘垃圾’)
判断对象是否存活的方法:
3.1.1,引用计数算法
- 给每个对象添加计数器,统计他被引用的数量,为0时表示不被使用了。
- 优点:原理简单,效率高,
- 缺点:很多例外情况,必须大量的额外处理,比如循环引用问题。
3.1.2,可达性分析算法
- 通过“GC Roots”的根对象,开始根据引用关系向下搜索,搜索走过的路径为引用链,搜索完成,某对象没有任何引用链,
则对象不可达。即不被使用了。
- GC Roots :一些确定位置的(一团乱麻的几个明显线头),好找到位置的对象,如虚拟机栈(本地方法栈)中引用的对象,
方法区静态属性引用的对象。方法区常量池中里的引用。
3.2,引用
- 判断对象是否存活,与对象是否被引用密切相关,
- 引用:如果reference类型的数据中储存的数值代表的是另一块内存的起始地址,就称该reference数据代表的某块内存,某个对象的引用。
- 上面引用定义只能表示被引用或未被引用两种状态,若一些模糊的定义,如当内存足够时,能保留再内存中(被引用),内存不够了
就可以抛弃这些对象,
3.2.1,强引用:只要强引用关系存在,垃圾收集器就永远不会回收掉被引用的对象。
3.2.2,软引用:在系统内存将要溢出前(负载达到一定比例),会把这些对象列进回收范围之中进行二次回收,如果这次回收还没有足够内存,则抛出异常
3.2.3,弱引用:用来描述非必要对象,被弱引用关联的对象只能生存到下一次垃圾收集发生,即当垃圾收集器开始工作,无论内存释放足够,
都会回收被弱引用关联的对象。
3.2.4,虚引用:被虚引用关联的对象的唯一目的只是,为了能在这个对象被收集器回收时收到一个系统通知。
3.3,finalize()方法
- 被可达性分析算法标记为不可达的对象,也不是‘非死不可’,一个对象死亡需要两次标记,①可达性分析中被标记为不可达,
②筛选对象是否执行finalize()方法,(首先对象得覆写finalize方法),若有必要执行,该对象会被加到F-Queue,队列中
等待虚拟机自动建立得线程执行
即finalize()方法是对象逃脱死亡的最后一次机会,(即在finalize方法中将对象与引用链上任何一个对象建立关联即可)
- finalize()方法最多只会被系统自动调用1次。
3.4,方法去的回收:废弃的常量,不在使用的类型
常量回收与对象类似,移除常量池
判断类不在使用:
- 该类的实例都已被回收
- 加载该类的加载器已被回收
- 该类对应的java.lang.Class对象没有在任何 地方引用,无法在任何地方通过反射访问这个类。
不在使用的类,虚拟机‘可以’回收,不一定非得回收。
4,垃圾回收算法
4.1,分代收集理论
新生代:大部分对象都朝生夕灭,每次垃圾回收都有大批对象死去
老年代:新手代中存活过一定收集次数(达到年龄,确定为长时间存活的对象),移入老年代
- 跨代引用,可达性分析需要遍历老年代,增加回收负担----记忆集,把老年代分为若干小块,只有存在跨代引用的才加入到GCRoot扫描。
Minor GC:新手代收集,指目标为新生代的垃圾收集
Major GC:指目标为老年代的垃圾收集
Full GC:收集整个堆和方法区的垃圾收集
4.2,标记-清除算法
先标记,再清理掉标记的对象,
缺点:①执行效率不稳定,执行效率随对象数量增长而降低
②内存空间碎片化。清除后产生大量不连续的内存碎片。
4.3,标记-复制算法
- 半区复制:将可用内存划分为大小相等的两块,每次使用其中一块。当块用完(触发垃圾回收),扫描此块,将存活的复制到
另一块内存中,回收整块内存
缺点:①如果存活对象较多,将会产生大量内存复制开销
②可用空间缩小为50%
- 优化半区复制:将新手代,分为Eden区和两个Survivor区【HotSpot虚拟机,Eden与Survivor大小比例是8:1】每次分配
只是用Eden区和其中一个Survivor区成为FromSurvivor,内存利用率90%,当用完时(或者触发垃圾回收时),将Eden和FromSurvivor中
存活的对象复制到另一个Survivor称为ToSurvivor。
问题:如果toSurvivor(10%)不够存放存活对象,将通过分配担保机制直接进入老年代。
4.4,标记-整理算法
- 标记-复制算法在对象存活将多场景下就要进行较多复制,且如果不想浪费50%空间,就要有额外空间进行分配担保。
- 使内存区域内存活对象集中移动到,一端,回收区域剩下的空间
缺点:①老年代中大量对象存活,移动开销很大,且这种移动存活对象操作必须在,全程暂停用户线程才能进行。Stop The Word
4.5 ,总结
清理算法与整理算法区别在于,是否移动对象使,内存‘规整’。
- 移动对象回收时更复杂,不移动内存分配会更复杂(空间碎片化,可用空间列表)
- 不移动对象,垃圾回收过程用时更短,移动吞吐量高,吞吐量=执行用户代码时间/(执行用户代码时间+垃圾回收时间),因为用户程序中内存分配与访问
操作相比垃圾收集频率高很多。所以如果追求吞吐量,宁愿增加垃圾收集阶段负担,也不能增加分配时的负担。
- 折中:虚拟机大部分时间用标记清除算法,当内存碎片化程度影响到内存分配时,采用一次标记-整理算法收集一次,规整一下内存空间。---CMS收集器
5,垃圾收集器
5.1,Serial/SerialOld收集器:单线程工作收集器,垃圾收集时暂停所有工作线程
新生代采用标记-复制算法,老年代采用标记-整理算法。
5.2,ParNew收集器:是Serial收集器的单线程版(指的是垃圾收集时多个线程并行收集),
ParNew/SerialOld 新生代采用标记-复制算法,老年代采用标记-整理算法。
5.3,Parallel Scavenge收集器,--新生代收集器,目标:达到一个可控制的吞吐量,
收集器提供两个参数,最大垃圾收集停顿时间和,吞吐量大小,收集器控制,垃圾收集时间尽量不超过用户设置的值。
Parallel Old收集器,Parallel Scavenge收集器的老年代版本,标记整理算法,支持多线程并发收集
- 并发可达性分析
可达性分析整个遍历要保障一致性,不想在遍历过程中暂停用户线程,就要考虑再标记过程中,对象被其他线程引用或断开引用导致对象消失的情况,
解决方案:①增量更新:在可达性分析遍历过程中,把已标记过的对象中被其他线程添加了新引用的对象记录下来,并发扫描完后,以记录的节点为根
重新扫描标记一遍(暂停用户线程),
②原始快照:在可达性分析遍历过程中,把未扫描完的对象中被其他线程删除了指向未扫描对象的引用记录下来,并发扫描完后,以记录的节点为根
重新扫描标记一遍(暂停用户线程),
把并发扫描过程中,被其他线程访问修改了的节点记录下来,并发扫描结束后,暂停用户线程,已记录了的节点为根进行局部重新扫描标记
5.4,CMS收集器 目标:以获得最短回收停顿为目的,
流程:①初始标记:Stop The World,标记GC Roots能直接关联到的对象,
②并发标记:时间较长,与用户并发执行,并通过增量更新技术把,被用户线程修改的对象记录下来。
③重新标记:Stop The World,重新扫描②中记录的对象。
④并发清除:(标记-清除算法---最短停顿时间)时间较长,与用户线程一起并发执行。
缺点:①对处理器资源敏感,虽然不用停顿用户线程,但占用了一部分线程,【CMS默认开启线程数:(CPU核心线程数+3)/4】
②CMS无法收集浮动垃圾,(并发标记与清理过程中,用户线程新产生的垃圾)并发清理失败会,引发Full GC,
③空间碎片化。
5.5, G1垃圾收集器(主要面向服务端) 目标:建立”停顿时间模型“ 在延时可控的情况下获得尽可能高的吞吐量。
- 面向局部收集,基于Region分区的内存形式。
①将连续的堆划分为多个大小相等的独立Region,根据需要,Region可扮演Eden,Survivor,Old空间。
②Regin中有Humongous区域用于储存大对象。
③以Region为最小回收单元,G1跟踪每个Region的回收价值(付出与回报双重考虑),及所需时间,维护一个优先队列,
每次根据用户设定的允许收集 停顿时间(默认200ms),优先处理回收价值比较高的Region,由回收率和所有时间共同决定),
- 关键细节:
①跨region(代)引用问题,每个都维护一个记忆集,与其他垃圾收集器相比,内存负担较大。
②并发标记问题:原始快照,SATB
③怎样建立可靠的停顿预测模型?以衰减均值为理论基础来实现,---比平均值更容易受到新数据的影响。
流程:
①初始标记:Stop The World,标记GC Roots能直接关联到的对象,
②并发标记:与用户线程并发执行,原始快照
③最终标记:Stop The World,重新扫描②中记录的对象
④筛选回收:Stop The World,更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的
停顿时间制定回收计划,将回收Region中的对象复制到空中,清理旧Region.
优缺点:
- 从整体看使用标记-整理算法,从局部看,为标记-复制算法, 不产生内存碎片。
- 内存占用,执行负载较CMS都偏高,(记忆集,回收优先队列)
- 小应用CMS优,大应用,G1优,
从G1开始,最先进的垃圾收集器的设计导向都不约而同的变为追求能够应付应用的内存分配速率。
6,内存分配与回收策略:
①对象优先在Eden中分配。
②大对象直接进入老年代--减少复制整理操作的开销。
③长期存活的对象将进入老年代,Survivor区中的对象,每活过一个Minor GC,GC年龄+1,默认15岁移入老年代。
④动态年龄判定:在Survivor空间中,相同年龄的所有对象大小总和超过Survivor空间一半,年龄>=该年龄的对象
可以直接进入老年代。
⑤空间分配担保:在Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间,是否大于新生代所有对象的总空间
如果条件成立,那么这一次Minor GC是安全的,因为如果Eden+FromSurvivor中存活对象空间>ToSurvivor,
有充足的空间提高担保。如果担保失败会触发Full GC
7,低延迟垃圾收集器
内存占用
延迟 吞吐量
- 随着硬件的发展,收集器多占用一定内存可以接受,但响应一定要快。
7.1,Shenandoah收集器 目标:实现一种能在任意堆内存大小都可以把垃圾收集的停顿时间限制在10ms以内。
即并发标记,并发收集,并发清理
- 流程:
①初始标记 --Stop ②并发标记 ③最终标记--Stop
④并发清理--这个阶段清理那些整个Region区域连一个存活对象都没有的Region
⑤并发回收--通过转发指针实现,与用户线程并发的执行环境下,将存活对象复制到其他空Region
引用更新:并发收集结束后,需要把堆中指向就对象的引用修正到复制后的地址
⑥初始引用更新:短暂Stop,确保所有收集线程已完成存活对象复制任务。
⑦并发引用更新:按内存物理地址,线性搜索,把旧值改为新值。 (重定向)
⑧最终引用更新:短暂Stop,解决堆中的引用更新后,还有修正GC Roots中的应用。
⑨并发清理:经过并发回收(复制存活对象)和并发引用更新(修改新地址)后,回收整个回收集
- 转发指针Brroks Pointer:在对象前维护一个指针,初始指向自己对象头,复制对象后,指向新对象对象头。(并发竞争,CAS保证)
优点:移动时只修改转发指针,不用暂停用户线程。
缺点:额外转型开销(用以维护转发指针)
7.2,ZGC收集器, 目标:实现一种能在任意堆内存大小都可以把垃圾收集的停顿时间限制在10ms以内。
- 基于Region堆内存布局,具有动态性-----动态创建和销毁,动态分配区域内存大小
小型Region:2MB,存放小于256KB对象
中型Region:32MB,存放256KB-4MB的对象
大型Region:容量不固定,2MB的整树倍,存放大于4MB的对象。
- 并发整理算法:染色指针,Colored Pointer----是一种直接将少量信息储存在指针上的技术。
64为linux指针只支持46位(64TB)物理地址空间。高18位不能用来寻址,ZGC将46位指针宽带的高4位提取出来,储存4个标志信息
即ZGC能管理42位地址寻址(4TB),
- 流程:
①并发标记:包含初始标记,最终标记过程(Stop),标记更新染色指针状态位。
②并发预备重分配:确定重分配集(Region集合--回收集),
③并发重分配:核心阶段。把重分配集中存活对象复制到新region上,并为重分配集中每一个Region维护一个转发表,
记录从旧对象到新对象的转发关系。 得益于染色指针,ZGC收集器能仅从引用上确定一个对象是否处于重分配集中,
如果用户线程并发访问处于重分配集中的对象,这次访问会被内存屏障截获,然后从转发表中找到新对象。并同时修改
该引用的值-----指针自愈
与Shenandoah的转发指针相比,只有第一次需要‘转发’。
④并发重映射:修改整个堆中的指向重分配集旧对象的所有引用。
----这是一个非比要过程,ZGC将并发重映射阶段工作合并到下一次垃圾收集循环中的并发标记阶段区完成,因为指针自愈。