JVM 年青代内存分配
引入
先看一段代码:
Test
public void testJVM() {
System.out.println("maxMemory=");
System.out.println(Runtime.getRuntime().maxMemory()+" bytes");
System.out.println("free mem=");
System.out.println(Runtime.getRuntime().freeMemory()+" bytes");
System.out.println("total mem=");
System.out.println(Runtime.getRuntime().totalMemory()+" bytes");
byte[] b = new byte[1*1024*1024];
System.out.println("分配了1M空间给数组");
System.out.println("maxMemory=");
System.out.println(Runtime.getRuntime().maxMemory()+" bytes");
System.out.println("free mem=");
System.out.println(Runtime.getRuntime().freeMemory()+" bytes");
System.out.println("total mem=");
System.out.println(Runtime.getRuntime().totalMemory()+" bytes");
b = new byte[4*1024*1024];
System.out.println("分配了4M空间给数组");
System.out.println("maxMemory=");
System.out.println(Runtime.getRuntime().maxMemory()+" bytes");
System.out.println("free mem=");
System.out.println(Runtime.getRuntime().freeMemory()+" bytes");
System.out.println("total mem=");
System.out.println(Runtime.getRuntime().totalMemory()+" bytes");
}
运行时配置JVM参数如下:
-Xmx20m -Xms20m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+PrintTenuringDistribution
运行后日志如下:
-XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+PrintTenuringDistribution -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC
[GC [DefNew
Desired survivor size 327680 bytes, new threshold 1 (max 15)
- age 1: 655360 bytes, 655360 total
: 5504K->640K(6144K), 0.0036274 secs] 5504K->861K(19840K), 0.0036505 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
maxMemory=
20316160 bytes
free mem=
18542976 bytes
total mem=
20316160 bytes
分配了1M空间给数组
maxMemory=
20316160 bytes
free mem=
17494384 bytes
total mem=
20316160 bytes
[GC [DefNew
Desired survivor size 327680 bytes, new threshold 15 (max 15)
- age 1: 19488 bytes, 19488 total
: 2534K->19K(6144K), 0.0025142 secs] 2755K->1902K(19840K), 0.0025677 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
分配了4M空间给数组
maxMemory=
20316160 bytes
free mem=
14063464 bytes
total mem=
20316160 bytes
Heap
def new generation total 6144K, used 4279K [0x00000000f9a00000, 0x00000000fa0a0000, 0x00000000fa0a0000)
eden space 5504K, 77% used [0x00000000f9a00000, 0x00000000f9e29130, 0x00000000f9f60000)
from space 640K, 2% used [0x00000000f9f60000, 0x00000000f9f64c20, 0x00000000fa000000)
to space 640K, 0% used [0x00000000fa000000, 0x00000000fa000000, 0x00000000fa0a0000)
tenured generation total 13696K, used 1882K [0x00000000fa0a0000, 0x00000000fae00000, 0x00000000fae00000)
the space 13696K, 13% used [0x00000000fa0a0000, 0x00000000fa276bf0, 0x00000000fa276c00, 0x00000000fae00000)
compacting perm gen total 21248K, used 5483K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 25% used [0x00000000fae00000, 0x00000000fb35ae18, 0x00000000fb35b000, 0x00000000fc2c0000)
No shared spaces configured.
分析
可以看到,当前最大内存为由-XX:MaxHeapSize = 20971520 我们根据分配了20M 20*1024*1024=20971520
字节。刚好显示的与配置的是对应的,但Runtime.getRuntime().maxMemory() = 20316160
字节,比设定的为何少了呢?这是因为分配给堆的内存空间和实际可用空间并非一个概念
。我们知道垃圾回收在年青代的so s1(from/to)空间的回收算法一般采用的是复制算法,该算法是以空间换时间的策略,他的算法特点以后会展开说,总的来说,就是该算法会使两个相同大小的空间,在同一时刻只会有一个空间在使用。因此,实际最大可用内存为-Xmx的值减去from的空间大小
.默认情况下,我们知道eden/from = 8
且从堆打印上可以算出from的大小from space 640K, 2% used [0x00000000f9f60000, 0x00000000f9f64c20, 0x00000000fa000000)
其中[0x00000000f9f60000, 0x00000000f9f64c20, 0x00000000fa000000)
这三个分别表示该空间的下界,当前上界,上界,用上界减去下界就是大小了:0x00000000fa000000-0x00000000f9f60000=0xA0000=A*16^4=655360
字节 用20971520 -655360=20316160
刚好为可用的内存,但有一个很好玩的问题
我们知道以下根据我们的JVM配置以下参数的值如下:
NewRatio=2,NewRatio=Old/Yong=Old/(Eden+Survivor*2)
SurvivorRatio=8,SurvivorRatio=Eden/Survivor
TargetSurvivorRatio=0.5
我们可以计算出From应该为:20*1024*1024/3/10=699050
但实现中却只有655360
为何呢?
// 根据配置参数SurvivorRatio来确定年青代中Eden/From/To三个内存区的大小
size_t compute_survivor_size(size_t gen_size, size_t alignment) const {
size_t n = gen_size / (SurvivorRatio + 2);
return n > alignment ? align_size_down(n, alignment) : alignment;
}
具体的详细内容可以看这个联连内存代管理器DefNewGeneration的对象内存分配
再看align_size_down(n, alignment)
#define align_size_down(size, alignment) (size & (~(alignment-1)) )
可见虚拟机分配from/to时进行内存对齐操作。
上面代码 alignment 在非ARM平台上为 2^16 gen_size
表示新生代的总数 n =gen_size / (SurvivorRatio + 2)=6990500/10=699050而(size & (~(alignment-1)) = 699050&(~(2^16-1))=655360
刚好为实际分配的 655360
对齐方法
这个对齐方法,也是linux中常用的对齐函数。他是怎么做到对齐的呢,下面我们分开看
1. ~(alignment-1) 先看这一部分
假设alignment是8 对应的二进制为:0000 1000
(a-1)为:0000 0111
~(a-1)为:1111 1000
任何一个数与~(a-1)按位与 都可以把后面的位置为0 也就是把这个数置为alignment的倍数
其实如果我们要求某数以alignment为倍数的界数 就让这个数(要计算的这个数)表示成二进制时,最后几位为0就可以达到这个目标
如本例中的8 只要下面这个数与待计算的数进行”与运算”就可以了: 11111111 11111111 11111111 11111000
而这个值就是 ~(alignment-1)
2.
计算size以alignment为倍数的上下界数:
#define alignment_down(size, alignment) (size & (~(alignment-1)) )
#define alignment_up(size, alignment) ((size+alignment-1) & (~ (alignment-1)))
注: 上界数的计算方法,如果要求出比size大的是不是需要加上8就可以了?可是如果size本身就是8的倍数,这样加8不就错了吗,所以在size基础上加上(alignment- 1), 然后与alignment的对齐码进行与运算.
例如:
size=0, alignment=8, 则alignment_down(size,alignment)=0, alignment_up(size,alignment)=0.
size=6, alignment=8, 则alignment_down(size,alignment)=0, alignment_up(size,alignment)=8.
size=8, alignment=8, 则alignment_down(size,alignment)=8, alignment_up(size,alignment)=8.
size=14, alignment=8,则alignment_down(size,alignment)=8, alignment_up(size,alignment)=16.
注:alignment应当为2的n次方, 即2, 4, 8, 16, 32, 64, 128, 256, 1024, 2048, 4096 ...
参考:
内存对齐
原码, 反码, 补码 详解