算法 自动采集列表(Java内存模型-和内存分配以及jdk、jre是什么关系)

优采云 发布时间: 2021-10-13 10:05

  算法 自动采集列表(Java内存模型-和内存分配以及jdk、jre是什么关系)

  如果想了解Java内存模型参考:jvm内存模型-内存分配与jdk、jre、jvm的关系是什么(阿里、美团、京东)

  相信像编辑器这样的程序员在日常工作或者面试中经常会遇到JVM的垃圾回收问题。深夜的JVM垃圾回收机制有详细的了解吗?没时间抚摸也没关系,因为接下来我要抚摸你。

  一、 你需要了解技术背景

  按照套路,需要先安装X,说说JVM垃圾回收的前世今生。说到垃圾采集(GC),大多数人把这项技术看作是 Java 语言的配套产品。事实上,GC 的历史比 Java 更长。早在 1960 年,Lisp 就使用了内存动态分配和垃圾采集技术。设计和优化C++语言的高手们久等了~~

  二、 哪些内存需要回收?

  大家都知道JVM的内存结构包括五个区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中,程序计数器、虚拟机栈、本地方法栈都是用线程产生和熄灭的。所以这些区域的内存分配和回收都是确定性的,所以不需要过多考虑回收的问题,因为当方法结束或者线程结束的时候,内存自然会被回收。Java堆区和方法区是有区别的!(怎么说区别就上口了),这部分内存的分配和回收是动态的,这是垃圾采集器需要注意的部分。

  垃圾回收器在回收堆区和方法区之前,首先要确定这些区域中哪些对象可以回收,哪些暂时不能回收。这就需要一个算法来判断对象是否还活着!(面试官肯定没少问你吧)

  2.1 引用计数算法2.1.1 算法分析

  引用计数是垃圾采集器的早期策略。在这个方法中,堆中的每个对象实例都有一个引用计数。当一个对象被创建时,对象实例被赋值给一个变量,变量计数被设置为1。当任何其他变量被赋值给这个对象的引用时,计数增加1(a=b,那么b + 1) 引用的对象实例的计数器,但是当对象实例的引用超过生命周期或设置为新值时,对象实例的引用计数器减1。任何引用计数器为 0 的对象实例可以被当作垃圾回收处理。当一个对象实例被垃圾回收时,它所引用的任何对象实例的引用计数器都减 1。

  2.1.2 优缺点

  优点:引用计数采集器的执行速度非常快,交织在程序的运行中。更有利于程序不需要长时间中断的实时环境。

  缺点:无法检测循环引用。如果父对象引用了子对象,则子对象又会引用父对象。这样,它们的引用计数永远不会为 0。

  2.1.3 是不是很无聊,来点代码来压制一下惊喜

  public class abc_test {

public static void main(String[] args) {

// TODO Auto-generated method stub

MyObject object1=new MyObject();

MyObject object2=new MyObject();

object1.object=object2;

object2.object=object1;

object1=null;

object2=null;

}

}

class MyObject{

MyObject object;

}

  此代码用于验证引用计数算法无法检测循环引用。最后两句将object1和object2赋值为n​​ull,表示object1和object2所指向的对象不能再被访问,但是因为它们相互引用,它们的引用计数器都不为0,那么垃圾采集器就永远不会了被回收。

  2.2 可达性分析算法

  可达性分析算法是从离散数学中的图论引入的。程序把所有的引用关系看成一个图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点后,继续寻找这个节点,当所有的引用节点都被搜索到时,剩下的节点被认为是未引用节点,即无用节点,无用节点将被判断为可回收对象。

  

  在Java语言中,可以作为GC Roots的对象包括:(京东)

  a) 虚拟机栈中引用的对象(栈帧中的局部变量表);

  b) 方法区中类的静态属性所引用的对象;

  c) 方法区常量引用的对象;

  d) 本地方法堆栈中由 JNI(本地方法)引用的对象。

  该算法的基本思想是以一系列称为“GC Roots”的对象为起点,从这些节点开始向下搜索。搜索所采用的路径称为参考链。当一个对象到达 GC Roots 时,没有任何引用链连接(用图论的话,该对象从 GC Roots 是不可到达的),证明该对象不可用。如图所示,objects object 5、object 6、object 7虽然相互关联,但是对于GC Roots是不可达的,所以会被判断为可回收的对象。

  现在的问题是,可达性分析算法会不会存在对象之间循环引用的问题?答案是肯定的,即对象之间不会有循环引用。GC Root 在对象图之外,它是一个专门定义的“起点”,不能被对象图中的对象引用。

  死或不死(死或不死)

  即使是在可达性分析算法中不可达的对象也不是“必须死”的。此时,他们暂时处于“缓刑”阶段。要真正声明一个对象死亡,它至少要经过两个标记过程:如果该对象经过可达性分析,发现没有连接到GC Roots的引用链,那么它会第一次被标记,并且一个过滤器将被执行。过滤条件是对象是否有必要执行 finapze() 方法。当对象没有覆盖finapze()方法,或者finapze()方法已经被虚拟机调用时,虚拟机将这两种情况都视为“无需执行”。在程序中,你可以通过覆盖finapze()来进行一次“惊心动魄”的自救过程,但这只是一次机会。

  /**

* 此代码演示了两点:

* 1.对象可以在被GC时自我拯救。

* 2.这种自救的机会只有一次,因为一个对象的finapze()方法最多只会被系统自动调用一次

* @author zzm

*/

pubpc class FinapzeEscapeGC {

pubpc static FinapzeEscapeGC SAVE_HOOK = null;

pubpc void isApve() {

System.out.println("yes, i am still apve :)");

}

@Override

protected void finapze() throws Throwable {

super.finapze();

System.out.println("finapze mehtod executed!");

FinapzeEscapeGC.SAVE_HOOK = this;

}

pubpc static void main(String[] args) throws Throwable {

SAVE_HOOK = new FinapzeEscapeGC();

//对象第一次成功拯救自己

SAVE_HOOK = null;

System.gc();

//因为finapze方法优先级很低,所以暂停0.5秒以等待它

Thread.sleep(500);

if (SAVE_HOOK != null) {

SAVE_HOOK.isApve();

} else {

System.out.println("no, i am dead :(");

}

//下面这段代码与上面的完全相同,但是这次自救却失败了

SAVE_HOOK = null;

System.gc();

//因为finapze方法优先级很低,所以暂停0.5秒以等待它

Thread.sleep(500);

if (SAVE_HOOK != null) {

SAVE_HOOK.isApve();

} else {

System.out.println("no, i am dead :(");

}

}

}

  操作结果如下:

  finapze mehtod executed!

yes, i am still apve :)

no, i am dead :(

  2.3 Java中的引用你了解多少

  无论是通过引用计数算法判断对象的引用次数,还是通过可达性分析算法判断对象的引用链是否可达,都与“引用”有关,以确定对象是否为活。在Java语言中,引用分为强引用、软引用、弱引用和幻引用四种。这四次引用的强度逐渐减弱。无论是通过引用计数算法判断对象的引用次数,还是通过可达性分析算法判断对象的引用链是否可达,都与“引用”有关,以确定对象是否为活。在 JDK 1.2 之前,Java中引用的定义非常传统:如果引用类型数据中存储的值代表另一块内存的起始地址,则称这块内存代表一个引用。在JDK1.2之后,Java扩展了引用的概念,将引用分为强引用、软引用、弱引用和幻像引用。有四种类型,这四种类型的引用强度逐渐降低。

  程序代码中常见的,类似于Object obj = new Object()引用,只要强引用还存在,垃圾回收器就永远不会回收被引用的对象。

  用于描述一些有用但不是必需的对象。对于软引用关联的对象,在系统出现内存溢出异常之前,会将这些对象列在恢复范围内进行二次恢复。如果本次采集后内存不足,会抛出内存溢出异常。

  它也用于描述非本质对象,但其强度比软引用弱。与弱引用关联的对象只能存活到下一次垃圾回收发生。垃圾回收器工作时,无论当前内存是否足够,都会回收那些只是弱引用的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。比如threadlocal

  它也被称为幽灵引用或幻影引用(这个名字真的很神奇),它是最弱的一种引用关系。一个对象是否有幻影引用根本不会影响它的生命周期,不可能通过幻影引用获得一个对象实例。它的功能是当这个对象被采集器回收时接收系统通知。. 在JDK1.2之后,提供了PhantomReference类来实现虚引用。

  不要被概念吓到,不要着急,你没有跑题,深入就难说了。小编列出这四个概念的目的是为了说明引用计数算法和可达性分析算法都是基于强引用的。

  软引用示例:

  package jvm;

import java.lang.ref.SoftReference;

class Node {

pubpc String msg = "";

}

pubpc class Hello {

pubpc static void main(String[] args) {

Node node1 = new Node(); // 强引用

node1.msg = "node1";

SoftReference node2 = new SoftReference(node1); // 软引用

node2.get().msg = "node2";

System.out.println(node1.msg);

System.out.println(node2.get().msg);

}

  输出是:

  node2

node2 

  2.4 对象死亡前的最后挣扎(并被回收)

  即使是在可达性分析算法中不可达的对象也不是“必须死”的。此时,他们暂时处于“缓刑”阶段。要真正声明一个对象死亡,至少需要两个标记过程。

  第一次标记:如果对象在可达性分析后发现没有连接到GC Roots的引用链,则进行第一次标记;

  第二个标记:在第一个标记之后,将执行过滤。过滤条件是这个对象是否有必要执行finalize()方法。如果在 finalize() 方法中没有重新建立与引用链的关系,则会进行第二次标记。

  第二次成功标记的对象将真正被回收。如果对象在 finalize() 方法中重新建立与引用链的关联关系,它将逃脱这种回收并继续生存。猿猴还在,嘿嘿。

  2.5 如何判断方法区是否需要回收

  猿类,对方法区存储的内容是否需要回收的判断是不同的。方法区回收的主要内容是:丢弃的常量和无用的类。过时的常量也可以通过引用可达性来判断,但是对于无用的类,必须同时满足以下三个条件:

  关于类加载的原理,也是阿里面试的主角。面试官还问我例如:我可以自己定义String吗,答案是否定的,因为jvm在加载类的时候会进行父母委托。

  原理请参考:Java类加载机制(阿里面试题)

  说了半天,主角终于要上台了。

  如何确定垃圾对象

  几乎所有的对象实例都存储在 Java 堆中。在垃圾采集器回收堆之前,它首先需要确定哪些对象还“活着”,哪些对象已经“死了”,即不会以任何方式使用的对象。

  三、常用的垃圾回收算法

  3.0 引用计数方法

  引用计数方法实现简单,效率高,在大多数情况下是一种很好的算法。原理是:给对象添加一个引用计数器。每当有对象引用时,计数器加1,引用无效时,计数器减1,当计数器值为0时,不再使用该对象。需要注意的是,引用计数方法难以解决对象之间相互循环引用的问题,主流Java虚拟机也没有使用引用计数方法来管理内存。

  public class abc_test {

public static void main(String[] args) {

// TODO Auto-generated method stub

MyObject object1=new MyObject();

MyObject object2=new MyObject();

object1.object=object2;

object2.object=object1;

object1=null;

object2=null;

}

}

class MyObject{

MyObject object;

}

  3.1 标记-扫描算法(Mark-Sweep)

  这是最基本的垃圾采集算法。之所以是最基本的,是因为它最容易实现,也是最简单的想法。标记清除算法分为标记阶段和清除阶段两个阶段。标记阶段的任务是标记所有需要回收的对象,清理阶段的任务是回收被标记对象占用的空间。具体流程如下图所示:

  

  从图中不难看出,mark-sweep算法更容易实现,但是有一个比较严重的问题,就是容易出现内存碎片。分片过多可能会导致后续进程需要为大对象分配空间时找不到足够的空间。空间并提前触发新的垃圾采集。

  mark-sweep算法使用GC Roots扫描来标记幸存的对象。标记完成后,对整个空间中未标记的物体进行扫描回收,如下图所示。mark-sweep算法不需要移动对象,只处理非存活对象,当存活对象较多时效率极高,但由于mark-sweep算法直接回收非存活对象,会造成内存占用碎片化。

  

  3.2 复制算法(Copying)

  为了解决Mark-Sweep算法的不足,提出了Copying算法。它根据容量将可用内存分成大小相等的两块,一次只使用其中一块。当这块内存用完时,将幸存的对象复制到另一个块中,然后立即清除已用的内存空间,这样就不容易发生内存碎片。具体流程如下图所示:

  

  该算法虽然实现简单,运行效率高,不易出现内存碎片,但由于可以使用的内存减少到原来的一半,因此在内存空间的使用上付出了高昂的代价。

  显然,Copying 算法的效率与幸存对象的数量有很大关系。如果存活对象较多,则Copying算法的效率会大大降低。

  提出复制算法,克服句柄开销,解决内存碎片问题。一开始,它将堆划分为一个对象表面和多个自由表面。程序从物体表面为物体分配空间。当对象满时,基于复制算法的垃圾回收从根集(GC Roots)开始扫描活动对象,并将每个活动对象复制到空闲表面(这样占用的内存之间没有空闲空洞)活动物体),这样自由面变成物体面,原物体面变成自由面,程序会在新物体面上分配内存。

  

  3.3 Mark-compact 算法(Mark-compact)

  为了解决Copying算法的不足,充分利用内存空间,提出了Mark-Compact算法。这个算法的标记阶段和Mark-Sweep一样,但是标记完成后,并不是直接清理可回收的对象,而是将所有幸存的对象移到一端(美团面试题,记住标记完成后,做先不清理。先移动再清理回收的对象),再清理结束边界外的内存(美团提问)

  标记-组织算法使用与标记-清除算法相同的方法来标记对象,但清理方式不同。回收非存活对象占用的空间后,所有存活的对象将被移动到左边的空闲空间,并更新相应的对象。指针。mark-arrangement算法基于mark-clear算法,移动对象,所以成本较高,但解决了内存碎片问题。具体流程如下图所示:

  

  3.4代采集算法分代采集算法

  分代采集算法是目前大多数 JVM 垃圾采集器使用的算法。它的核心思想是根据对象生存的生命周期将内存划分为几个不同的区域。一般来说,堆区分为Tenured Generation和Young Generation,堆区之外还有一个代,就是Permanet Generation(永久代)。老年代的特点是每次垃圾采集只需要采集少量对象,而新生代的特点是每次垃圾采集需要采集大量对象,所以最适合可根据不同世代的特点采取采集。算法。

  目前大部分垃圾回收器对新生代采用Copying算法,因为新生代每次垃圾回收都会回收大部分对象,这意味着需要复制的操作次数较少,但在实践中是不是按照1:1的比例来划分新生代空间的,一般是将新生代划分为一个较大的Eden空间和两个较小的Survivor空间(通常是8:1:1),每次使用Eden空间) 用其中一个Survivor空间,回收时,将Eden和Survivor中的幸存对象复制到另一个Survivor空间,然后清理Eden和之前使用过的Survivor空间。

  但是由于老年代的特点是每次回收的对象很少,所以一般采用Mark-Compact算法。

  

  3.4.1 Young Generation的回收算法(Recycling主要基于Copying)

  a) 所有新生成的对象首先被放入年轻代。年轻代的目标是尽快采集生命周期较短的对象。

  b) 新生代内存按照8:1:1的比例划分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区和两个Survivor区(一般来说)。大多数对象在Eden区生成,回收时将eden区的survivor对象复制到一个survivor0区,然后清空eden区,当survivor0区也满时,将eden区和survivor0区的survivor对象复制到另一个,然后清掉eden和survivor0区,这时候survivor0区是空的,再交换survivor0区和survivor1区,也就是留survivor1区空着(美团面试,问题太详细了,为什么留survivor1为空,答案:为了让eden和survivor0交换存活对象),如此来回。当 Eden 空间不足时,会触发 jvm 发起 Minor GC

  c) 当survivor1区不够存放eden和survivor0的survivor对象时,直接将survivor对象存入老年代。如果老年代满了,就会触发Full GC(Major GC),即新生代和老年代都被回收。

  d) 新生代发生的GC也叫Minor GC,MinorGC的频率比较高(不一定要等Eden区满才触发)。

  3.4. 2年老年代(Old Generation)回收算法(回收主要基于Mark-Compact)

  a) 在年轻代中存活 N 次垃圾回收的对象将被放置在年老代中。因此,可以认为老年代存储的对象是长寿命对象。

  b) 内存也比新生代大很多(约1:2)。当老年代内存满时,触发Major GC,即Full GC。Full GC的频率为相对较低,老年代的对象存活时间较长,比例较高。

  3.4.3 永久代的回收算法(即方法区)

  用于存放静态文件,比如Java类、方法等。持久化生成对垃圾回收没有明显影响,但是有些应用可能会动态生成或调用一些类,比如Hibernate等。这时候一个比较大的持久化运行时需要设置生成空间来存储这些新添加的类。持久代也称为方法区,具体恢复见上文第5节。

  再写一遍:

  方法区存储的内容是否需要回收的判断不同。方法区回收的主要内容是:丢弃的常量和无用的类。过时的常量也可以通过引用可达性来判断,但是对于无用的类,必须同时满足以下三个条件:

  5 新生代和老一代的区别(阿里面试官提问):

  **所谓的年轻代和老年代是为分代采集算法定义的,新生代分为Eden和Survivor两个区域。把老年加到这三个区。数据会先分配到Eden区(当然也有特殊情况,如果是大对象,会直接放到老年代(大对象是指需要大量连续的java对象)内存空间)),当Eden没有足够空间时会触发jvm发起Minor GC。如果对象在 Minor GC 中幸存下来并且可以被 Survivor 空间接受,它将被移动到 Survivor 空间。并将其年龄设置为1,对象在Survivor中每存活一次Minor GC,年龄加1。当年龄达到一定级别时(默认为15),它将被提升到老年。当然,提升到老年代的年龄是可以设置的。如果老年代满了,执行:Full GC不是经常执行,所以使用Mark-Compact算法清理

  其实新生代和老年代都是对对象进行分区存储,更方便回收等**

  跟上,猿人,离优惠不远了!!!

  四、普通垃圾采集器

  下图显示了 HotSpot 虚拟机中收录的所有采集器。图片是从这里借来的:

  

  五、什么时候触发GC?(最常见的面试问题之一一)

  由于对象是分代处理的,垃圾回收的区域和时间也不同。GC 有两种类型:Scavenge GC 和 Full GC。

  5.1 Scavenge GC

  一般情况下,当有新对象产生,在Eden区申请空间失败时,会触发Scavenge GC对Eden区进行GC,移除不存活的对象,并将存活的对象移动到Survivor区。然后组织Survivor的两个区域。这种GC方式是在年轻代的Eden区进行的,不会影响到老年代。因为大部分对象都是从Eden区开始的,而且Eden区不会分配的很大,所以Eden区的GC会频繁的执行。因此,这里一般需要使用快速高效的算法,才能让Eden尽快自由。

  5.2 全 GC

  对整个堆进行排序,包括 Young、Tenured 和 Perm。Full GC需要回收整个堆,所以比Scavenge GC慢,所以要尽量减少Full GC的次数。在调优JVM的过程中,很大一部分工作是Full GC的调整。以下原因可能会导致 Full GC:

  a) 任期已满;

  b) 持久代(Perm)被填充;

  c) System.gc() 被显式调用;

  d) Heap 域分配策略在最后一次 GC 后动态变化;

  参考:看看JVM的垃圾回收机制,你准备好迎接下一次面试了吗?

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线