深入进阶:图解分析JVM内存堆布局
从JDK7开始永久代的移除工作,贮存在永久代的一部分数据已经转移到了Java Heap或者是Native Heap。但永久代仍然存在于JDK7,并没有完全的移除:符号引用(Symbols)转移到了native heap;字面量(interned strings)转移到了java heap;类的静态变量(class statics)转移到了java heap。随着JDK8的到来,JVM不再有PermGen。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。 在JVM中共享数据空间划分如下图所示 上图中,刻画了Java程序运行时的堆空间,可以简述成如下2条 1.JVM中共享数据空间可以分成三个大区,新生代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation),其中JVM堆分为新生代和老年代 2.新生代可以划分为三个区,Eden区(存放新生对象),两个幸存区(From Survivor和To Survivor)(存放每次垃圾回收后存活的对象) 3.永久代管理class文件、静态对象、属性等(JVM uses a separate region of memory, called the Permanent Generation (orPermGen for short), to hold internal representations of java classes. PermGen is also used to store more information ) 4.JVM垃圾回收机制采用“分代收集”:新生代采用复制算法,老年代采用标记清理算法。 作为操作系统进程,Java 运行时面临着与其他进程完全相同的内存限制:操作系统架构提供的可寻址地址空间和用户空间。 操 作系统架构提供的可寻址地址空间,由处理器的位数决定,32 位提供了 2^32 的可寻址范围,也就是 4,294,967,296 位,或者说 4GB。而 64 位处理器的可寻址范围明显增大:2^64,也就是 18,446,744,073,709,551,616,或者说 16 exabyte(百亿亿字节)。 地址空间被划分为用户空间和内核空间。内核是主要的操作系统程序和C运行时,包含用于连接计算机硬件、调度程序以及提供联网和虚拟内存等服务的逻辑和基于C的进程(JVM)。除去内核空间就是用户空间,用户空间才是 Java 进程实际运行时使用的内存。 默认情况下,32 位 Windows 拥有 2GB 用户空间和 2GB 内核空间。在一些 Windows 版本上,通过向启动配置添加 /3GB 开关并使用 /LARGEADDRESSAWARE 开关重新链接应用程序,可以将这种平衡调整为 3GB 用户空间和 1GB 内核空间。在 32 位 Linux 上,默认设置为 3GB 用户空间和 1GB 内核空间。一些 Linux 分发版提供了一个hugemem内核,支持 4GB 用户空间。为了实现这种配置,将进行系统调用时使用的地址空间分配给内核。通过这种方式增加用户空间会减慢系统调用,因为每次进行系统调用时,操作系统必须在地址空间之间复制数据并重置进程地址-空间映射。 下图为一个32 位 Java 进程的内存布局: 可寻址的地址空间总共有 4GB,OS 和 C 运行时大约占用了其中的 1GB,Java 堆占用了将近 2GB,本机堆占用了其他部分。请注意,JVM 本身也要占用内存,就像 OS 内核和 C 运行时一样。 注意: 1. 上文提到的可寻址空间即指最大地址空间。 2. 对于2GB的用户空间,理论上Java堆内存最大为1.75G,但一旦Java线程的堆达到1.75G,那么就会出现本地堆的Out-Of-Memory错误,所以实际上Java堆的最大可使用内存为1.5G。 在JVM运行时,可以通过配置以下参数改变整个JVM堆的配置比例
在上面的配置中,老年代所占空间的大小是由-XX:SurvivorRatio这个参数进行配置的,看完了上面的JVM堆空间分配图,可能会奇怪,为啥新生代空间要划分为三个区Eden及两个Survivor区?有何用意?为什么要这么分?要理解这个问题,就得理解一下JVM的垃圾收集机制(复制算法也叫copy算法),步骤如下: 复制(Copying)算法 将内存平均分成A、B两块,算法过程: 1. 新生对象被分配到A块中未使用的内存当中。当A块的内存用完了, 把A块的存活对象对象复制到B块。 2. 清理A块所有对象。 3. 新生对象被分配的B块中未使用的内存当中。当B块的内存用完了, 把B块的存活对象对象复制到A块。 4. 清理B块所有对象。 5. goto 1。 优点:简单高效。缺点:内存代价高,有效内存为占用内存的一半。 图解说明如下所示:(图中后观是一个循环过程) 对复制算法进一步优化:使用Eden/S0/S1三个分区 平均分成A/B块太浪费内存,采用Eden/S0/S1三个区更合理,空间比例为Eden:S0:S1==8:1:1,有效内存(即可分配新生对象的内存)是总内存的9/10。 算法过程: 1. Eden+S0可分配新生对象; 2. 对Eden+S0进行垃圾收集,存活对象复制到S1。清理Eden+S0。一次新生代GC结束。 3. Eden+S1可分配新生对象; 4. 对Eden+S1进行垃圾收集,存活对象复制到S0。清理Eden+S1。二次新生代GC结束。 5. goto 1。 默认Eden:S0:S1=8:1:1,因此,新生代中可以使用的内存空间大小占用新生代的9/10,那么有人就会问,为什么不直接分成两个区,一个区占9/10,另一个区占1/10,这样做的原因大概有以下几种 1.S0与S1的区间明显较小,有效新生代空间为Eden+S0/S1,因此有效空间就大,增加了内存使用率 (编辑:好传媒网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |