JVM 垃圾回收
垃圾回判断方法:
引用计数算法
简单来说就是给对象加一个引用计数器,每当对象被引用时+1,取消引用的话就-,当这个对象的计数器为0时
就会判断该对象是可回收对象.
有点:算法简单
缺点:无法解决对象间的相互引用,还有就是额外引用了计数器,浪费资源
可达性分析法
可达性分析法呢就是通过一系类的GCroots对象,由他们去向下探索,经过的路径叫做引用链,经过一次探索,没有在引用链上的对象会被标记成不可达对象,一次探索并不能确定是否是可回收对象,一般经过两次探索还没有与引用链建立关联的话就会被判定为可回收对象进行垃圾回收.
GCroot 对象有哪些:
Java栈中的引用对象
本地方法区中的引用对象
方法区中的引用常量
方法区中的静态引用常量
垃圾回收算法以及应用场景
垃圾回收的三种算法 标记清除算法 复制算法 标记整理算法
标记清除算法:
首先说一下标记清除算法,标记清除算法的话,可以将它分为标记和清理两个阶段,他会先对内存中所有需要清除的对象进行标记,然后在进行清除.
优点:操作简单
缺点:效率低,标记和清除的效率都不高,还有就是在清除的时候会造成不连续的空间碎片,这样会影响以后为占用内存大的对象,没有足够大的空间分配.
应用场景:一般用于老年带内存清理
复制算法:
复制算法的话主要是发生在我们的新生代中,新生代将内存分为 eden区from区和to区,一半对象都是存储在eden区和from区中,在进行垃圾回收的时候,会将eden区和from区存活的对象复制到to区中,然后清空eden区和from区,最后将from区和to区进行置换.from区变成新的to区
优点:简单,不会造成空间碎片.
缺点:将内存缩小了原来的一半
应用场景:新生代中的垃圾回收
新生代 : 老年代 1:2
eden : from : to 8:1:1
标记整理算法:
标记整理算法的话,主要是在标记清除算法的基础上进行了一个整理的操作,它会将不连续的对象进行整理排列,最后执行清除操作.这样就不会出现空间碎片了,但也降低了效率
优化收集方法的思路
分代收集算法
原理:根据对象存活的周期的不同将内存划分为几块,然后再选择合适的收集算法。
一般是把java堆分成新生代和老年代,这样就可以根据各个年待的特点采用最适合的收集算法。在新生代中,每次垃圾收集都会有大量的对象死去,只有少量存活,所以选用复制算法。老年代因为对象存活率高,没有额外空间对他进行分配担保,所以一般采用标记整理或者标记清除算法进行回收。
新生代:
是用来存放新生的对象。一般占据堆的 1/3 空间。由于频繁创建对象,所以新生代会频繁触发 MinorGC 进行垃圾回收。新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。
Eden 区
Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老 年代)。当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行 一次垃圾回收。
ServivorFrom
上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
ServivorTo
保留了一次 MinorGC 过程中的幸存者。
MinorGC 的过程(复制->清空->互换)
MinorGC 采用复制算法
1:eden、servicorFrom 复制到 ServicorTo,年龄+1 首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年 龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不 够位置了就放到老年区); 2:清空 eden、servicorFrom 然后,清空 Eden 和 ServicorFrom 中的对象;
3:ServicorTo 和 ServicorFrom 互换 最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom 区。
老年代
主要存放应用程序中生命周期长的内存对象。一般占据堆的 1/3 空间 老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行 了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足 够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。 MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没 有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减 少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的 时候,就会抛出 OOM(Out of Memory)异常。
永久代
指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被 放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这 也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
JVM 内存区域的划分
运行时数据区的组成
虚拟机在执行的时候会将内存划分为几个区域,分别是Java堆.方法区,虚拟机栈,本地方法栈,程序计数器
其中 堆和方法区是线程共享区域,虚拟机栈,本地方法栈,程序计数器是线程私有的区域,他的生命周期和
用户线程的生命周期一样
Java堆
Java堆的话是JVM内存中最大的一块区域,是一个被所有线程共享的区域.一般用来存放对象实例,几乎所有的对象都是在堆里完成内存的分配..Java堆也是我们垃圾回收对象重点执行的区域,,因为这边存放着我们各种的实例对象.
方法区
方法区也是我们所有线程共享的区域,一般存放的使我们的类的信息,(类的名称.字段方法,方法信息),静态变量,常量以及编译器编译后的代码等等.
运行时常量区:运行时常量区是方法区的一部分,class文件中除了有类的版本,字段,方法,接口,等描述信息外还有一项信息是常量池,用于存放编译期生成的字面变量和符号引用,这部分内容会在类加载进入方法区后的运行时常量区
程序计数器
程序计数器是一个表较小的内存空间,它可以看做我们线程所执行字节码的指示器.它在编译器中的作用是,指示下一步该干啥,到哪了.大家都知道在多线程的环境下,CPU执行线程是通过切换线程来实现的,也就是说一个CPU处理器都只会执行一条线程中的指令,因此为了保证线程切换之后能回到真确的位置,每个线程都有自己的线程计数器,这样线程之间就不会互相影响了,所以我们称程序计数器是线程私有的.
Java虚拟机栈
我们通常会将虚拟机内存粗糙的分为堆和栈,虚拟机栈就是栈的一部分,或者是虚拟机栈中局部变量表部分,和程序计数器一样,是线程私有的,虚拟机栈的线程也是私有的,每次执行方法时虚拟机栈都会创建一个栈针,每个栈帧对应一个方法,栈针里存放着我们的局部变量动态链表,出口信息等,在方法执行时将栈针压入栈中,等到方法执行完成后才会释放栈针
本地方法栈
本地方法栈与虚拟机栈很相似,只不过虚拟机栈执行的java方法,而本地方法栈执行的是本地服务
OOM(内存溢出)造成原因及解决方案
造成原因:
2.1、内存泄漏
由于长期保持某些资源的引用,垃圾回收器无法回收它,从而使该资源不能够及时释放,也称为内存泄露。因而尽量不要将所有引用都使用为强引用,可以在合适的地方使用弱引用和软引用。
2.2、超大对象
保存多个耗用内存过大或当加载单个超大的对象时,该对象的大小超过了当前剩余的可用内存空间。比如查询数据库中的数据,一次查询过多,直接导致内存溢出了。因此查询数据库如果数据过多尽量使用分页查询。
2.3、其他各种原因
比如是否存在死循环,大循环重复产生对象,是否有集合对象使用完之后,依然被引用着,导致无法清除,是否使用了不恰当的数据结构,导致占用空间过大等等
解决方案
修改JVM启动参数,直接增加内存
JVM默认可以使用的内存为64M,Tomcat默认可以使用的内存为128MB,对于稍复杂一点的系统就会不够用。在某项目中,就因为启动参数使用的默认值,经常报“Out Of Memory”错误。因此,-Xms,-Xmx(堆内存的最小大小和最大大小值)参数一定不要忘记加。
找出可能发生内存溢出的位置,并解决
检查代码中是否有死循环或递归调用。
检查是否有大循环重复产生新对象实体。
检查对数据库查询中,是否有一次获得全部数据的查询。
检查List、Map等集合对象是否有使用完后,未清除的问题。
使用内存查看工具动态查看内存使用情况
JDK自带的监控工具:
jconsole JDK 中自带的 java 监控和管理控制台,用于对 JVM 中内存,线程和类等的监控
Jvisualvm jdk 自带全能工具,可以分析内存快照、线程快照;监控内存变化、 GC 变化等。
常用调优参数
-XMS 设置堆内存的初始值
-XMX 设置堆内存的最大值
-XX:NewSize 设置新生代的大小
-XX:NewRatio 设置分代垃圾回收器 新生代和老年代的比例
-XX:MAXNewSize 设置新生代的最大内存
-XX:SurvivorRatio 设置新生代中 eden from to 区的比列