JVM内存管理机制和垃圾回收机制

一个java程序的编译和执行过程如下:

  • .java ——编译——> .class
  • 类加载器负责加载各个字节码文件(.class)
  • 加载完.class后,由执行引擎执行,在执行过程中,需要运行时数据区提供数据

根据 JVM 规范,JVM 内存共分为虚拟机方法区、堆、栈、程序计数器、本地方法栈五个部分。


  • PC寄存器(程序计数器):用于记录当前线程运行时的位置,每一个线程都有一个独立的程序计数器,线程的阻塞、恢复、挂起等一系列操作都需要程序计数器的参与,因此必须是线程私有的。

  • java 虚拟机栈:在创建线程时创建的,用来存储栈帧,因此也是线程私有的。java程序中的方法在执行时,会创建一个栈帧,用于存储方法运行时的临时数据和中间结果,包括局部变量表、操作数栈、动态链接、方法出口等信息。这些栈帧就存储在栈中。如果栈深度大于虚拟机允许的最大深度,则抛出StackOverflowError异常。

    • 局部变量表:方法的局部变量列表,在编译时就写入了class文件
    • 操作数栈:int x = 1; 就需要将 1 压入操作数栈,再将 1 赋值给变量x
  • java 堆:java堆被所有线程共享,堆的主要作用就是存储对象。如果堆空间不够,但扩展时又不能申请到足够的内存时,则抛出OutOfMemoryError异常。

  • 方法区:方发区被各个线程共享,用于存储静态变量、运行时常量池等信息。

  • 本地方法栈:本地方法栈的主要作用就是支持native方法,比如在java中调用C/C++


1.Java与C语言相比的垃圾回收的一个优势?
Java与C语言相比的一个优势是,可以通过自己的JVM自动分配和回收内存空间。

垃圾回收机制是由垃圾收集器Garbage Collection来实现的,GC是后台一个低优先级的守护进程。
在内存中低到一定限度时才会自动运行,因此垃圾回收的时间是不确定的。

 
2.为何要这样设计(GC回收不确定时间,自动运行)?
因为GC也要消耗CPU等资源,如果GC执行过于频繁会对Java的程序的执行产生较大的影响,因此实行不定期的GC。


3.全部都通过GC自动回收吗?
不是,垃圾回收GC只能回收通过new关键字申请的内存(在堆上),但是堆上的内存并不完全是通过new申请分配的。
还有一些本地方法,这些内存如果不手动释放,就会导致内存泄露,所以需要在finalize中用本地方法(nativemethod)如free操作等,再使用gc方法。

4.何为垃圾?
对象之间的引用可以抽象成树形结构,通过树根(GC Roots)作为起点,当一个对象到GC Roots没有任何引用链相连时(不可达),则证明这个对象为可回收的对象。
有一下三种:
(1)栈帧中的本地变量表所引用的对象。
(2)方法区中类静态属性和常量引用的对象。 
(3)本地方法栈中JNI(Native方法)引用的对象。

【垃圾产生的情况举例】
1.改变对象的引用,如置为null或者指向其他对象  
Object obj1 = new Object();  
Object obj2 = new Object();  
obj1 = obj2; //obj1成为垃圾  
obj1 = obj2 = null ; //obj2成为垃圾 

2.引用类型:
强引用:是最难被GC回收的,宁可虚拟机抛出异常,中断程序,也不回收强引用指向的实例对象。
//强引用(指向实例对象,存在堆中)出现内存不够用OutOfMemoryError也不会回收
    Object obj=new Object();

软引用 (SoftReference),在内存不足时,GC会回收软引用指向的对象(软引用可用来实现内存敏感的高速缓存。)
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
       //软引用
   String str="hello yihaha";  
   SoftReference<String> soft=new SoftReference<String>(str);//将强引用转成软引用
   System.out.println(soft.get());


弱引用(WeakReference),不管内存足不足,只要我GC,我都可能回收弱引用指向的对象(。不过由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些弱引用的对象。)
      //弱引用
   WeakReference<String> wReference=new WeakReference<String>(str);
   System.out.println(wReference.get());


3.循环每执行完一次,生成的Object对象都会成为可回收的对象 
 
 for(int i=0;i<10;i++) {  
        Object obj = new Object();  
        System.out.println(obj.getClass());  
    }    
虚引用(PhantomReference ),虚引用必须和引用队列(ReferenceQueue)联合使用。
   当垃圾回收器发现一个对象有虚引用时,首先执行所引用对象的finalize()方法,在回收内存之前,把这个虚引用对象加入到引用队列中,
   你可以通过判断引用队列中是否有该虚引用对象,来了解这个对象是否将要被垃圾回收。
   然后就可以利用虚引用机制完成对象回收前的一些工作。(注意:当JVM将虚引用插入到引用队列的时候,虚引用执行的对象内存还是存在的。但是PhantomReference并没有暴露API返回对象。
  所以如果我想做清理工作,需要继承PhantomReference类,以便访问它指向的对象。)
     //虚引用
   ReferenceQueue<String> queue=new ReferenceQueue<>();
   PhantomReference<String> phantomReference=new PhantomReference<String>(str,queue);
   System.out.println(phantomReference.get());
   
4.类嵌套  
class A{  
   A a;  
}  
A x = new A();//分配了一个空间  
x.a = new A();//又分配了一个空间  
x = null;//产生两个垃圾 


5.线程中的垃圾  

calss A implements Runnable{  
    void run(){  
    //....  
   }  

//main  
A x = new A();  
x.start();  
x=null; //线程执行完成后x对象才被认定为垃圾  


5.如何高效地进行垃圾回收?

  引用计数法
 引用计数法是最经典的一种垃圾回收算法。其实现很简单,对于一个A对象,只要有任何一个对象引用了A,则A的引用计算器就加1,当引用失效时,引用计数器减1.只要A的引用计数器值为0,则对象A就不可能再被使用。
 缺点:无法处理循环引用的问题,因此在Java的垃圾回收器中,没有使用该算法
      引用计数器要求在每次因引用产生和消除的时候,需要伴随一个加法操作和减法操作,对系统性能会有一定的影响。

Mark-Sweep(标记-清除)算法

 标记-清除算法分为两个阶段:标记阶段和清除阶段。
 标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。
 标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发GC。


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


  Mark-Compact(标记压缩)算法
  为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。
  该算法标记阶段标记出所有需要被回收的对象,但是在完成标记之后不是直接清理可回收对象,而是将存活的对象都移向一端,然后清理掉端边界以外的所有内存(只留下存活对象)。


 Generational Collection(分代收集)算法
  核心思想是将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以在不同代的采取不同的最适合的收集算法。
  新生代采用copying算法。但是不是划分成两块一样大小的空间,是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间(from空间,和to空间)(比例8:1:1)每次使用Eden空间和Survivor from空间,当进行回收时,
  将还存活的对象复制到Survivor to空间中,然后清理掉Eden和from空间。在进行了第一次GC之后,Eden区和from被清空,from和to交换角色,下次GC时会将存活对象复制到to空间,如此反复循环。
  新生代回收发生在新生代内存已经满了,或者说剩余内存小于即将new出来的对象的体积的时候。此时会发生新生代GC。   

  老年代采用标记压缩算法。它的优点是 实现简单高效,但是缺点是会给用户 带来停顿
  老年代回收发生在剩余内存无法装载新生代存活的对象的时候和无法装载大对象的时候(大对象直接进入老年代)。
  
 方法区中有 永久代,它用来存储class文件、静态对象、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
   持久代大小通过-XX:MaxPermSize=&lt;N>进行设置。 
  注意:当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,对象年龄达到15时,(或者当 Survivor to空间满了)就会移动到老年代中。一般来说,大对象会被直接分配到老年代,所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组,比如:byte[] data = newbyte[4*1024*1024]。
    
  分区算法
  算法思想:分区算法将整个堆空间划分为连续的不同小区间,每一个小区间都独立使用,独立回收。
  算法优点是:可以控制一次回收多少个小区间
  通常,相同的条件下,堆空间越大,一次GC所需的时间就越长,从而产生的停顿时间就越长。为了更好的控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而 减少一个GC的停顿时间。
 

6.关于收集器介绍?
  对于新生代Copying算法,
    Seria l收集器是 单线程,并且在它进行垃圾收集时,必须 暂停所有用户线程
    ParNew收集器是Serial收集器的 多线程版本,在单CPU甚至两个CPU的环境下,由于线程交互的开销,无法保证性能超越 Serial收集器。
    Parallel Scavenge 新生代的多线程收集器( 并行收集器),它主要是为了达到一个 可控的吞吐量
  对于老年代使用Mark-Compact算法
    Serial Old单线程 ,Serial收集器的老年代版本。
    Parallel Old  多线程可控吞吐量(Parallel Scavenge收集器的老年代版本)
    CMS(Current Mark Sweep)收集器采用的是 标记-清除算法, 内存碎片问题不可避免。是一种 并发低停顿收集器。(可以 使用-XX:CMSFullGCsBeforeCompaction
 设置执行几次CMS回收后,跟着来一次内存碎片整理。)
    G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,充分利用多CPU、多核环境。因此它是一款 并行与并发收集器,并且它能建立 可预测的停顿时间模型

  7.关于GC种类?

   GC其实准确分类只有两大种:
  Partial GC:并不收集整个GC堆的模式
       Young GC(Minor GC):只收集young gen的GC(当young gen中的eden区分配满的时候触发。)
       Old GC( Major GC ):只收集old gen的GC。只有CMS的concurrent collection是这个模式
       Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
    Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。
(young GC准备触发时,统计出young GC中要保存到年老代中的数据大小大于年老代剩余的存储空间时,或者要在持久代中分配空间但是没有足够空间时,便触发full GC)


 整理自:https://blog.csdn.net/seu_calvin/article/details/51892567

https://blog.csdn.net/lojze_ly/article/details/49456255

https://blog.csdn.net/u010429424/article/details/77333311

猜你喜欢

转载自blog.csdn.net/sinat_35821285/article/details/80236377