前置知识点1
- 方法的局部变量是跟着栈帧走的
什么时候类才会被回收
- 类对应的实例化对象在堆中被回收了
- 加载该类的 ClassLoader 被回收了
- 引用该类的 class 引用没了
一些核心参数
**-Xms**
:Java堆内存的大小**-Xmx**
:Java堆内存的最大大小**-Xmn**
:Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了**-XX:PermSize**
:永久代大小**-XX:MaxPermSize**
:永久代最大大小**-Xss**
:每个线程的栈内存大小
怎么样的 gc 会影响性能
- 设置的堆太小
- 过高的访问,导致新生代频繁有对象创建和销毁,导致频繁的 minor gc
- 如果对应访问的业务操作过长(慢请求),会导致一次访问中的对象长时间被引用
- 会躲过多次 minor gc,从而更容易进入老年代
- 老年代会因为慢请求而塞满,从而引发 full gc
- full gc 又是很慢的
- 如果对应访问的业务操作过长(慢请求),会导致一次访问中的对象长时间被引用
如何评估架构所需要的堆大小
- 根据一条核心业务,看它在高峰期1s内创建的核心对象大小和量
- 类对象可以根据字段类型计算 byte 1字节 ,short 2字节 int 4字节,long 8字节,float 4字节,double 8字节,boolean 1字节,char 2字节 Unicode码占两字节
- 算出 1s 内创建对象的大小和总大小
- 类对象可以根据字段类型计算 byte 1字节 ,short 2字节 int 4字节,long 8字节,float 4字节,double 8字节,boolean 1字节,char 2字节 Unicode码占两字节
- 正常生成环境会创建其他对象,所以将上面核心对象的总量乘以 10-20 倍
- 这些量平均给几台机器
- 看机器配置进行设置,算下 gc 时间
合理设置永久代大小
前置知识2
那些对象不会被回收?
- 可达性算法判断
- 对每个对象都分析下有谁在引用它,一层层往上找,看能不能找到 GC Roots
- GC Roots 主要是 类的静态变量 和 方法的局部变量
对象引用类型
- 强引用
new
出来的对象
- 软引用
SoftReference<Object>
- 如果 gc 发现内存还是不够放新的对象,不管软引用是否被变量引用,都会被回收
- 弱引用
WeakReference<Object>
- gc 就会干掉它
- 虚引用
- 很少用
会被回收的对象小总结
- 被
GC Roots
引用的对象不能回收,没有被GC Roots
引用的对象会被回收,被GC Roots
引用的软引用有可能被回收,被GC Roots
引用的弱引用会被回收
finalize()
- 如果重写了该方法,让自身还被
GC Roots
引用,就不会被回收 - 小细节,一般不会动他
Minor GC
复制算法
- 新生代分包含 Eden区, 两个 Survivor 区 (可以看为 From-Survivor、To-Survivor或者 S1, S2)
- 步骤如下
- 对象优先分配在 eden 区中,如果 eden 满了触发 minor gc,将 eden 存活的对象存到 from-survivor 中,然后回收 eden 中的被回收对象
- 接下来如果 eden 满了触发 minor gc,将 eden 和 from-survivor 存活的对象存到 to-survivor 中,然后回收 eden 和 from-survivor 中的被回收对象
- 然后理解为 from-survivor 和 to-survivor 的身份互换
- 重复步骤 3、4
- 对象优先分配在 eden 区中,如果 eden 满了触发 minor gc,将 eden 存活的对象存到 from-survivor 中,然后回收 eden 中的被回收对象
- 根据 1% 对象存活的原则,一般 to-survivor 中的对象应该是不到 10% 的
新生代的对象什么时候进入老年代
- 躲过15次 minor gc 后进入老年代,可以通过设置
-XX:MaxTenuringThreshold
来设置,默认 15 次- jdk8 仅能设置 0-15 之内
- jdk8 仅能设置 0-15 之内
- 动态对象年龄判断(从第二次 minor gc 开始(包括此次),每次 minor gc 后触发)
- 不用考虑15次 minor gc
- 如果survivor区的一批对象总大小大于 suvivor 区的一半,那么剩下的大于等于该批对象最大年龄的对象,直接进入老年代
- 不用考虑15次 minor gc
- 大对象直接进老年代,大 这个概念可以用过
-XX:PretenureSizeThreshold
来设置字节数 - minor gc 后对象太多放不进 suvivor 区,会将这些存活对象扔进老年代 (这是优化重点,且这个操作看下面老年代空间担保原则中的一个过程)
堆内存分配原则
- 和上一小节 新生代的对象什么时候进入老年代 部分重复
对象优先在 Eden 分配
大对象直接进入老年代
长期存活的对象进入老年代
动态对象年龄判断
空间分配担保
jdk 6 之前(不包含 jdk6 )
- full gc 很多情况下会在 minor gc 后触发
- 所以说为啥 full gc 会附带一次 minor gc
- 其实绕一个圈子就是为了减少 full gc 的概率
jdk 6 之后
- 1.6
-XX:-HandlePromotionFailure
参数无效了 - 两种情况下,有一种不满足即可
- 在 minor gc 前
- 判断老年代最大可用的连续空间是否大于新生代所有对象总空间
- 否, 直接 old gc
- 是,可以 minor gc
- 判断老年代最大可用的连续空间是否大于新生代对象历次晋升老年代的对象的平均大小
- 否, 直接 old gc
- 是,可以 minor gc
- 判断老年代最大可用的连续空间是否大于新生代所有对象总空间
**
如何触发 full gc
- 第一是老年代可用内存小于新生代全部对象的大小
- jdk6 之前,如果没开启空间担保参数,会直接触发Full GC,所以一般空间担保参数都会打开;
- jdk6 之后,直接 full gc
- 第二是老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC;
- jdk6 之前,本步骤是判断空间担保参数为true的后续步骤
- jdk6 之后,并非后续步骤
- 第三是新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时老年代内存不足。
- 第四
-XX:CMSInitiatingOccupancyFaction
参数,(92%) 如果老年代可用内存大于历次新生代GC后进入老年代的对象平均大小,但是老年代已经使用的内存空间超过了这个参数指定的比例,也会自动触发Full GC
如何做到只有 minor gc
- 堆够大
- 尽量不要让对象进入老年代
- 尽量让 survivor 区能够存放 minor gc 后的对象
- 并且不会因为 动态年龄判断 导致对象直接进入老年代
- 只要每次 minor gc 后,To-Survivor(S2)能存放下存活对象,就能控制基本上只有 minor gc
例子
- 别人的例子
- 每台机器可以提供给JVM的最大内存: each_m,比如2核4G机器,可提供JVM最大内存2G 栈占用:stack_m = QPS估值 1M 20倍数,估值30QPS,栈约为600M
- 新生代以30分钟一次GC计算总内存:30(Monitor GC间隔) 60 QPS估值 * 接口内存估值,young_m 所需机器数量,假设等于N
- 方法区:200M,一般够用,method_m 老年代:500M,一般不大,300M也行,像我们结算服务,100M都够用
- old_m 演算公式: JVM最大内存N = stack_m + young_m + old_m + method_m N 机器数N,也同时估算出来
GC 名称
- minor gc/ young gc: 新生代的 gc
- old gc: 老年代的 gc
- full gc: 新生代、老年代和永久代的 gc
- major gc: 很少用,避免混淆,少用
- mixed gc: 使用 g1 垃圾回收器时,如果老年代占据堆内存 45% (默认) 以上触发