loading请求处理中...

内存分配与回收策略

2021-12-02 05:48:47 阅读 12230次 标签: c#开发基础 作者: 蓝blue

Java自动内存管理的两个问题:

  1. 给对象分配内存
  2. 回收分配给对象的内存

内存分配与回收策略

对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代中,分配的规则不是百分之百固定的,其细节在于使用哪种垃圾收集器组合,还有虚拟机中与内存相关的参数。

对象优先在Eden分配

绝大多数情况,对象都在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机会发生一次Minor GC。

虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。在实际应用中,内存回收日志一般是打印到文件后通过日志工具进行分析,不过本实验的日志并不多,直接阅读就能看的很清楚。

测试内存分配代码:

public class TestAllocation {
    private static final int _1MB = 1024 * 1024;

    /**
     * VM参数:-XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
      */
    public static void testAllocation() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];  // 出现一次Minor GC
     }
    public static void main(String []args){
        TestAllocation.testAllocation();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

运行结果: 
[GC (Allocation Failure) [DefNew: 7129K->531K(9216K), 0.0037131 secs] 7129K->6675K(19456K), 0.0137564 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap 
def new generation total 9216K, used 4709K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) 
eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000) 
f rom space 1024K, 51% used [0x00000000ff500000, 0x00000000ff584c58, 0x00000000ff600000) 
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) 
tenured generation total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) 
the space 10240K, 60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000) 
Metaspace used 2573K, capacity 4486K, committed 4864K, reserved 1056768K 
class space used 285K, capacity 386K, committed 512K, reserved 1048576K 
注意:使用64位的需要添加参数-XX:+UseSerialGC,使其强制使用SerialGC 
以上的方法试图分配3个2MB大小和一个4MB大小的对象,在运行通过 -Xms20M -Xmx20M -Xmn10M三个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。-XX:SurvivorRatio=0决定了新生代中Eden区与一个Survivor区的空间比例为8:1,从输出的结果也可以清晰地看到“eden space 8192K、f rom space 1024K、to space 1024K”的信息,新生代的总可用空间为9216KB(Eden区+一个Survivor区的总容量)。

执行testAllocation()中分配allocation4的语句时会发生一次Minor GC,这次GC的结果时新生代6651KB变为148KB,而总内存占用几乎没有减少(因为allocation1、allocation2、allocation3三个对象都是存活的,虚拟机几乎没有找到可回收的对象)。这次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB。剩余空间已经不足以分配allocation4所需的4MB内存,因此发生Minor GC。GC 期间虚拟机又发现已有的3个2MB大小的对象全部无法放入Survivor空间(仅有1MB),所以只好通过分配担保的机制提前转移到老年代去了。

这次GC结束后,4MB的allocation4的对象顺利分配在Eden中,因此程序执行完的结果是Eden占用4MB(被allocation4占用),Survivor空闲,老年代被占用6MB(被allocation1allocation2allocation3占用)。通过GC日志可以证实这点。

Minor GC和Full GC不同点:

  • Minor GC:指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。。
  • Major GC/Full GC:指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10倍以上。

大对象直接进入老年代

所谓大对象就是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组(笔者例子中的byte[]数组就是典型的大对象)。大对象对虚拟机的内存分配来说就是一个坏消息(替Java虚拟机抱怨一句,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝(复习一下:新生代采用复制算法收集内存)。 
代码:

public class TestPretensureSizeThreshold {
    private static final int _1MB = 1024 * 1024;

    /**
     * VM参数:-XX:+UseSerialGC -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
     * -XX:PretenureSizeThreshold=3145728
     */
    public static void testPretenureSizeThreshold() {
        byte[] allocation;
        allocation = new byte[4 * _1MB];  //直接分配在老年代中
    }

    public static void main(String []args){
        TestPretensureSizeThreshold.testPretenureSizeThreshold();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

运行结果: 
Heap 
def new generation total 9216K, used 1148K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) 
eden space 8192K, 14% used [0x00000000fec00000, 0x00000000fed1f3f0, 0x00000000ff400000) 
f rom space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) 
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) 
tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) 
the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000) 
Metaspace used 2572K, capacity 4486K, committed 4864K, reserved 1056768K 
class space used 285K, capacity 386K, committed 512K, reserved 1048576K

执行如上代码的testPretenureSizeThreshold()方法后,我们看到Eden空间几乎没有被使用,而老年代10MB的空间被使用了40%,也就是4MB的allocation对象直接就分配在老年代中,这是因为PretenureSizeThreshold被设置为3MB(就是3145728B,这个参数不能与-Xmx之类的参数一样直接写3MB),因此超过3MB的对象都会直接在老年代中进行分配。

长期存活的对象进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

读者可以试试分别以-XX:MaxTenuringThreshold=1和-XX:MaxTenuringThreshold=15两种设置来执行代码清单3-7中的testTenuringThreshold()方法,此方法中的allocation1对象需要256KB内存,Survivor空间可以容纳。当MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后非常干净地变成0KB。而MaxTenuringThreshold=15时,第二次GC发生后,allocation1对象则还留在新生代Survivor空间,这时新生代仍然有1MB被占用。 
代码:

public class TestTenuringThreshold {
    private static final int _1MB = 1024 * 1024;  

    /**  
     * VM参数:-XX:+UseSerialGC -verbose:gc -Xms80M -Xmx80M -Xmn40M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 
     * -XX:+PrintTenuringDistribution  
     */  
    @SuppressWarnings("unused")  
    public static void testTenuringThreshold() {  
      byte[] allocation1, allocation2, allocation3;  
      allocation1 = new byte[_1MB];    
       // 什么时候进入老年代取决于XX:MaxTenuringThreshold设置  
      allocation2 = new byte[16 * _1MB];  
      allocation3 = new byte[16 * _1MB];  
      allocation3 = null;  
      allocation3 = new byte[16 * _1MB];  
    } 

    public static void main(String[] args) {
        TestTenuringThreshold.testTenuringThreshold();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

以MaxTenuringThreshold=1得到的结果:

[GC (Allocation Failure) [DefNew: 19374K->1555K(36864K), 0.0073413 secs] 19374K->17939K(77824K), 0.0073791 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 17939K->0K(36864K), 0.0016595 secs] 34323K->17938K(77824K), 0.0016844 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 36864K, used 16712K [0x00000000fb000000, 0x00000000fd800000, 0x00000000fd800000)
  eden space 32768K,  51% used [0x00000000fb000000, 0x00000000fc052040, 0x00000000fd000000)
  **f rom space 4096K,   0%** used [0x00000000fd000000, 0x00000000fd000000, 0x00000000fd400000)
  to   space 4096K,   0% used [0x00000000fd400000, 0x00000000fd400000, 0x00000000fd800000)
 tenured generation   total 40960K, used 17938K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
   **the space 40960K,  43%** used [0x00000000fd800000, 0x00000000fe984970, 0x00000000fe984a00, 0x0000000100000000)
 Metaspace       used 2573K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

以MaxTenuringThreshold=15得到的结果:

[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 15 (max 15)
- age   1:    1592488 bytes,    1592488 total
: 19374K->1555K(36864K), 0.0089774 secs] 19374K->17939K(77824K), 0.0090155 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 2097152 bytes, new threshold 15 (max 15)
- age   2:    1591648 bytes,    1591648 total
: 17939K->1554K(36864K), 0.0011282 secs] 34323K->17938K(77824K), 0.0011471 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 36864K, used 18266K [0x00000000fb000000, 0x00000000fd800000, 0x00000000fd800000)
  eden space 32768K,  51% used [0x00000000fb000000, 0x00000000fc052040, 0x00000000fd000000)
  f rom space 4096K,  37% used [0x00000000fd000000, 0x00000000fd184960, 0x00000000fd400000)
  to   space 4096K,   0% used [0x00000000fd400000, 0x00000000fd400000, 0x00000000fd800000)
 tenured generation   total 40960K, used 16384K [0x00000000fd800000, 0x0000000100000000, 0x0000000100000000)
   the space 40960K,  40% used [0x00000000fd800000, 0x00000000fe800010, 0x00000000fe800200, 0x0000000100000000)
 Metaspace       used 2572K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 285K, capacity 386K, committed 512K, reserved 1048576K
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

动态对象年龄判断

对象的年龄到达了MaxTenuringThreshold可以进入老年代,同时,如果在survivor区中相同年龄所有对象大小的总和大于survivor区的一半,年龄大于等于该年龄的对象就可以直接进入老年代。无需等到MaxTenuringThreshold中要求的年龄。

具体代码如下:

public class AllocationTest2 {
    private static final int _1MB = 1024 * 1024;

    /*
     *     -Xms20M -Xmx20M -Xmn10M 
        -XX:SurvivorRatio=8 
        -XX:+PrintGCDetails
        -XX:+UseSerialGC
        -XX:MaxTenuringThreshold=15
        -XX:+PrintTenuringDistribution
     * */

    public static void testTenuringThreshold2() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }

    public static void main(String[] args) {
        testPretenureSizeThreshold2();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

得到结果:

[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:    1048576 bytes,    1048576 total
: 5592K->1024K(9216K), 0.0026321 secs] 5592K->5139K(19456K), 0.0026687 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
: 5120K->0K(9216K), 0.0009009 secs] 9235K->5139K(19456K), 0.0009171 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  f rom space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 5139K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  50% used [0x00000000ff600000, 0x00000000ffb04c20, 0x00000000ffb04e00, 0x0000000100000000)
 Metaspace       used 2573K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used bool TenuredGeneration::promotion_attempt_is_safe(size_t
max_promotion_in_bytes) const {
   // 老年代最大可用的连续空间
   size_t available = max_contiguous_available();  
   // 每次晋升到老年代的平均大小
   size_t av_promo  = (size_t)gc_stats()->avg_promoted()->padded_average();
   // 老年代可用空间是否大于平均晋升大小,或者老年代可用空间是否大于当此GC时新生代所有对象容量
   bool   res = (available >= av_promo) || (available >=
max_promotion_in_bytes);
  return res;
}

开发公司推荐

成为一品威客服务商,百万订单等您来有奖注册中

留言( 展开评论

快速发任务

价格是多少?怎样找到合适的人才?

官方顾问免费为您解答

 
相关任务
DESIGN TASK 更多
桌面应用程序开发

¥50000 已有0人投标

招商引资APP开发

¥20000 已有0人投标

APP原生开发

¥20000 已有1人投标

开发ai拍照软件

¥100 已有2人投标