java面试问题整理-垃圾回收

时间:2021-02-28 13:49:17

  对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象,通过这种方式确定哪些对象是"可达的", 哪些对象是"不可达的"。但是,为了保证GC能够在不同平台实现的问题,Java规范对GC的很多行为都没有进行严格的规定。例如,对于采用什么类型的回收算法、什么时候进行回收等重要问题都没有明确的规定。因此,不同的JVM的实现者往 往有不同的实现算法。(释放对象时,只要将对象所有引用赋值为null)

-----------------------------------------------------------------------------------------------------------------

  垃圾回收回收的对象占据的内存空间而不是对象。

  垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源。当对象在JVM运行空间中无法通过根集合到达(找到)时,这个对象被称为垃圾对象,这个对象就可以被回收了。根集合是由类中的静态引用域与本地引用域组成的,JVM通过根集合索引对象。 在释放对象占用的内存之前,垃圾收集器会调用对象的finalize()方法,一般建议在该方法中释放对象持有的资源。如果对象的引用被置为null,垃圾收集器不会立即释放对象占用的内存,在下一个垃圾回收周期中,这个对象将是可被回收的。调用system.gc()方法会大大的影响系统性能。

--------------------------------------------------------------------------------------------------------------

  一般情况下关注于两中内容:堆内存栈内存。堆内存主要用来存储程序在运行时创建或实例化的对象与变量,例如通过new关键字创建的对象。而栈内存则是用来存储程序代码中声明为静态或非静态的方法。

java面试问题整理-垃圾回收

  Java的堆更像一个传送带,每分配一个新对象,它就往前移动一格。这意味着对象存储空间的分配速度相当快。Java的“堆指针”只是简单地移动到尚未分配的领域。也就是说,分配空间的时候,“堆指针”只管依次往前移动而不管后面的对象是否还要被释放掉。如果可用内存耗尽之前程序就退出就再好不过了,这样的话垃圾回收器压根就不会被激活。

栈是留给JVM自己用的,用来存放类的信息的,它和堆不同,运行期内GC不会释放空间,当超过变量的作用域后,java会自动释放掉为该变量所分配的内存空间:

1、如果程序声明了static的变量,就直接在栈中运行的,进程销毁了,不一定会销毁static变量。
2、在函数中定义的基本类型变量和对象的引用变量都在函数的栈内存中分配

  堆的优势是可以动态分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。缺点就是要在运行时动态分配内存,存取速度较慢。
栈的优势是存取速度比堆要快,缺点是存在栈中的数据大小与生存期必须是确定的无灵活性。

------------------------------------------------------------------------------------------------------------------------------------------------------------

  JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。

---------------------------------------------------------------------------------------------------------------

  在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段: 
  创建阶段;(1)为对象分配存储空间; (2) 开始构造对象;  (3) 从超类到子类对static成员进行初始化; (4)超类成员变量按顺序初始化,递归调用超类的构造方法;(5)子类成员变量按顺序初始化,子类构造方法调用。
     
应用阶段;此阶段的特点:(1)系统维护着对象的>=1个强引用(Strong Reference); (2)所有对该对象的引用全部是强引用(除非我们显示地适用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference)). 

-----------------------

  强引用:一般的引用都是强引用。通过用new 方式创建的对象,并且显示关联的对象。
     软引用:只有当内存不够的时候,才回收这类内存,因此内存足够时它们通常不被回收,如果内存空间不足了,就会回收这些对象的内存。软引用可用来实现内存敏感的高速缓存

  弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。弱引用与软引用的区别在于,只具有弱引用的对象拥有更短暂的生命周期。

  虚引用:虚引用并不会决定对象的生命周期,指一些执行完了finalize函数,并为不可达对象,但是还没有被GC回收的对象。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。这种对象可以辅助finalize进行一些后期的回收工作,我们通过覆盖了Refernce的clear()方法,增强资源回收机制的灵活性。 虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

  在实际程序设计中一般很少使用弱引用和虚引用,是用软引用的情况较多,因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出等问题的产生。

      -------------------
  不可视阶段;
 在其他区域的代码中已经不可以在引用它,其强引用已经消失。
  不可到达阶段;
 (有两种方法来知道这个对象有没有被引用:第一种是遍历堆上的对象找引用;第二种是遍历堆栈或静态存储区的引用找对象。Java采用的是后者)在虚拟机的对象引用根集合中再也找不到直接或间接地强引用,这些对象一般是所有线程栈中的临时变量。所有已经装载的静态变量或者是对本地代码接口的引用。 
  可收集阶段; 
  终结阶段; 
  释放阶段  

<1> 回收器发现该对象已经不可达。 
       <2> finalize方法已经被执行。 
       <3> 对象空间已被重用。

-------------------------------------------------------------------------------------------------------------

垃圾回收算法:

  Mark-Sweep(标记-清除)算法:两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。容易产生内存碎片。

  Copying(复制)算法:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。内存缩减到原来的一半。Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

  Mark-Compact(标记-整理)算法:标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

  Generational Collection(分代收集)算法:它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation,每次垃圾收集时只有少量对象需要被回收)和新生代(Young Generation,每次垃圾回收时都有大量的对象需要被回收),那么就可以根据不同代的特点采取最适合的收集算法。

------------------------------------------------------------------------------------------------------------------

    新生代都采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,但是实际中并不是按照1:1的比例来划分新生代的空间的,一般来说是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden空间和其中的一块Survivor空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。

    老年特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。】

    新生代:新创建的对象都存放在这里。因为大多数对象很快变得不可达,所以大多数对象在年轻代中创建,然后消失。当对象从这块内存区域消失时,我们说发生了一次“minor GC”。

    老年代:没有变得不可达,存活下来的年轻代对象被复制到这里。这块内存区域一般大于年轻代。因为它更大的规模,GC发生的次数比在年轻代的少。对象从老年代消失时,我们说“major GC”(或“full GC”)发生了。

    新生带——>老年代

    年轻代总共有3块空间,其中2块为Survivor区。各个空间的执行顺序如下:

    1.   绝大多数新创建的对象分配在Eden区。
    2.   在Eden区发生一次GC后,存活的对象移到其中一个Survivor区。
    3.   在Eden区发生一次GC后,对象是存放到Survivor区,这个Survivor区已经存在其他存活的对象。
    4.   一旦一个Survivor区已满,存活的对象移动到另外一个Survivor区。然后之前那个空间已满Survivor区将置为空,没有任何数据。
    5.   经过重复多次这样的步骤后依旧存活的对象将被移到老年代。

------------------------------------------------------------------------------------------------------------------------------------

Java虚拟机采用一种“自适应”的垃圾回收技术

Java有两种方找到的存活对象:

  (1)停止-复制: 先暂停程序的运行(所以它不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的全是垃圾。

  (2)标记-清扫:从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当它找到一个存活对象,就会给对象一个标记。这个过程中不会回收任何对象。

  自适应:当垃圾回收器第一次启动时,它执行的是“停止-复制”,因为这个时刻内存有太多的垃圾。然后Java虚拟机会进行监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,就切换到“标记-清扫”方式;同样,Java虚拟机会跟踪“标记-清扫”效果,要是堆空间出现很多碎片,就会切换到“停止-复制”方式。这就是所谓的“自适应”技术。

--------------------------------------------------------------------------------------------------------------------------------------

  • 引用计数法

  每个对象上都有一个引用计数,对象每被引用一次,引用计数器就+1,对象引用被释放,引用计数器-1,直到对象的引用计数为0,对象就标识可以回收。

  • root搜索算法

  这种算法目前定义了几个root,也就是这几个对象是jvm虚拟机不会被回收的对象,所以这些对象引用(强引用)的对象都是在使用中的对象,这些对象未使用的对象就是即将要被回收的对象。简单就是说:如果对象能够达到root,就不会被回收,如果对象不能够达到root,就会被回收。

  以下对象会被认为是root对象:

    •   被启动类(bootstrap加载器)加载的类和创建的对象
    •   jvm运行时方法区类静态变量(static)引用的对象
    •   jvm运行时方法去常量池引用的对象
    •   jvm当前运行线程中的虚拟机栈变量表引用的对象
    •   本地方法栈中(jni)引用的对象

----------------------------------------------------------------------------------------------------------------------------