Java 垃圾回收机制原理

    最近作一个ETL的项目模块,常常因为查询数据量比较大用消息中间件MQ时引发了内存溢出的报错。作完后没事研究了一下JVM和垃圾回收的相关知识点。java

一:垃圾回收机制的意义c++

java  语言中一个显著的特色就是引入了java回收机制,是c++程序员最头疼的内存管理的问题迎刃而解,它使得java程序员在编写程序的时候不在考虑内存管理。因为有个垃圾回收机制,java中的额对象不在有“做用域”的概念,只有对象的引用才有“做用域”。垃圾回收能够有效的防止内存泄露,有效的使用空闲的内存;程序员

说到这,不得不提起内存泄漏(memory leak)和内存溢出(out of memory算法

内存泄漏是指程序在申请内存后,没法释放已申请的内存空间,一次内存泄漏彷佛不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。数据库

内存溢出:指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,可是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。 
服务器

通俗的说就是停车场(堆)保安(gc)让好久不用的废弃车子(无用的对象)从车位上挪走,可是这个车子又没办法挪走。这就是内存泄漏。停车场全部的车位都有车子占用了,再来车子没地了,或者说给你一个小汽车的停车位(int),你非要停一辆高铁(Long),这就是内存溢出。多线程

内存泄露量大到必定程度会致使内存溢出。可是内存溢出不必定是内存泄露引发的。
并发

内存泄漏的分类(按发生方式来分类)app

  1. 常发性内存泄漏。发生内存泄漏的代码会被屡次执行到,每次被执行的时候都会致使一块内存泄漏。
  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操做过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。因此测试环境和测试方法对检测内存泄漏相当重要。
  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者因为算法上的缺陷,致使总会有一块仅且一块内存发生泄漏。好比,在类的构造函数中分配内存,在析构函数中却没有释放该内存,因此内存泄漏只会发生一次。
  4. 隐式内存泄漏。程序在运行过程当中不停的分配内存,可是直到结束的时候才释放内存。严格的说这里并无发生内存泄漏,由于最终程序释放了全部申请的内存。可是对于一个服务器程序,须要运行几天,几周甚至几个月,不及时释放内存也可能致使最终耗尽系统的全部内存。因此,咱们称这类内存泄漏为隐式内存泄漏

  1. 内存溢出缘由: 
    1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据; 
    2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收; 
    3.代码中存在死循环或循环产生过多重复的对象实体; 
    4.使用的第三方软件中的BUG; 
    5.启动参数内存值设定的太小

内存溢出的解决方案: 函数

第一步,修改JVM启动参数,直接增长内存。(-Xms,-Xmx参数必定不要忘记加。)

第二步,检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。

第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。


二:垃圾回收策略



分代的垃圾回收策略,是基于这样一个事实:不一样的对象的生命周期是不同的。所以,不一样生命周期的对象能够采起不一样的回收算法,以便提升回收效率。

年轻代(Young Generation)

1.全部新生成的对象首先都是放在年轻代的。年轻代的目标就是尽量快速的收集掉那些生命周期短的对象。

2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(通常而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,而后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另外一个survivor1区,而后清空eden和这个survivor0区,此时survivor0区是空的,而后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收

4.新生代发生的GC也叫作Minor GC,MinorGC发生频率比较高(不必定等Eden区满了才触发)

年老代(Old Generation)

1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。所以,能够认为年老代中存放的都是一些生命周期较长的对象。

2.内存比新生代也大不少(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

持久代(Permanent Generation)

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,可是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候须要设置一个比较大的持久代空间来存放这些运行过程当中新增的类。

说到持久代,也称永久代,不得不说一句,在jdk新版本中,已经没有了永久代这个区域。

三.GC(垃圾收集器)

新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge

老年代收集器使用的收集器:Serial Old、Parallel Old、CMS

Serial收集器(复制算法)

新生代单线程收集器,标记和清理都是单线程,优势是简单高效。

Serial Old收集器(标记-整理算法)

老年代单线程收集器,Serial收集器的老年代版本。

ParNew收集器(中止-复制算法) 

新生代收集器,能够认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。

Parallel Scavenge收集器(中止-复制算法)

并行收集器,追求高吞吐量,高效利用CPU。吞吐量通常为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。

Parallel Old收集器(中止-复制算法)

Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先

CMS(Concurrent Mark Sweep)收集器(标记-清理算法)

高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择

四:GC的执行机制

因为对象进行了分代处理,所以垃圾回收区域、时间也不同。GC有两种类型:Scavenge GC和Full GC。

Scavenge GC

通常状况下,当新对象生成,而且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,而且把尚且存活的对象移动到Survivor区。而后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。由于大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,因此Eden区的GC会频繁进行。于是,通常在这里须要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC

对整个堆进行整理,包括Young、Tenured和Perm。Full GC由于须要对整个堆进行回收,因此比Scavenge GC要慢,所以应该尽量减小Full GC的次数。在对JVM调优的过程当中,很大一部分工做就是对于FullGC的调节。有以下缘由可能致使Full GC:

1.年老代(Tenured)被写满

2.持久代(Perm)被写满

3.System.gc()被显示调用

4.上一次GC以后Heap的各域分配策略动态变化