背景
以前看了周志明的《深入理解Java虚拟机》,今天突然想谈起它来。
所以今天这篇文章会说说 java的内存分配策略和垃圾收集器。
提出问题
java 中内存如何申请的,什么样的数据存放在新生代,什么样的数据存放在老生代,什么样的数据存放在永久代。
数据的回收:这3个区域的数据是如何回收的呢?各使用什么样的垃圾收集器。
今天的文章围绕他们解决。
对象如何被转移
这里有两个概念:MinGc:新生代GC, Major GC:老生代GC。
新生代往老生代转移的原则是:
1).在进行15次 MinGC还存活下来的就转移到 Old GC。
2).如果对象太大,直接放到 Old区。
3).如果MinGC 过程中,Survivor放不下会被担保到Old区。
当Eden区满了就会触发MinGC,当Old区满了就会触发Major GC。
为什么会有2个Survivor,是由于必须经过15次MinGC才会转移到Old GC,起到转腾的作用。
比如 第一次MinGC:Eden +Suv1 -> Surv2,第二次Min GC: Eden + Surv2 -> surv1。
这里虚拟机为每个对象定义了年龄计数器,当年龄大于阀值得时候就进入老年代。
大对象直接进入老年代,大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
堆内存如何被申请
虚拟机如何执行 Class1 c1 = new Class1()这条指令。
在执行这条指令的时候要做两件事情:
1.检查这个指令的参数是否能在常量池中定位到一个符号引用,并且检查这个符号引用是否被加载、解析、初始化。如果没有必须先执行类加载过程。
2.申请内存,内容申请需要看堆中内存是否连续,如果是连续通过移动指针头(称为指针膨胀)。如果不连续的话就通过“空闲列表”定位。 而内存是否连续跟垃圾回收算法是否带有压缩功能有关。
serial和parNew带有压缩功能,而CMS基于mark-sweep算法的收集器,通常采用空闲列表。
分配空间的过程中,还需要考虑一个问题是。如果多个线程同时修改指针会出现并发的问题。
虚拟机采用两种办法 a).CAS+失败重试(CAS就是我们所说的乐观锁机制)。 b).内存分配的动作划分到不同的线程空间中。
3.内存申请完之后就是初始化。
这里涉及到对象的内存布局,对象分为对象头(如hashcode,对象类型指针)、实例数据、对象填充。
对象如何被定位呢?
我们知道对象的引用放在栈里,所以我们有两种方式引用到堆里的内存。
1.栈里的reference 直接指向 堆里的对象。
2.栈里reference 指向堆里 对象句柄(对象句柄这个地址是固定的),对象类型数据指针是放在 对象句柄中的,所以当对象移动了只会修改 对象句柄中实例数据指针。
由于使用句柄的方式多了一次指针定位的开销,所以比第一种会慢一些。
什么样的数据会被回收?
简单的说就是无用的数据应该被回收,什么是无用的呢?
怎么判定无用:市面上有
1).引用计数法(解决不了循环引用的场景 A与B互相引用)
2).可达性分析算法
这个算法就是通过 一系列称为 "GC Roots"的对象作为起点,搜索锁走过的路径称为引用链。当一个对象到GC Roots没有任可达路径则会被回收。
那什么是GC Roots对象呢?
GC Roots对象包含如下几点:a.虚拟机栈中引用的对象。 b.方法去中类静态属性引用的对象。 c.方法区中常量引用的对象。 d.本地方法栈中JNI引用的对象。
什么时候被回收
什么时候被回收是根据其引用的类型来决定的,java中的引用分为以下几种。
1.强引用 2.软引用 3.弱引用 3.虚引用
强引用就是类似于 Class1 c1 = new Class1(); 永远不会被回收
软引用用来描述有用但是非必须的,在内存溢出的会被回收。 java中通过SoftReference实现软引用。
弱引用强度比软引用更加的弱一些,再下一次垃圾回收时就会被回收。
虚引用,我们不会通过虚引用来获取一个对象也不会对生存时间构成影响,它的作用就是在被回收掉后收到一个系统通知。
怎么被回收
垃圾回收算法
这里描述使用什么算法及其如何回收内存。
垃圾回收算法应该考虑以下几点:
1).回收的效率和时间。
2).垃圾回收的时候是否影响正在运行的程序。
我们先不考虑分代的问题,垃圾回收就是想把没有用的清楚掉把有用的留下来。对于无用的对象,我们可以标记清楚 - 标记清楚法。 对于有用的对象我们可以采用标记整理法 或者 复制法。
这里提一下复制法:新生代中包含(Eden + Survivor1 + Survivor2),每次会把Eden +Survivor中的存活对象移动到另一个 Survivor中,如果另一个Survivor空间不够就需要老生代担保。
而实际jvm中是分为新生代、老生代。对于新生代由于回收频率高存活对象比较少,所以采用复制法。对于老生代由于对象存活率高没有额外的区域进行担保所以使用(标记清除、标记整理)法。
垃圾回收器
前面谈到了收集算法,但是垃圾具体是通过收集器进行回收的。市面上最简单的垃圾收集器是serial收集器,由于其简单专心做垃圾收集适用于运行在client模式下的虚拟机(新生代也只有几十到100多兆)。
a).parNew是Serial的多线程版本,其余行为包括垃圾收集算法(复制算法)都与Serial收集器一致。parNew是运行在server下的首选新生代收集器。
b).Cocurrent Mark-sweep,这个是虚拟机真正意义上第一款并发收集器,由于其使用Mark-sweep算法所以其适用于老生代。
c).Parallel Scavenge 提供两个参数MaxGCPauseMillis,GCTimeRatio来设置虚拟机优化目标,该收集器注重的的是吞吐量,对于与用户交互少但是有很多后台程序比较适合。 -- 老生代还是新生代???
d).Serial old 收集器是Serial的老年代版本(使用标记-整理算法), Parralle Scavenge无法与CMS工作只能与Serial Old配合一起工作。这是parralle old收集器出现它采用的是标记-整理 算法
d).CMS收集器,为什么Cocurrent特性呢?仔细分析然来CMS收集器把收集过程分为4个子阶段:
1).初始标记 -- 把GC Root对象找出来
2).并发标记 -- GC Roots tracing
3).重新标记 -- 把进行 并发标记 那一段时间,标记变动的那一部分重新标记,这个阶段比初始标记时间长比并发标记时间短,但是必须停止用户线程。
4).并发清除
初始标记、重新标记是需要”stop the world“,并发标记和并发清除这两个阶段还是可以垃圾回收线程与用户线程一起执行。由于CMS是并发的执行收集工作,所以它是一个很耗资源的收集器,会使得用户程度整体执行时间长(虽然瞬时停顿感少了)。
由于CMS收集器与用户线程一起工作,所以不能像其他收集器比如par Old一样等到老生代占到100%在启用,一般等到60%就启用CMS收集器,这里有一个参数配置-XXCMSInitiatingOccupancyFraction。平常我们可以提高这个参数来降低CMS启动收集次数,但是如果比率过高,预留的空间不足用户线程使用会出现“Cocurrent Mode Failure”,这样的话性能反而会降低。
还有一个问题CMS容易产生碎片,如果需要进行碎片整理(无法并发)的话必须停止用户线程。我们通过一个参数CMSFullGCBeforeCompaction来控制执行多少次无压缩的收集然后执行一次带压缩的收集。
e).G1收集器
G1收集器在多核CPU等可以充分利用硬件环境,它是面向服务端的垃圾回收器。采用的是“标记-整理”的方式,所以没有碎片的出现。
G1收集器的堆内存与其他收集器有很大的区别,它将整个堆划分为很多region。G1能够建立预测模型会跟踪每个Region垃圾回收价值大小(回收所获得的空间大小和及回收所需时间的经验值),然后选一个价值最高的region进行回收(这是Garbage-First 名称的由来)。
堆被分成许多region也会带来一个问题,如果一个region A被其他region B引用,当我回收A的时候我还要扫描整个堆,把引用A的regin扫描出来。所以G1采用空间换时间的办法即提供一个rememerSet的数据结构来维护这些引用关系。
如何查看垃圾收集器日志
开启GC日志:参数 - XX:+PrintGC(或者 - verbose:gc)开启了简单 GC 日志模式,
简单GC日志不会打印具体使用的算法,比如下面日志:
[GC 246656K->243120K(376320K), 0.0929090 secs]
[Full GC 243120K->241951K(629760K), 1.5589690 secs]
第一行表示执行的MinGC 堆空间使用从246656K 到 243120K 堆的总大小为376320K,执行时间消耗0.0929090 secs。
第二行表示执行了FUll GC堆空间从243120K 到 241951K,执行时间消耗1.5589690 secs。
这里没有描述执行GC的时间,数据有没有从新生代转到老生代,也没有显示具体使用的算法。
如果想要看详细的GC日志,则-XX:PrintGCDetails,这时候会打印出详细日志:
如下描述的是执行了一次新生代GC,新生代使用由142816K减少到10752K。堆的总容量大小由246648K减少到243136K。
[GC
[PSYoungGen: 142816K->10752K(142848K)] 246648K->243136K(375296K), 0.0935090 secs
]
[Times: user=0.55 sys=0.10, real=0.09 secs]
对于FullGC 详细日志如下:
打印了每一代的堆使用情况变化,其中PsYoungGen 9707 +ParOldGen 232244 = 堆的总使用大小 241951.
[Full GC
[PSYoungGen: 10752K->9707K(142848K)]
[ParOldGen: 232384K->232244K(485888K)] 243136K->241951K(628736K)
[PSPermGen: 3162K->3161K(21504K)], 1.5265450 secs
]
如果想打印出时间参数可以使用- XX:+PrintGCTimeStamps 可以将时间和日期也加到 GC 日志中。
如果指定了 - XX:+PrintGCDateStamps,每一行就添加上了绝对的日期和时间。
2014-01-03T12:08:38.102-0100: [GC 66048K->53077K(251392K), 0.0959470 secs]
2014-01-03T12:08:38.239-0100: [GC 119125K->114661K(317440K), 0.1421720 secs]
2014-01-03T12:08:38.513-0100: [GC 246757K->243133K(375296K), 0.2761000 secs]
如果想输出到文件 就加上 -Xloggc
写完后的想法
对这块知识,边看书边收集资料整理理解。
下一篇文章将通过代码具体模拟触发MinGC,及其通过GC 日志分析,查看JVM活动的细节。
这是我个人面对这个问题的逻辑推导不是粘贴别人的答案花费了我大半天的时间但是很值。