尽力做到十全十美~~
JVM运行时数据区域,也叫内存布局。如下图,它由五大部分组成。
首先,我们来简单了解一下内存溢出与内存泄漏。
内存泄漏:我们在栈上占用的空间,方法结束,栈会自动销毁。但我们在堆上申请的空间(使用new,或者C++的malloc开辟的空间),如果没有手动释放,这块空间就会一直被占用,我们也用不了,相当于荒废了,这就可能导致一个严重的问题- - -内存泄漏。若程序24小时运转,这种内存泄漏越来越多,最终没有可用的空间,后果会非常严重。什么是垃圾呢,垃圾指的是不再使用的内容,垃圾回收就是不用的内容帮我们自动释放了。
内存溢出:程序在申请内存时,没有足够的内存空间供其使用,以下是有可能引起内存溢出的原因。
JVM虚拟机中虚拟机栈,程序计数器等是每个线程有一份,线程结束,内存空间会自动回收,这里的垃圾回收,我们主要针对堆上的内容。系统是怎么判断一个对象是否是垃圾的呢?当一块内存的内容我们不再使用,这个内存的内容就是垃圾了。GC回收以对象为单位。
那么GC是通过怎样的方法来判断一个对象是否是垃圾的呢?
关键看,这个对象有没有引用指向它,如果没有引用指向它,这个对象也找不到,没办法再使用了,就可以当作垃圾进行回收。
这种办法是Python,PHP的做法,Java没有使用。
JVM给每一个对象分配了一个计数器,没有一个引用指向它,计数器加一。当计数器为0时,这个对象就可以被回收了。如下图所示.
Test t1 = new Test();//Test对象计数1
Test t2 = t1;//Test对象计数2
Test t3 = t2;//Test对象计数3
一旦对象超出作用域,失效了,计数就会减1.当计数器为0时,回收Test对象.
这种方法虽简单有效,但缺点也很明显,每一个对象都分配计数器,当对象很多的时候,就会占用大量空间.当同一个类里的两个对象互相引用,它们的计数器最少为1,无法被正确释放.
Java中的对象都是通过引用来访问的,经常一个引用,指向一个对象,这个对象的成员又指向别的对象.这种关系,JVM通过链式或者树将他们串起来.
每隔一段时间,从根节点遍历一次,没办法遍历到的,就作为垃圾处理.
在Java中,GCroot对象包含以下几种,虚拟机栈中的本地变量和参数、类静态属性引用的对象、常量池中的引用的对象、JNI 引用等.以上这些对象都是被认为是无需进行垃圾回收的,所以任何一个能够与GC Roots连接的对象都会被视为是存活对象,而不能够连接到GC Roots的对象则被认为是垃圾对象。一个代码中有很多这样的起点,把每个起点都往下遍历一遍,就完成了一次扫描.
标记所有不再使用的对象,标记完成后,统一回收.
如下图,回收之前
回收之后,有很多无法使用的内存碎片.长时间后,会有大量的内存小碎片,如果申请大一点的内存空间,就会失败.
这种算法是把内存分成大小相等的两部分,只用某一半,当内存需要垃圾回收时,把还存活的对象复制到另一半,之后,把这块空间全部释放.
如下图,这是释放之前的
将存货对象复制到右半块,然后,将这左半块空间整个释放掉.
优点是防止了内存碎片,缺点是,每一次只能使用一半的内存空间,空间利用率低,并且,在垃圾少,存活对象多时,需要搬运大量的存活对象,效率低.
为了优化复制算法,标记整理算法的做法是,先标记需要回收的对象,之后,再把存活的对象都移向一端,之后,清理边界之外的内存,如下图所示.
优点是避免内存碎片,但效率依旧不是很高
分代回收,是基于不同类型的对象,来进行不同方法的回收.JVM有一个规律,一个对象,它的生命周期,要么特别短,要么特别长.如果,一个对象它存在的时间很长,那么,他大概率会继续长时间存在下去.
依此,我们给对象引入一个概念- - -年龄,以熬过GC的轮次为单位,经过一轮可达性的遍历,这个对象仍然存在,它的年龄就加1.
如下图,是堆的区域划分.