Java垃圾收集器与内存分配策略
Contents
最近在看《深入理解Java虚拟机:JVM高级特性与最佳实践》这本书,觉得有必要记录一下. 如无说明,则图片是我用Google Drawings制作的, under
CC BY-NC-SA 3.0 CN
License.
Java运行时内存
先上图
虽然Java中没有直接(明显)的指针操作,但是在内部的实现里用的还是指针的.在访问对象的过程中,有两种方式可以实现:句柄访问,直接指针.对象实际上是一个reference类型的数据,其中存储的是他自己的地址,通过句柄访问则是句柄的地址.
VM options
IDEA设置
书中需要设置各种虚拟机参数,书里用的是Eclipse
,这里介绍一下IDEA的怎么设置.
点击导航栏 Run
-> Edit Configurations
-> VM options
.这样就可以对不同的main方法设置不同的参数了.
参数介绍
名称 | 描述 |
---|---|
-XX:+PrintGC | 打印 GC 信息 |
-XX:+PrintGCDetails | 打印 GC 信息… |
-XX:+PrintGCTimeStamps | 打印 GC 信息.. |
-XX:+UseConcMarkSweepGC | 使用 ParNew + CMS + Serial Old(备用)进行内存收集 |
-XX:+UseParallelGC | 在 Server 模式下的默认值,使用 Parallel Scavenge + Serial Old. |
-XX:+UseSerialGC | 在 Client 模式下的默认值,使用 Serial Old + Serial 的组合收集器 |
-XX:SurvivorRatio= | Eden 区与 Survivor 区的大小比值 |
-Xmn | 新生代大小 |
-Xms | 初始堆大小 |
-Xmx | 最大堆大小 |
垃圾回收算法
根搜索算法
根搜索算法应该不算是垃圾回收算法,而是用来判断对象是否存活的算法.我为了方便就放到这里了.
基本思路就是通过一系列 GC Roots
的对象作为起点,从这些节点开始进行搜索.如果有对象是 GC Roots
不能到达的,就将对象标记为可清理的.
那么什么样的对象可以成为GC Roots呢?
- 虚拟机栈中局部变量引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈(Native Method Stack)中的引用的对象.
分代算法
虚拟机大多采用这种分代收集的算法,也就是根据对象存活的周期将内存划分几个区域.一把把Java Heap 分为新生代和老年代.对不同的年代,使用不同的回收方法.
新生代一般采用复制算法.
老年代一般采用标记-清理 或 标记-整理 算法.
标记-清理
图片来自CSDN
顾名思义,这个算法分成两部分:标记和清理.标记部分就是用的根搜索算法.在标记后,统一回收被标记的对象.从图中可以看出,清理后,留下了大量的不连续的内存碎片.这可能会导致无法分配大对象,而触发另一次的GC.
复制
图片来自CSDN
复制(Copying)可以解决标记-清理算法产生大量内存碎片的问题.
它将可用内存分为等大的两块,每次使用其中的一块,当这一块内存用完时,将其中还存活的对象移到另一块内存中,然后一次性就把已使用的一块内存清理掉.
新生代一般采用这种算法,只不过,虚拟机里的并不是按1:1来分配内存的,而是分为一块大的Eden,和两块小的Survivor.每次使用Eden和一块Survivor,回收时将Eden中和已使用的一块 Survivor中的存活对象,移到另一块Survivor中(当然,在对象存活率高的时候,这一块Survivor可能不够用,那么就会依赖老年代了,(下面将讨论这个问题)),最后进行清理.
一般的,Eden和Survivor的比例是8:1;也就是说新生代的可用内存是整个新生代容量的90%(=80% + 10%).可以通过 -XX:SurvivorRatio
来设置比例.
标记-整理
图片来自CSDN
那么还是顾名思义…
标记-整理(Mark-Compact)分为两个部分:标记和整理,标记部分和"标记-清理"算法一样,整理就是将存活的对象都整理到一边,而后清理.
内存回收策略
图片来自网络 `
-
对象优先在Eden分配
-
大对象直接进老年代
-
长期存活的对象将进入老年代
垃圾收集器
Minor GC 和 Full GC
-
新生代GC(Minor GC)
-
老年代GC(Full GC / Major GC)
这样一标注应该就清楚多了.
下面主要通过比较两种参数情况下的垃圾回收机制进行分析.
测试代码:
|
|
UseSerialGC
参数设置为:
|
|
此时的GC日志:
|
|
注意到,新生代占用了差不多(大概其他的使用吧)4MB,而老年代却是6MB.这是为什么呢?
在给 allocation4
分配内存的时候,Eden(8MB)已经使用了6MB,因此无法分配.发生了一次Minor GC,而三个2MB的对象(都是存活的)无法一次性放入另一个Survivor(1MB)里.所以就要依赖老年代了.把这三个对象移到了老年代.
这次的GC结束后,4MB的 allocation4
就可以分配到Eden里了.
UseParallelGC
参数设置为:
|
|
GC日志为:
|
|
通过日志可以看到,老年代的内存占用为 4096kb=1024*4
.可以推测4MB的大对象直接进入了老年代.因此也没有触发GC.(对不对就不知道了(:3」∠❀)_)
不过新生代占用了8028kb(7.8MB差不多),加起来11.8MB比对象总的10MB大啊(逃)…