java的内存回收技术(Java内存模型和垃圾回收)java基础 / Java内存管理与垃圾回收...

wufei123 发布于 2024-06-28 阅读(6)

“Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。”1. 运行时数据区域按照java虚拟机规范,抽象的Java虚拟机如下图所示:

1.1 程序计数器每条线程都有一个独立的程序计数器,用于记录当前线程所执行的字节码的行号如果执行的是java方法,计数器记录的是虚拟机字节码指令的地址,如果是本地方法,则计数器值为空1.2 Java虚拟机栈。

Java虚拟机栈也是线程私有的,和线程的生命周期相同虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息每一个栈帧在虚拟机中入栈到出栈的过程,对应了一个方法从调用到执行完成的过程。

当进入一个方法时,这个方法需要在帧中分配多大的局部变量表是完全确定的,在方法运行期间局部变量表的大小不会改变其中局部变量表是我们最为关注的部分,他存放了编译期可知的8种基本类型数据、对象引用和returnAddress

类型1.3 本地方法栈本地方法栈和虚拟机栈作用类似,不过是为虚拟机要使用的本地方法提供服务1.4 Java堆Java堆是Java虚拟机管理的内存中最大的一部分他是被所有线程共享的一块内存区域,在虚拟机启动时创建。

Java堆的目的是存放对象实例,基本上所有的对象和数组都需要在堆上进行分配Java堆也是垃圾收集器管理的主要区域1.5 方法区方法区也是被各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、

静态变量和即时编译器编译后的代码等数据方法区的内存回收目标主要是针对常量池的回收和对类型的卸载1.6 运行时常量池Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种。

字面量和符号引用而运行时常量池相对于Class文件常量池的特征是具备动态性,只有没预置入Class文件中常量池的内容才能进入方法区运行时常量池,比如String的intern()方法2. 对象2.1 对象的创建。

虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过若没有,需要先执行相应的类加载过程在类加载检查通过后,虚拟机会为新生对象。

分配内存,分配方式一般有两种:指针碰撞和空闲列表当Java堆中的内存规整时,直接把指针挪动对象大小的距离即可,即指针碰撞;如果Java堆中的内存不规整,需要维护一个记录哪些内存可用的列表,分配时从列表中给对象分配空间,即空闲列表。

内存分配完成后,虚拟机需要将分配的内存空间初始化为零值然后,虚拟机要对对象进行必要的设置将在对象头中设置对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息最后,会执行对象的。

方法,把对象按照程序员的意愿进行初始化2.2 对象的内存布局在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头、实例数据和对齐填充对象头包括两部分信息:第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志。

线程持有的锁、偏向线程ID、偏向时间戳等另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例实例数据部分是对象真正存储的有效信息,HotSpot分配策略中,相同宽度的字段总是被分配到一起,满足这个条件的前提下,在父类中定义的变量会出现在子类之前。

对齐填充是非必须的,仅仅起到占位符的作用,由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,所以当对象实例数据部分没有对齐时,需要通过对齐填充来补全2.3 对象的访问定位。

主流的访问方式有两种:句柄和直接指针两种句柄:Java堆中会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体信息优点是当对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要改变。

直接指针:Java堆对象的布局中放置了访问类型数据的相关信息,reference中存储的是对象地址优点是速度更快,节省了一次指针定位的开销HotSpot使用直接指针的方式进行对象访问3. 垃圾收集器与内存分配策略。

第一节我们提到,程序计数器、虚拟机栈、本地方法栈三个区域随线程生,随线程灭,每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的而Java堆和方法区不同,我们只有在程序运行期间才能确定会创建那些对象,这部分的内存分配和回收都是动态的,垃圾回收时主要关注的是这部分的内存。

3.1 判断对象是否存活垃圾收集器进行垃圾回收前,首先需要判断那些对象还是存活的3.1.1 引用计数法给对象添加一个引用计数器,每当有引用时计数器加1,引用失效时计数器减1,计数器为0的对象就是“垃圾对象”。

优点:实现简单,判定效率高缺点:很难解决对象之间的循环引用问题3.1.2 可达性分析法通过一系列被称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”,当一个对象到GC Root没有任何引用链相连时,证明此对象是“垃圾对象”。

Java语言中,可作为GC Roots的对象有:虚拟机栈(栈帧中的本地变量表)中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象本地方法栈中JNI引用的对象3.2 对象的自我拯救对象可以通过覆盖finalize()方法,在其中和引用链上的任何一个对象建立关联,逃过一次垃圾回收,但因为每个对象的finalize()方法只会被系统自动调用一次,所以对象最多通过这种方式逃过一次垃圾回收。

不过这种方式并不被推荐使用3.3 回收方法区方法区的垃圾回收主要有:废弃常量和无用的类当一个常量池中的常量(字面量和符号引用)没有被在任何地方被引用,且发生了内存回收的话,这个常量就会被清理出常量池无用的类:

该类的所有实例都已经被回收加载该类的ClassLoader已经被回收该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问3.4 垃圾回收算法G1之前的垃圾回收算法,将堆划分为如下结构:

新生代:eden space + 2个survivor老年代:old space永久代:1.8之前的perm space元空间:1.8之后的metaspace3.4.1 标记清除(Mark-Sweep)算法

首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象不足:效率较低,标记清除后会产生大量不连续的内存碎片3.4.2 复制(Copying)算法将内存划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另一块上面,然后把已经使用过的内存空间全部清理掉。

优点:实现简单,运行高效缺点:讲内存缩小为原来的一半,过于浪费空间IBM研究表明,新生代中的对象98%都是“朝生夕死”的,所以不需要按照1:1来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor空间。

HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,所以只有10%的空间会被“浪费”,可以通过-XX:SurvivorRatio参数调整这个比例复制算法在对象存活率较高时,需要进行较多的复制操作,而且需要额外的空间进行分配担保,所以老年代一般不能直接选用这种算法。

3.4.3 标记整理(Mark-Compact)算法首先对存活的对象进行标记,然后将所有存活的对象向一端移动,然后直接清理掉端边界以外的内存3.5 垃圾收集器收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。

HotSpot包含的垃圾收集器如图所示:

到目前为止,没有最好的收集器,我们应该针对具体的使用场景选择最合适的垃圾收集器3.5.1 Serial收集器这个收集器是一个单线程收集器,使用复制算法,他会在进行垃圾收集时,暂停所有其他的线程,直到收集结束。

其运行过程如下:

由于其简单而高效(与其他收集器的单线程相比,没有线程交互的开销),他依然是虚拟机运行在Client模式下的默认新生代收集器3.5.2 ParNew收集器ParNew收集器是Serial收集器的多线程版本,使用复制算法,工作过程如下:。

他是运行在Server模式下的虚拟机中首选的新生代收集器,其中一个很重要的原因是只有Serial收集器和ParNew收集器可以和CMS收集器配合工作ParNew默认开启的收集线程数与CPU数量相同,在CPU很多的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾回收的线程数。

3.5.3 Parallel Scavenge收集器此收集器也是使用复制算法的收集器,但他的关注点是达到可控制的吞吐量,所以也被称为”吞吐量优先“收集器吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。

此收集器比较适用于需要与用户交互的程序和后台运算较多的程序它提供了两个参数用于精确控制吞吐量:最大垃圾收集停顿时间: -XX:MaxGCPauseMillis,参数值是一个大于0的毫秒数设置吞吐量大小: -XX:GCTimeRatio,参数值一个[0, 100)的整数,也就是垃圾收集时间占总时间的比率,1 / (1 + N),默认值是99。

此收集器还有一个参数-XX:+UseAdaptiveSizePolicy,开启后虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或最大的吞吐量,被称为GC自适应的调节策略

3.5.4 Serial Old收集器此收集器是Serial收集器的老年代版本,使用单线程和”标记整理“算法,主要用于Client模式的虚拟机如果在Server模式下,还有两大用处:在JDK 1.5之前与Parallel Scavenge收集器搭配使用。

作为CMS收集器的后备预案3.5.5 Parallel Old收集器此收集器是Parallel Scavenge的老年代版本,使用多线程和”标记整理“算法在注重吞吐量CPU资源敏感的场合,可以优先考虑Parallel Scavenge加Parallel Old收集器,工作过程如下:。

3.5.6 CMS(Concurrent Mark Sweep)收集器此收集器是一种以获取最短回收停顿时间为目标的收集器,使用”标记清除“算法过程分为四步:初始标记(CMS initial mark)并发标记(CMS concurrent mark)

重新标记(CMS remark)并发清除(CMS concurrent sweep)其中,初始标记和重新标记还是需要”Stop The World“初始标记仅仅标记可以和GC Roots关联到的对象并发标记进行GC Roots Tracing

重新标记用于修正并发标记期间因为用户程序继续运行导致标记产生变动的对象的标记记录

整个回收过程中,耗时最长的是并发标记和并发清除过程,但这两个过程都是可以和用户线程一起工作的,所以从总体上说,CMS收集器的内存回收过程和用户线程是并发执行的优点: 并发手机、低停顿缺点:对CPU资源非常敏感,并发阶段会导致总吞吐量降低。

无法处理浮动垃圾,需要预留一部分空间提供给并发收集时的程序运作,如果CMS运行期间预留的内存无法满足程序需要,会触发一次”Concurrent Mode Failure“失败,临时启用Serial Old收集器。

会产生大量的内存碎片,提供-XX:+UseCMSCompactAtFullCollection参数(默认开启),用于要进行FullGC时开启内存碎片的合并整理过程,还提供了一个-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的FullGC后,再进行一次带压缩的(默认值为0)

3.5.7 G1收集器此收集器是一款面向服务端应用的垃圾收集器,其特点有:并行与并发,使用多核减少STW停顿时间,GC动作通过并发方式让Java程序继续执行分代收集空间整合,整体是基于”标记整理“算法实现的,局部是基于”复制“算法实现的,不会产生内存空间碎片

可预测的停顿,可以指定在长度为M毫秒的时间片断内,垃圾收集时间不超过N毫秒G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),并跟踪各个Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据允许的时间,优先回收价值最大的Region。

每个Region是逻辑上连续的一段内存结构如下:

其中当新建对象大小超过Region大小一半时,会直接在一个或多个新的连续Region中分配此对象,并标记为Humongous对象3.5.7.1 RegionRegion的大小为1M——32M的2的N次幂,默认数量为2048个,如果G1HeapRegionSize为默认值,则会在堆初始化时计算Region的实际大小。

在G1收集器中,垃圾回收只回收一部分Region,所以回收时需要知道Region之间的对象引用,在使用复制算法移动对象时,需要更新引用为对象的新地址这种分代收集中,年轻代垃圾收集时,需要老年代到年轻代的引用记录,通常称为Remembered Set。

当虚拟机发现程序在对Reference类型的数据进行写操作时,会场生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之间,如果是,则通过CardTable讲相关引用信息记录到被引用对象所属Region的Remembered Set中。

3.5.7.2 GC模式G1中一共有三种垃圾回收的模式:Young GC、Mixed GC和Full GCYoung GC对象优先在Eden Region中进行分配,当所有Eden Region被耗尽时,会触发一次Young GC,存活的对象会被复制到Survivor Region中,空闲的Region被放入空闲列表中。

Mixed GC当越来越多的对象进入Old Region时,虚拟机会触发一次Mixed GC,回收整个Young Region和部分Old Region,触发时机通过-XX:InitiatingHeapOccupancyPercent=N,则当老年代大小占整个堆的N%时,会触发一次Mixed GC,过程类似于CMS

Full GC如果对象内存分配速度过快,Mixed GC来不及回收导致老年代被填满,会触发一次Full GC,使用Serial Old方式进行垃圾回收3.5.7.3 G1工作过程G1的工作过程如下:初始标记(Initial Marking)

并发标记(Concurrent Marking)最终标记(Final Marking)筛选回收(Live Data Counting and Evacuation)

初始标记阶段仅仅只是标记一下GC Roots能够直接关联的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段的用户程序并发运行的时候,能在正确可用的Region中创建新对象,这个阶段需要暂停线程。

并发标记阶段从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行最终标记阶段则是修正在并发标记阶段因为用户程序的并发执行而导致标记产生变动的那一部分记录,这部分记录被保存在Remembered Set Logs中,最终标记阶段再把Logs中的记录合并到Remembered Set中,这个阶段是并行执行的,需要暂停用户线程。

最后在筛选阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划3.6 内存分配与回收策略对象优先在Eden分配大多数情况下,对象在新生代的Eden区中分配,当Eden区空间不足时,虚拟机会发起一次Minor GC。

大对象直接进入老年代所谓大对象,最典型的就是长字符串和数组长期存活的对象进入老年代对象晋升到老年代的年龄阈值,可以通过-XX:MaxTenuringThreshold设置,默认为15岁动态对象年龄判断如果在Survivor空间中相同年龄的所有对象大小的总和超过Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。

参考1、周志明,深入理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社2、占小狼,G1垃圾收集器介绍

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

河南中青旅行社综合资讯 奇遇综合资讯 盛世蓟州综合资讯 综合资讯 游戏百科综合资讯 新闻77688