Java运行时内存区域
程序计数器,线程独占,当前线程所执行的字节码的行号指示器,每个线程需要记录下执行到哪儿了,下次调度的时候可以继续执行,这个区是唯一不会发生oom的
栈,线程独占,包含虚拟机栈或native method stack,用于存放局部变量的
堆,线程共享,用于分布对象实例的,后面说的内存管理和垃圾回收基本都是针对堆的
方法区,线程共享,用于存放被虚拟机加载的类,常量,静态变量; Java虚拟机规范,把方法区描述为堆的逻辑部分,所以也被称为“永久代”,在大量使用反射,动态代理,ClassLoader的场景下,要考虑永久代的回收
对于一个简单的,对象生成
Object obj = new Object();
会涉及到3个区,
图中描述两种对象访问方式,有句柄,可以屏蔽对象实际地址的改变(gc时对象地址经常会改变),不用句柄效率更高
垃圾回收
垃圾判定
对于如何判定对象是垃圾,教科书的答案是引用计数,并且也在COM,python中得到较好的应用
引用计数的问题是,难以解决循环引用的问题,
A.instance = B; B.instance =A
这样会导致A,B的引用计数都不为0
所以对于Java,C#,Lisp都是采用GC Roots Tracing的方式,
原理也很简单,就是选取一系列GC Roots对象,只保留从root对象可达的对象,其余都是垃圾
Java中的GC roots包含,
- JavaStack中的引用的对象
- 方法区中静态引用指向的对象
- 方法区中常量引用指向的对象
- Native方法中JNI引用的对象
垃圾回收算法
Mark-Sweep,最原始的想法,先标记出所有垃圾对象,然后回收掉;问题是,效率低,而且会产生大量内存碎片
Copying,最简单的copying,把内存分两半,先用一半,然后回收时,把有效对象copy到另外一半
这个首先只适用于年轻代,即垃圾对象占较高比例的case
再者,空间太浪费了,只能用一半
所以,现在实际使用的版本为,分为一个较大的eden区,两个较小的survivor区
比例一般为8:1:1,其中一个survivor区是空的,这样只浪费10%的空间
能这样做的前提就是,每次只有最多10%的对象存活
对象先放到eden区,eden区满了,进行minor gc,把eden区和有数据的survivor区的存活对象,放到另一个空的survivor区中
两个survivor区,虽然命名为from和to,但是其实没有任何区别,完全对等的
当如果survivor区的空间不够,就需要放到年老代(称为handle promotion,即年老代要为survivor提供担保,你那空间不够,可以放我这,类似贷款担保),如果年老代的空间也不够或不能接受Handle promotion失败,就需要full gc去回收年老代
Mark-Compact,前面说copying只适用于存活对象比例较低的case,所以适合年轻代,但对于年老代这样的,用copying肯定是不行的;
Mark-compact,mark还是一样的,只是在回收时,会把对象做平移,消除碎片
垃圾收集器
Serial收集器
最简单的,单线程,收集时会stop the world,所有用户线程暂停
可以用于收集新生代或老年代
收集新生代,用copying算法
收集老年代,用标记-整理,称为serial old
简单高效,适用于单CPU环境;是虚拟机Client模式的默认收集器
缺点,会stop the world
ParNew收集器
只是serial的多线程版本,适用于多核环境,如果在单核的机器上,效率还不如serial
优势是,可以配合CMS收集器使用,因为CMS作为老年代的收集器,只能配合Serial或ParNew作为新生代的收集器
CMS(Concurrent Mark Sweep)收集器
以最短停顿时间为目标的收集器,适合用于网站或B/S系统的服务端,重视服务响应速度的场景。
该收集器相对比较复杂,整个过程分为,
1. 初始标记(CMS initial mark),stop the world,标记GC Roots直接关联到的对象,速度很快
2. 并发标记(CMS concurrenr mark), 并行进行GC Roots Tracing的过程
3. 重新标记(CMS remark),stop the world,由于并发标记和用户线程是并发执行的,所以需要对标记进行最后的修正,消耗时间会大大小于并发标记时间
4. 并发清除(CMS concurrent sweep),最后进行sweep
缺点,
a. CPU敏感,频繁GC会导致CPU卡死
b. 无法处理浮动垃圾(floating garbage),在并发清理阶段产生的新垃圾无法在这次完成回收
c. 需要预留较大的内存,由于CMS收集过程是和用户应用并发进行的,所以不能等到老年代快被占满再做,需要提前进行收集,默认是设为68%,这是比较保守的设定,可以减少以降低gc的次数;但是如果在CMS收集过程中,出现用户应用程序内存不够的情况,会发生”Concurrent Mode Failure”,这样虚拟机只能用后备方案,serial old收集器进行年老代的收集(因为应用已经无法并发执行),这样应用停顿时间就会很长,所以需要设置合理的比例
d. 因为采用sweep,会有大量碎片
Parallel Scavenge收集器
新生代收集器,设计目的是达到可控制的throughput,CPU运行用户代码时间/CPU总消耗时间,说白了,就是保证CPU更多的执行用户代码而非gc
适合后台运算,不太需要交互的场景
但鱼和熊掌不可兼得,throughput和GC停顿时间是需要tradeoff的
降低新生代的空间大小,缩小gc的间隔,都可以减少GC的停顿时间,但也会降低throughput
并且该收集器,支持UseAdaptiveSizePolicy参数, 这样不需要使用者指定新生代大小,Eden和Survivor比例,年老代晋升年龄等,虚拟机会根据运行性能监控去优化调整
Parallel Old收集器
前面提到的parallel sacvenge收集器是无法与CMS收集器配合使用的,所以之前只能配合Serial Old来收集老年代,导致效率低
Parallel Old作为Serial Old的多线程版本,可以更好的和parallel sacvenge收集器配合,真正达到throughput优先的收集
G1(Garbage First)收集器
新一代的收集器,不同于之前的收集器,会收集整个新生代或老年代
G1会把整个Java堆划分为多个固定大小的区域,并跟踪垃圾堆积程度,并每次收集垃圾最多的区域,可以大大提高收集效率
JVM收集器对应参数
内存分配和回收策略
对象往往在新生代的Eden区分配,Eden区空间不够,发起minor gc,会将eden和一个survivor区的存活对象copy到另一个survivor区,如果另一个survivor区空间不够,存入老年代
大对象会直接进入老年代,比如很长的字符串或很大的数组,大对象对于JVM内存分配是个坏消息,因为大对象需要找到连续内存,否则会触发gc,所以短命的大对象是需要尽量避免的
长期存活的对象进入老年代,对象在新生代每经历一次minor gc,年龄加1, 默认达到15岁会进入老年代
每次Minor GC时,虚拟机会检测每次晋升到老年代的平均大小是否大于老年代当前剩余大小,如果小于,则进行full gc