通过之前的学习我们知道程序计数器、虚拟机栈、本地方法栈这3个区域随着线程而生,随线程而灭。栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的。因此这几个区域的内存分配和回收都具备确定性,无需过多考虑内存回收的问题。而Java堆则是我们关注的重点。
Java中最大的特点在于具备良好的垃圾收集器。GC是JAVA中最重要的安全保证。
整个JVM中的GC的处理机制:对不需要的对象进行标记,而后进行清除。
一. 堆内存划分
note:(1.8之前元空间的位置是永久代,这是最大的变化)。JDK1.8开始,之前的永久代空间取消了。取消永久代的目的是为了将HotSpot与JRocket两个虚拟机标准合成一个,因为以前只有HotSpot才有永久代。
在整个的JVM堆内存之中实际上将内存分为了三块:
- 年轻代:新对象和没达到一定年龄的对象都在年轻代;(比如18岁前)
新生对象在Eden,而年龄达到一定的在SO或者S1,还有伸缩区域可以扩展;
- 年老代:对象被长时间使用的对象。(比如18岁到80岁),老年代的内存空间应该比年轻代的内存空间更大。
- 元空间:像一些方法中的操作临时对象等,直接使用物理内存;
最初的永久代是需要在JVM堆内存里面进行划分;而元空间直接使用物理内存;
为何分区:是为了更好的进行每一块的管理(好比磁盘分区管理),可以确定哪一块内存可以被清空,哪一块不能。
二. GC流程
所有的数据都会保存在JVM的堆内存之中,但是在实际的开发中经常会创建许多的临时对象,也会有一些常驻对象在(比如单例),所以为了保证GC的性能问题,对于GC的处理流程如下图所示:
对于GC流程里面,那么最需要处理的就是年轻代与老年代内存清理操作,而元空间(永久代)都不在GC范围内。其实元空间和永久代差不多,只是一个是内部内存,一个是外部内存。
图的说明如下:
- 当现在有一个新的对象产生(Eden),那么对象一定需要内存空间,于是现在就需要为该对象进行内存空间的申请。
- 首先会判断Eden园区是否有内存空间,如果此时有内存空间,则直接将新对象保存在Eden区域;如果此时Eden内存空间不足,那么他会出发一个Minor GC操作,将Eden的无用内存空间进行清理。(分区的好处不会影响到别的区域);清理之后会继续判断Eden的内存空间是否充足。充足的话则将新的对象直接在Eden进行空间分配;如果执行了Minor GC后,Eden内存任然不足,那么这个时候会进行存活区的内存判断,则会将Eden的部分对象存在存活区,(Eden就会空出一部分空间出来了),那么这个时候又继续判断Eden的空间是否够,够就存。 如果存活区也没有内存空间了,那么就继续判断老年区。如果此时老年区的空间充足,则将存活区中的活跃对象保存到老年代,而后存活区就会出现空余空间,随后Eden将活跃对象保存到存活区之中,从而为Eden开辟内存空间。--------------说明所有的新对象都会在Eden区。
- 如果这个时候老年代也满了,那么这个时候将产生Major GC(Full GC),那么这个时候就会进行老年代的垃圾清理。
- 如果老年代执行了Full GC之后发现仍然无法进行对象的保存,就会产生OOM异常,OutOfMemoryError.
三. JVM内存调整参数(调优关键)
通过之前的分析发现,实际上每一块子内存区域中都会存在有一部分的可变伸缩区域,其基本流程为:如果空间不足,则在可变的空间范围内,扩展内存空间;当一段时间内,发现内存空间不那么紧张了,因此开始收缩内存空间。(因为判断伸缩是花时间的,因此它是调优的关键,我们应该尽量让这个伸缩区域不存在)。
前两个最常用。
在整个堆内存的调整策略之中,有经验的人基本上都只会调整两个参数(前两个):一个叫做最大内存,一个叫做初始化内存。如果让这两个相等,那么就没有可变的范围,性能就可以提升。如果要想取得这些内存的整体信息直接利用Runtime类即可。
上面的单位是字节,最好用M单位来表示,就除以两个1024
根据结果会发现:默认情况下分配的内存是总内存的“1/4”(上面的总内存是32G).而初始化的内存为1/64.那么伸缩区就是491.0M到7276.5M之间,那么现在就有可能造成程序的性能下降。那么最好的做法就是让这两个参数一样大小。那么这个时候就避免了伸缩去的可调策略,从而提升了整个程序的性能。
比如这样:
note:在eclipse里面可以配置的,比如打开Run configuratios----VM arguments
四. 年轻代
所有的新对象都会在年轻代产生。如果年轻代的空间的不足,无法生成对象,则会引发Minor GC和Major GC(Full GC)。
所有使用关键字new新实例化的对象一定会在Eden保存,而存活区保存的一定是已经在Eden中存在好久并且经过了好几次的Minor GC还保存下来的存活对象。那么这个对象将晋升到存活区之中。存活区分为两个存活区,而且这两个存活区一定是相等的大小。目的:一块存活区为了晋升,另外一块为了对象回收。From和to不固定,是会互相换的。这两块内存空间一定有一块是空的。
Minor GC的算法如下图:
在年轻代中使用的是Minor GC,这种GC算法采用的是复制算法。
解释:当Eden空间不足的时候,会把存活对象上升到存活区中,即把绿色放到存活区中的。当存活区中的GC完了以后,将统一把存活对象放到一个存活区中,另外一个存活区就会被清空。
通过以上的分析可以发现,在整个的处理过程中,Eden中大多说都是临时的新对象,可能会发生频繁的Minor GC,所以在HotpSpot虚拟机之中为了加快此空间的内存分配,所以采用了两种技术优化实现:Bump-The-Pointer和Thread-LocalAllocation Buffers.
五. 老年代
老年代主要是接收由年轻代发来的对象。一般是经过了好几次Minor GC后的。如果你要保存的对象超过了Eden大小,那么这个对象可以直接保存到老年代。当老年代内存不足时,将引发Full GC.(Major GC)
老年代中的算法有2个:
在回收清除的过程之中,发现所有在老年代被回收的对象并没有进行空间的整理,所以老年代里面最头太疼的问题就是碎片问题。于是还有第二种算法:
所以:以后在进行老年代存储的时候,尽可能保存长期会被使用的对象,并且不会被轻易回收的大对象的存在。
六. 永久代(1.8之后消失了)-------取而代之的是元空间
虽然SEJDK1.8,但是JavaEE还是JDK1.7呢.
永久代时在堆内存之中保存的,但是永久代不会被回收,比如inter()方法产生的对象,是不会被回收的。如果操作不当,导致永久代中的数据过大,这个时候程序会抛出OOM异常。
七. 元空间
唯一的区别是永久代使用的堆内存空间,而元空间时使用的物理内存,直接受到本地的物理内存限制。
参考文献:
https://blog.csdn.net/SEU_Calvin/article/details/51404589
《深入理解JAVA虚拟机》
李兴华老师的《Java内存模型》