引用有哪些类型?
强引用:通过new创建出来的对象。只要强引用存在,垃圾回收器将不会回收。
软引用:通过SoftReference实现软引用,系统将要发生内存溢出之前才会对这些对象进行回收。
弱引用:通过WeakReference实现弱引用,无论当内存足够,GC运行时都会进行回收。
虚引用:通过PhantomReference实现,通过虚引用无法回去对象的实例,虚引用的作用就是当此对象被回收时,会收到一个系统通知。
如何判断一个对象是否应该被回收?
1、引用计数算法,每当有一个地方引用他,就加1,引用失效,就减1.数量为0,说明可以进行回收。
引用计数算法的缺点就是无法判断两个对象之间存在相互引用的情况。
jvm中没有使用这种判断方式
2、可达性分析算法,通过一系列GCRoot对象作为起始点,从这些点开始向下搜索,搜索的路径成为引用链,当一个对象到GC没有任何引用链,说明对象可以被回收。
GCRoot对象有哪些?
(1)虚拟机栈的栈帧中引用的对象。
(2)方法区中静态属性引用的对象。
(3)方法区中常亮引用的对象。
(4)本地方法栈中jni引用的对象。
枚举根节点时候需要GC停顿,保证分析结果的准确性。
使用GCRoot枚举根节点,由于在整个方法区进行枚举会耗费时间。如何解决?
执行准确式GC并不需要检查执行上下文中所有的引用的位置,在Hotspor中通过OopMap的数据结构来达到这个目的。在类加载完成的时候,虚拟机会将对象内什么偏移量什么数据计算出来,在JIT编译的时候,会在特定的位置记住栈和寄存器中什么地方存在引用,GC直接对这些引用进行扫描。
什么是安全点?
OopMap虽然可以进行快速准确的进行GC Root枚举,但是由于虚拟机的指令太多,如果为每个指令都生成对应的OopMap会浪费大量的空间,所以虚拟机会在特定的位置生成OopMap,这些特定的位置称作安全点。所以程序不是在任何时候都能够进行GC,只有到达安全点才能进行GC。
安全点如何选定?
依据是否能够让程序长时间的运行为特点进行选定,由于每条指令的运行时间都十分短,所以一般选用的点为方法的调用,循环跳转,异常跳转等。
发生GC时需要线程停顿,如何让线程在发生GC时候跑到最近的安全点停顿下来?
1、抢先式中断:发生GC时中断所有的线程,如果发现某条线程不在安全点,就恢复此线程让他跑到安全点上。(虚拟机一般不使用)
2、主动式中断:不对线程进行操作。设置一个标志位,让所有的线程去轮询标志位,发现标志位为真,就自动挂起。轮询标志的地方和安全点是重合的,再加上创建对象需要分配内存的地方。
什么是安全区域?
安全点解决了GC问题,但是当发生GC的时候线程处于sleep状态,此时线程无法响应中断请求。此时需要使用安全区域进行解决。安全区域就是代码片段中引用关系不会发生变化的地方。当线程执行到安全区域的时候,会对线程进行标记,发生GC时候,jvm不管这些线程,在GC的时候,如果这些线程要离开安全区域,此时,判断jvm是否已经完成GC,如果完成,则线程执行,如果没有完成,则线程停顿等待GC完成的信号。(当线程发生sleep时正处于安全区域)
可达性算法不可达的对象就一定会被回收吗?
不一定,当发现对象不可达的时候,将会对此对象进行第一次标记,对标记的对象就行筛选,筛选的条件是是否有必要执行finalize()方法。
当此对象已经调用过finalize()方法或者在对象中没有覆盖finalize()方法,则判定次对象没有必要执行finalize()方法。
没有必要执行finalize()方法的对象将会直接被回收。
有必要执行finalize()方法的对象放在一个队列中,之后有虚拟机创建一个低优先级的线程去出发队列中的对象的finalize()方法,注意此处为触发并非等待finalize()执行结束,防止finalize()方法中出现死循环导致回收系统崩溃。
当一个对象的finalize()方法执行结束后,方法并没有被回收,稍后会对队列中的对象进行二次标记,此时标记的依据是对象是否可达。如果还是不可达,才会将此对象放入即将回收的集合。所以finalize()方法中如果为对象添加引用链,可以拯救此对象。
注意:每个对象的finalize()方法只会被jvm调用一次,如果一个对象在第一次执行finalize()时候被拯救,在下次执行回收会直接对对象就行回收,将不会调用对象的finalize()方法。
方法区中没有垃圾回收?
方法区有垃圾回收,但是回收的效率低。
方法区只要回收废弃的常量和无用的类。
如果没有任何地方对此常量进行引用,则此常量就会被回收。
方法区中哪些类是无用的类?
(1)java堆中不存在该类的任何实例。
(2)加载该类的ClassLoader已经被回收。
(3)该类的class对象没有任何地方被引用。
满足以上三个条件的类可以被回收,而不是和java堆中的对象一样必然会被回收。
对象的创建是如何创建的(new)?:
当虚拟机遇到new指令的时候,先去检查这个指令的参数是否能够在常量池中定位到一个符号的引用(类的全限定名),在判断这个符号引用代表的类是否已经被加载,如果没有被加载,将会进行类的加载操作。通过加载检查后,将对类分配内存。
对象创建时内存分配方式?
1、指针碰撞,对象需要多大的内存在类加载完成的时候就已经确定,此内存分配方式相当于,将已分配的内存放在放在一边,为分配的内存放在另一边,中间用指针进行隔离,当需要分配内存的时候只需要移动指针即可。
2、空闲列表,即维护一个列表,记录内存中还没有分配的内存位置。
分配内存的过程并非是线程安全的解决方案。
1、失败重试。
2、为每个线程分配本地线程缓冲区(TLAB),当TLAB使用完之后分配新的TLAB的时候,实行同步锁定。分配结束后,将分配的内存除对象头外,其余初始化为0值。如果使用TLAB,则初始化为0值提前到TLAB分配时执行。
GC对内存的分配。
内存一般分为新生代和老年代,新生代又分为一块较大的Eden和两块较小的Survivor区域(HotSpot虚拟机E:S=8:1),
新生的对象优先分配在新生代的E区,如果启用本地线程缓冲,优先在TLAB上进行分配,少数情况也会直接在老年代进行对象的分配,当在E分配对象发现内存不够使用的时候,会发生新生代的GC将E区对象转入S区,当发现S区无法存放时,通过分配担保将对象转入老年代。
对象进入老年代的判定。
大对象直接进入老年代(阀值可以通过参数进行设定),为了避免E区和S区之间发生大量的内存复制。
长期存活的对象进入老年代,虚拟机给每个对象定义了一个年龄计数器,在E区中的对象经过一次GC仍然存活并能够被S区容纳,设置此对象的年龄为1,在S区中的对象,每熬过一次GC,就将年龄加1,当年龄达到一定的程度(默认是15,可以通过参数进行设置)就会进入老年代,
动态年龄判断,不一定只有年龄达到阀值才会进入老年代,当相同年龄的对象的总大小大于S空间的一半,则大于等于这个年龄的对象将会进入老年代。
什么是空间分配担保?
当新生代发生GC的时候,先判断老年代中的剩余空间的总大小是否大于新生代中的总对象的大小,如果是,则发生进行新生代的GC,如果不是,则判断是否允许担保失败,如果不允许,则进行老年代的GC,如果允许,则判断老年代中的剩余空间的总大小是否大于新生代每次发生GC时进入老年代的对象的平均值,如果是,则进行新生代的GC,如果不是,则进行一次老年代的GC。
垃圾回收算法有那些?
1、标记-清除:标记所有需要被回收的对象,然后回收。次算法效率低,并且产生内存碎片,由于老年代中存活的对象多,在老年代中进行使用。
2、复制算法:将内存划分为等大小的两块,每次使用其中的一块,回收时,将存活的对象复制到没有使用的一块内存中,然后对使用的内存一次性就行清理。实现简单,运行高效,但是存在大量内存的浪费。由于新生代中存活的对象少,新生代中使用这种算法将E区存活的对象复制到S区。
3、标记整理算法:让所有存活的对象往一侧移动,然后清楚另一侧。在老年代中使用这种算法,避免产生内存碎片。
文中有些和GC无关的知识,如对象的创建,引用的类型等,知识为了让读者更好的了解GC,如果有疑问可以留言随时讨论。