参考:https://www.cnblogs.com/andy-zcx/p/5522836.html
https://blog.csdn.net/u012813201/article/details/73793668
一、垃圾回收机制的意义
Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。
二、垃圾回收机制具有以下的特点
1、 垃圾回收机制只负责回收堆内存,不会回收任何物理资源
2、 程序无法精确控制垃圾回收的进行,会在合适的时候进行
3、 在垃圾回收机制回收的任何对象之前,总会先调用它的finalize()方法
三、堆内存
1). 新生代(Young Generation)
新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。新生代内存按照 8:1:1 的比例分为一个eden区和两个survivor(survivor0,survivor1)区,大部分对象在Eden区中生成。在进行垃圾回收时,先将eden区存活对象复制到survivor0区,然后清空eden区,当这个survivor0区也满了时,则将eden区和survivor0区存活对象复制到survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后交换survivor0区和survivor1区的角色(即下次垃圾回收时会扫描Eden区和survivor1区),即保持survivor0区为空,如此往复。特别地,当survivor1区也不足以存放eden区和survivor0区的存活对象时,就将存活对象直接存放到老年代。如果老年代也满了,就会触发一次FullGC,也就是新生代、老年代都进行回收。注意,新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高,不一定等 Eden区满了才触发。
2). 老年代(Old Generation)
老年代存放的都是一些生命周期较长的对象,就像上面所叙述的那样,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。此外,老年代的内存也比新生代大很多(大概比例是1:2),当老年代满时会触发Major GC(Full GC),老年代对象存活时间比较长,因此FullGC发生的频率比较低。
3). 永久代(Permanent Generation)
永久代主要用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如使用反射、动态代理、CGLib等bytecode框架时,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。垃圾回收有两种类型,Minor GC 和 Full GC。
Minor GC:对新生代进行回收,不会影响到年老代。因为新生代的 Java 对象大多死亡频繁,所以 Minor GC 非常频繁,一般在这里使用速度快、效率高的算法,使垃圾回收能尽快完成。
Full GC:也叫 Major GC,对整个堆进行回收,包括新生代、老年代和永久代。由于Full GC需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数,导致Full GC的原因包括:老年代被写满、永久代(Perm)被写满和System.gc()被显式调用等。
四、对象在内存中的状态
1、可达状态:如果一个对象在创建之后,有一个或多个引用指向该对象,那么这个对象就处于可达状态。
2、可恢复状态:程序中,如果一个对象没有任何引用指向它,那么该对象就处于可恢复状态,处于可恢复状态下的对象,垃圾回收器在准备回收垃圾时,调用finalize方法,在finalize方法中,系统有可能重新让一个或多个引用指向该对象,那么这个对象就由可恢复状态变为可达状态。
3、不可恢复状态:垃圾回收器被触发调用finalize方法时,处于内存中的可恢复状态的对象没有重新获取引用,那么该对象就处于不可恢复状态。
五、对象的引用
1、强引用:可以理解为普通的引用,即我们在创建对象时,指向某个对象的引用。 是指创建一个对象并把这个对象赋给一个引用变量。如:
Object object =new Object();
String str ="hello";
2、软引用:由 java.lang.reg.SoftReference实现,就使用软引用的对象,当系统中内存足够,程序运行稳定时,垃圾回收器不会考虑回收该对象,而且程序也可以使用该对象,但是,当系统内存不足,垃圾回收器准备回收对象时,回收器可能将该对象进行回收。
软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。
如:
MyObject aRef = new MyObject();
SoftReference aSoftRef=new SoftReference(aRef);
此时,对于这个MyObject对象,有两个引用路径,一个是来自SoftReference对象的软引用,一个来自变量aReference的强引用,所以这个MyObject对象是强可及对象。随即,我们可以结束aReference对这个MyObject实例的强引用:
aRef = null;
此后,这个MyObject对象成为了软引用对象。
3、弱引用:由 java.lang.reg.WeakReference实现。弱引用与软引用差不多,内存空间充足,垃圾回收动作不会触发时,该对象可以被程序使用,但垃圾回收动作触发时,该引用指向的对象就被回收。并且弱引用的引用级别比软引用要低,就是说系统存在软引用和弱引用,垃圾回收器将首先回收弱引用指向的对象。
4、虚引用:由 java.lang.reg.phantomReference实现。虚引用比较玄幻,就是虚引用类似于没有。当一个对象有虚引用指向他时,该对象和没有引用差不多。所以虚引用的引用级别最低,而且虚引用不能单独使用,必须与引用队列 ReferenceQueue 联合使用。
5、4中引用的引用级别:强引用 > 软引用 > 弱引用 > 虚引用。
六、内存分配与回收策略
Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及 回收分配给对象的内存。一般而言,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓存(TLAB),将按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代中。总的来说,内存分配规则并不是一层不变的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
1) 对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。现在的商业虚拟机一般都采用复制算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。 当进行垃圾回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后处理掉Eden和刚才的Survivor空间。(HotSpot虚拟机默认Eden和Survivor的大小比例是8:1)当Survivor空间不够用时,需要依赖老年代进行分配担保。
2) 大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
3) 长期存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中。
4) 动态对象年龄判定。为了更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
需要注意的是,Java的垃圾回收机制是Java虚拟机提供的能力,用于在空闲时间以不定时的方式动态回收无任何引用的对象占据的内存空间。也就是说,垃圾收集器回收的是无任何引用的对象占据的内存空间而不是对象本身。
再理解两个概念 内存溢出和内存泄露
内存泄漏:分配出去的内存无法回收(不再使用的对象或者变量仍占内存空间),在Java中内存泄漏就是存在一些被分配的对象(可达的,却是无用的)无法被gc回收。
A. Java存在内存泄漏
Java判断内存空间是否符合垃圾回收标准有两个:给对象赋null且不再使用;给对象赋新值,重新分配内存。
内存泄漏的两种情况:一是堆中申请的内存没释放;二是对象已不再使用,但还在内存中保留着。
Gc可以有效的解决第一种情况,但是无法保证情况二,所以Java存在的内存泄漏主要是第二种。
B. 内存泄露的几种场景
1、长生命周期的对象持有短生命周期对象的引用,即静态集合类。例如:在static HashMap中缓存局部变量,且没清空,随时间的推移,这个map会越来越大,造成内存泄露。
2、变量不合理的作用域。
3、没有及时的将对象设置为null
3、各种连接没显示关闭。数据库连接、网络连接、IO连接,没显示的close,会造成很多对象无法回收。
4、监听器。释放对象时没有删除监听器。
C. 避免内存泄漏
1、尽早释放无用对象的引用
2、使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域
3、尽量少用静态变量,因为静态变量存放在永久代(方法区)
4、避免在循环中创建对象
5、开启大型文件或从数据库一次拿了太多的数据很容易造成内存溢出,所以在这些地方要大概计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。
内存溢出:程序要求的内存超出了系统所能分配的范围(比如:栈满还入栈 出现上溢,栈空还出栈 出现下溢)无法申请到足够的内存
1、堆内存溢出(outOfMemoryError:Javaheap space)堆中的内存是用来生成对象实例和数组的。堆内存(新生代老年代,新生代包括1个Eden2个survivor)。当生成新对象时,内存的申请过程如下:
a、jvm先尝试在eden区分配新建对象所需的内存;b、如果内存大小足够,申请结束,否则下一步;
c、jvm启动新生代GC,试图将eden区中不活跃的对象释放掉,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
d、Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
e、 当OLD区空间不够时,JVM会在OLD区进行full GC;
f、full GC后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”:
2.堆内存溢出(OutOfMemoryError)的例子:申请了很多内存,没释放
3.方法区内存溢出(outOfMemoryError:permgem space)方法区主要存放的是类信息、常量、静态变量等。所以如果程序加载的类过多,或者使用反射、gclib等这种动态代理生成类的技术,就可能导致该区发生内存溢出
while(true){
list.add(String.valueOf(i++).intern());
}
永久代的两个参数设置:PermSize, -XX:MaxPermSize
4、线程栈溢出(java.lang.StackOverflowError)线程栈时线程独有的一块内存结构,所以线程栈发生问题必定是某个线程运行时产生的错误。 一般线程栈溢出是由于递归太深或方法调用层级过多导致的。