A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

© 不二晨 金牌黑马   /  2019-2-15 10:09  /  519 人查看  /  2 人回复  /   1 人收藏 转载请遵从CC协议 禁止商业使用本文

1. 为什么要了解GC呢?

我们造,在C, C++中,new一个对象,会动态申请内存,如果用完之后不回收这块内存,那么这种垃圾内存空间就会越滚越多,最后导致系统扛不住。而Java呢,有自己的垃圾回收机制,可以比较省心地管理好内存,程序员帅锅镁铝们就可以专心地在上面码功能了。但是,JVM回收是有条件的,对象不可达,如果在编写代码的时候,本来能释放的对象被一直引用着,内存得不到释放,就会造成内存泄露,最后我会说一下内存泄漏的场景和注意事项。了解GC的原理可以让我们写出更优质的代码,在一些涉及到内存回收问题的时候,能有更好的解决方案。



2. GC会对哪些区域进行回收?

再来看看这张图:

2018121920211425.jpg

看过上一章JVM内容的同学都知道,Java栈,本地方法栈,程序计数器都是线程私有的,用完之后会自动释放。而作为GC重灾区,Java堆是不可避免的被回收区域,以及方法区,虽然方法区被称为“永久代”,但是HotSpot虚拟机的设计团队选择把GC分代收集扩展至了方法区,比如常量池的常量回收,类的回收等。



3. GC的分代理论。

20181220194411741.png

打断下:方法区在堆中?方法堆的别称不是“非堆”吗?

三种情况:

1、 java7之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变;

2、 java7中,static变量从永久代移到堆中;

3、 java8中,取消永久代,方法区存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可认为在堆中
这样就能理解了。

继续捋,可以看出,一共有三代:新生代,老生代和永久代。新生代又分成:Eden Region, From Survivor Region, To Survivor Region,内存占比是 8:1:1。而方法区就是永久代。


20181220205157214.png

再来看看上面这张图,理解下分代的概念:

(1)新生代

大多数情况下,对象在新生代Eden区中分配,新生代的目标就是尽可能快速的收集掉那些生命周期短的对象。大部分对象在Eden区中生成,当Eden区满的时候,还活着的对象会被复制到两个Survivor区的其中一个区,当这个Survivor满的时候,此区的存活对象将被复制到另外一个Survivor区,当最后这个Survivor区也满的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老代”。两个Survivor区是没有先后关系的,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到年老代的只有从第一个Survivor区过来的对象。新生代中的垃圾回收频率高,且回收的速度也较快。

(2)年老代

那么再说说年老代,在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的
都是一些生命周期较长的对象。如果年老代的空间也被占满,当来自新生代的对象再次请求进入年老代时就会报OutOfMemory异常,比如申请一个需要很大内存的对象。年老代的垃圾回收频率低,且回收的速度慢。

(3)永久代

就GC回收机制而言,JVM内存模型中的方法区更被人们倾向的称为永久代(Perm Generation),保存在永久代中的对象一般不会被回收。永久代进行垃圾回收的频率较低,速度也较慢。永久代的垃圾收集主要回收废弃常量和无用类。以String常量"abc"为例,当我们声明了此常量,那么它就会被放到运行时常量池中,如果在常量池中没有任何对象对"abc"进行引用,那么"abc"这个常量就算是废弃常量而被回收;判断一个类是否"无用",则需同时满足三个条件:

* 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;

* 加载该类的ClassLoader已经被回收;

* 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

虚拟机可以对满足上述3个条件的无用类进行回收,这里说的是可以回收而不是必然回收。

20180413104616831.png

总结下:大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC;同理,当年老代中没有足够的内存空间来存放对象时,虚拟机会发起一次Major GC/Full GC。只要年老代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full CG。



4. 如何判断一个对象是否活着?

(1)引用计数算法(Reference Counting)            

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的,这就是引用计数算法的核心。客观来讲,引用计数算法实现简单,判定效率也很高,在大部分情况下都是一个不错的算法。但是Java虚拟机并没有采用这个算法来判断何种对象为死亡对象,因为它很难解决对象之间相互循环引用的问题。

class A {

    private B b;

    public B getB() {
        return b;
    }

    public void setB(B b) {
        this.b = b;
    }
}

class B {

    private A a;

    public A getA() {
        return a;
    }

    public void setA(A a) {
        this.a = a;
    }
}

public class Test {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        a.setB(b);
        b.setA(a);
    }
}
(2)可达性分析算法(Reachability Analysis)

这是Java虚拟机采用的判定对象是否存活的算法。通过一系列的称为“GC Roots"的对象作为起始点,从这些结点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。可作为GC Roots的对象包括:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈JNI引用的对象。



在上图可以看到GC Roots左边的对象都有引用链相关联,所以他们不是死亡对象,而在GCRoots右边有几个零散的对象没有引用链相关联,所以他们就会别Java虚拟机判定为死亡对象而被回收。



5. 关于finalize()函数。

Java虚拟机在进行死亡对象判定时,会经历两个过程。除了第4个点GC Roots没有任何引用链相连时,这个对象会被JVM进行第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行finalize()方法,如果当前对象没有覆盖该方法,或者finalize方法已经被JVM调用过都会被虚拟机判定为“没有必要执行”。如果在finalize方法中该对象重新与引用链上的任何一个对象建立了关联,即该对象连上了任何一个对象的引用链,例如this关键字,那么该对象就会逃脱垃圾回收系统;但是尽量不要去重finalize()函数,因为它不一定会被调用,而且付出的代价比较大。



6. 垃圾收集算法。

(1) 标记-清除算法:

最基础的垃圾收集算法,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。

标记-清除算法的缺点有两个:首先,效率问题,标记和清除效率都不高。其次,标记清除之后会产生大量的不连续的内存碎片,空间碎片太多会导致当程序需要为较大对象分配内存时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

(2) 复制算法:

将可用内存按容量分成大小相等的两块,每次只使用其中一块,当这块内存使用完了,就将还存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉。这样使得每次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

复制算法的缺点显而易见,可使用的内存降为原来一半。

(3) 标记-整理算法:

标记-整理算法在标记-清除算法基础上做了改进,标记阶段是相同的标记出所有需要回收的对象,在标记完成之后不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,在移动过程中清理掉可回收的对象,这个过程叫做整理。

标记-整理算法相比标记-清除算法的优点是内存被整理以后不会产生大量不连续内存碎片问题。

复制算法在对象存活率高的情况下就要执行较多的复制操作,效率将会变低,而在对象存活率高的情况下使用标记-整理算法效率会大大提高。

(4) 分代收集算法:

根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。

现在的Java虚拟机就联合使用了分代复制、标记-清除和标记-整理算法。

新生代使用复制和标记-清除垃圾收集算法:将新生代划分为容量大小相等的两部分内存,而是将新生代分为Eden区,Survivor from和Survivor to三部分,其占新生代内存容量默认比例分别为8:1:1。Java虚拟机对新生代的垃圾回收称为Minor GC,次数比较频繁,每次回收时间也比较短。

年老代使用标记-整理垃圾收集算法:年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记-整理垃圾回收算法。Java虚拟机对年老代的垃圾回收称为MajorGC/Full GC,次数相对比较少,每次回收的时间也比较长。

java虚拟机内存中的方法区在Sun HotSpot虚拟机中被称为永久代,是被各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。永久代垃圾回收比较少,效率也比较低,但是也必须进行垃圾回收,否则会永久代内存不够用时仍然会抛出OutOfMemoryError异常。



7. GC选择之串行收集与并行收集。

了解下并行和并发的概念:

普通解释:
并行:同时做不同事情的能力。
并发:交替做不同事情的能力。


专业术语:
并行:不同的代码块同时执行。
并发:不同的代码块交替执行。
(1)并行Parallel

多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

多个线程执行垃圾回收。
适合于吞吐量的系统,回收时系统会停止运行。
(2)并发Concurrent

指用户线程与垃圾收集线程同时执行(但并不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

* 系统和垃圾回收一起执行,系统不会暂停。

* 适合于响应要求高的系统,回收时系统不会停止运行。



8. 谈谈Java内存泄漏。

造成内存泄漏的几种情况:

(1) 静态集合类引起内存泄漏

像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。

(2) 当集合里面的对象属性被修改后,再调用remove()方法时不起作用。

(3) 监听器

在释放对象的时候却没有去删除这些监听器,增加了内存泄漏的机会。

(4) 各种连接

比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。

(5) 内部类和外部模块的引用

内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如: public void registerMsg(Object b); 这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。

(6) 单例模式

不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏。特别是在android中,如果持有Activity引用,造成的内存泄漏是很大的,因为Activity是带有界面的组件。



9. 关于System.gc().

建议垃圾回收器进行回收,但不一定会马上执行。尽量少用,System.gc()调用只是建议JVM执行年老代GC,而年老代GC触发FULL GC,JVM会根据系统条件决定是否执行FULL GC,FULL GC又慢又长啊!所以尽量在编码时管理好对象,避免内存泄漏,不用的对象置为null。


---------------------
【转载,仅作分享侵删】
作者:况众文
原文:https://blog.csdn.net/u014294681/article/details/85134866
版权声明:本文为博主原创文章,转载请附上博文链接!

2 个回复

倒序浏览
看一看。
回复 使用道具 举报
今天也要加油鸭
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马