前几天突然收到一堆报警短信,吓得赶紧打开电脑查看日志,发现JVM不断做FULL GC,于是赶紧让运维重启服务器,接口监控提示一切正常。
废话不多说,想要了解jvm的gc那得先说说jvm的内存结构:
记录每个线程执行到哪条字节码指令,解释器通过它来选取下一条执行的字节码指令,线程私有。
以栈帧为单位,进行压栈和出栈,线程私有。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法的返回地址等信息。每一个方法从调用开始直至执行完成的过程,都对应的一个栈帧在虚拟机栈里入栈和出栈的过程。每次方法调用均会创建一个对应的Frame,方法执行完毕或者异常终止,Frame被销毁。一个方法A调用另一个方法B时,A的frame停止,新的frame被创建赋予B,执行完毕后,把计算结果传递给A,A继续执行。JVM Statck的大小可以是固定的,也可以是动态扩展的。如果线程需要一个比固定大小大的Stack,会发生StackOverflowError;如果动态扩展Stack时没有足够的内存或者系统没有足够的内存为新线程创建Stack,发生OutOfMemoryError。参数-Xss用来设置虚拟机栈的大小。

为native方法服务,线程私有,有可能和虚拟机栈合二为一。
jvm内存中最大的一块,线程共享,用来分配对象实例,数组。GC回收的主要区域,根据GC回收机制又可以分为年轻代和老年代。-Xms,初始使用,默认物理内存 1/64。-Xmx,最大内存,默认物理内存 1/4。虚拟机会根据堆的空闲情况动态调整推大小,空余大于 70%,会减少到 -Xms,空余小于 40%,会增大到 -Xmx。所以服务器如果配置 -Xms = -Xmx,则可以避免堆自动扩展。
主要存放已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(比如spring 使用IOC或者AOP创建bean时,或者使用cglib,反射的形式动态生成class信息等),线程共享。
关于这边有个有意思的东西:
Integer a=1; Integer b=1; Integer c=new Integer(1); System.out.println(a==b);//true System.out.println(a==c);//false
而我们知道==判断基本类型的时候是判断值相等,判断类的时候判断地址相等。那为什么会出现这种情况呢?因为Byte,Short,Integer,Long,Character,Boolean这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据放进了常量池,但是超出此范围仍然会去创建新的对象。 两种浮点数类型的包装类Float,Double并没有实现常量池技术。
所以从上面的解释我们可以有一个jvm内存结果的图:

在对jvm 内存的了解后,我们知道Java 堆中存放着几乎所有的对象实例,但是资源是有限的,所以我们需要对对象进行回收。垃圾收集器对堆中的对象进行回收前,要先确定这些对象是否还有用,判定对象是否为垃圾对象有如下算法:
给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1,当引用失效时,计数器值就减1,任何时刻计数器都为 0 的对象就是不可能再被使用的。引用计数算法的实现简单,判定效率也很高,在大部分情况下它都是一个不错的选择,但是Java语言并没有选择这种算法来进行垃圾回收,主要原因是它很难解决对象之间的相互循环引用问题。
这种算法的基本思路是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可用的。在 Java 语言里,可作为 GC Roots 的兑现包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中的常量引用的对象。
- 本地方法栈中 JNI(Native 方法)的引用对象。
一般情况下,在垃圾回收期间,一个无法触及的对象会立即被销毁。不过,覆盖了finalize()方法的对象会被移动到一个队列里,一个独立的线程遍历这个队列,调用每一个对象的finalize()方法。在finalize()方法调用结束之后,这些对象才成为真正的垃圾,等待下一轮垃圾回收。