内存回收策略
1、对象优先在Eden里分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minar GC。
在JDK8环境上面分析一下日志:
import java.util.HashSet;
import java.util.Set;
/**
* 优先分配eden园区
* jvm args -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+UseConcMarkSweepGC
*/
public class EdenFirst {
public static void main(String[] args) throws InterruptedException {
Set<byte[]> ss = new HashSet<>();
for (int i = 1; i <= 200; i++) {
ss.add(new byte[1024 * 1024]);
}
}
}
2、大对象直接进入老年代
大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组,byte[]数组就是典型的大对象。大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到一群"朝生夕灭”的"短命大对象”,我们写程序的时候应注意避免。
在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时,大对象就意味着高额的内存复制开销。HotSpot虚 拟机提供了-XX: PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区之间来回复制,产生大量的内存复制操作。
注意:-XX: PretenureSizeThreshold参数只对Serial和ParNew两款新生代收集器有效,HotSpot的其他新生代收集器,如Parallel Scavenge并不支持这个参数。如果必须使用此参数进行调优,可考虑ParNew加CMS的收集器组合。
import java.util.HashSet;
import java.util.Set;
/**
* 大对象可以放在老年代,避免在eden,from,to新生代区域来回切换,-XX:PretenureSizeThreshold=2M
* jvm args
* -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+UseConcMarkSweepGC -XX:PretenureSizeThreshold=2M
*/
public class PretenureSizeThresholdTest {
public static void main(String[] args) throws InterruptedException {
Set<byte[]> ss = new HashSet<>();
for(int i=1;i<=10;i++){
// ss.add(new byte[1024*1024]);
ss.add(new byte[1024*1024*3]);
}
}
}
3、长期存活的对象将进入老年代
HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age) 计数器,对象通常在Eden区里诞生,如果经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
import java.util.HashSet;
import java.util.Set;
/**
* 大对象可以放在老年代,避免在eden,from,to新生代区域来回切换,-XX:PretenureSizeThreshold=2M
* jvm args
* -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+UseConcMarkSweepGC -XX:PretenureSizeThreshold=2M
*/
public class PretenureSizeThresholdTest {
public static void main(String[] args) throws InterruptedException {
Set<byte[]> ss = new HashSet<>();
for(int i=1;i<=10;i++){
// ss.add(new byte[1024*1024]);
ss.add(new byte[1024*1024*3]);
}
}
}
这里还有一个经典问题:"premature promotion"(过早提升)
import java.util.HashSet;
import java.util.Set;
/**
* 简单来说是幸存者区不够了,然后提前晋升到了老年代,一次没得问题,次数多了,累计起来会耗光老年代的资源然后晋升老年代失败导致的OOM
* -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintHeapAtGC
* -XX:+UseConcMarkSweepGC -XX:+PrintTenuringDistribution -XX:MaxTenuringThreshold=15
*/
public class PrematurePromotion {
public static void main(String[] args) throws InterruptedException {
Set<byte[]> ss = new HashSet<>();
for(int i=1;i<=140*1024;i++){
ss.add(new byte[1024]);
}
}
}

解决办法:
需要观察多次 Minor GC 过程, 看 Minor GC 后存活下来的对象大小,最终确定 Survivor 的合适大小。 整个调优过程可能需要几次调整, 才能找到比较合适的值。调整几次后, 如果内存还是不够用, 就要需要考虑增大服务器内存, 或者把负载分担到更多的 JVM 实例上。
4、动态对象年龄判定
为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX: MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX: MaxTenuringThreshold中要求的年龄。
import java.util.HashSet;
import java.util.Set;
/**
* 为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到-XX: MaxTenuringThreshold才能晋升老年代,
* 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,
* 无须等到-XX: MaxTenuringThreshold中要求的年龄。
* jvm args -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+UseConcMarkSweepGC
* -XX:+PrintTenuringDistribution -XX:MaxTenuringThreshold=15
*/
public class DynamicTenuring {
public static void main(String[] args) throws InterruptedException {
Set<byte[]> ss = new HashSet<>();
for(int i=1;i<=4*1024;i++){
byte[] bb = new byte[1024*1024];
bb = null;
ss.add(new byte[1024]);
}
}
}

浙公网安备 33010602011771号