开篇闲话:
c哥(c语言)统治着编程界达20年余久,横跨北美、欧洲、亚洲等大陆板块,c哥名气大是因为他能贴近硬件、高效运行。但是c哥也有两个让人难受的臭毛病:①指针(直接操作内存,高效)不提供越界检查工具;②自己创建的内存空间,自己释放!
java哥穿着一件蓝色T恤,犀利的眼神闪闪发光,嘴唇不算厚,但是声音却很有穿透力:“码农宝宝们,跟我混,对象销毁的事就不用你们管了,你们只需要创建对象就行啦”。java哥说完,摄影镜头竟然也颤抖了一下。
预备知识:
1、JVM运行时内存分配
JVM读书笔记篇一:如何管理4个G的“封地”
2、client模式和server模式
当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器。 C2比C1编译器编译的相对彻底,服务起来之后,性能更高。
3、并行和并发
并行(Parallel):在同一时刻有多条垃圾收集线程并行工作,当然,此时的用户线程是处于等待状态的。
并发(Concurrent):在一个时间段内,垃圾回收线程与用户线程同时执行(可能是并行,可能是交替执行),比如用户程序继续运行,而垃圾回收器运行在另一个CPU上。
4、吞吐量(Throughput)
吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间)
5、安全点(SafePoint)
GC时需要在某一快照状态下(某一时刻的运行状态),从而保持一致性(在分析期间对象引用关系保持不变)。此时会产生GC停顿(因为所有java线程都被停止了),如果数据量比较大的话,在进行GC Roots Tracing(对象引用链)查找对象引用关系时,停顿时间会很长,在HotSpot虚拟机中,使用OopMap这个数据结构(在编译字节码指令时指明偏移量+基地址的内存物理地址处有什么引用)来存储对象的引用。
有了这个数据结构仍然不能解决字节码指令执行时导致的引用关系变化,所以并不会为每条指令都生成OopMap,而是在“安全点”:也就是在特定的字节码指令处生成。安全点一般设置在“方法调用”、“循环跳转”、“异常跳转”等指令序列复用的地方(指令序列复用的意思是说,这些指令包含的指令流都是可以被反复调用的)
哪些内存需要回收?
篇一介绍了java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域与线程同生共死,栈中的栈帧空间大小是在类结构确定下来就已知的(最大栈深度、最大局部变量表个数)。所以这3个区域的内存分配和回收都是确定的。
方法区和堆的内存分配是不固定的,例如一个接口中的多个实现类、一个方法中的多个分支所需的内存都可能不同, 堆中的对象是在运行时才创建的。因此方法区和堆才是我们关注的重点。
如何判定对象是否存活?
对象存在的意义是被使用,而被使用在JVM中也有明确的定义,即对象引用。引用存在于函数栈帧中的局部变量表、操作数栈(指向堆)和常量池引用(指向方法区)。我们顺着引用便可以找到对象是否存活的依据了。有两种方式可以判定:
1、引用计数法:
方式:给每个对象添加一个引用计数器,每当有一个地方引用它时,计数器加1,失效时,计数器减1;为0时进行回收。很简单吧,但是它没法解决循环引用,请看:
public class Main {
public static void main(String[] args) {
GCObject obj1 = new GCObject();//obj1的计数器值为1
GCObject obj2 = new GCObject();//obj2的计数器值为1
obj1.object = obj2;//obj2的计数器值为2
obj2.object = obj1;//obj2的计数器值为2
obj1 = null;//obj1的计数器值为1
obj2 = null;//obj2的计数器值为1
}
class GCObject{
public Object object = null;
}
}
我们发现obj1和obj2永远都不会被回收。
可达性分析模型示意图2、可达性分析:
从GC Roots对象开始往下搜索,当一个对象到GC Roots没有任何引用链相连,也就是从GC Roots到这个对象不可达时,则证明此对象是可回收的,如下图的object5、object6、object7三个:
哪些对象可以做为GC Roots:
①虚拟机栈中引用的对象(局部变量表)
②方法区中静态属性引用的对象
③方法区中常量引用的对象
④本地方法栈中JNI引用的对象
怎么回收?
1、标记-清除 算法 (Mark-Sweep)
分为标记与清除两个阶段:标记出所有需要回收的对象,然后回收所有标记的对象。不足的地方有二:
①标记和清除两个过程效率都比较低,
②产生大量的空间碎片
2、复制算法
将内存容量均分两份A、B,每次只使用其中一块,当块A内存用完后,将存活的对象复制到块B,清理掉块A即可。缺点是内存浪费了50%。
3、复制算法的改进
将内存的比例大致划分为8:1:1的Eden和Survival1、Survival2。当回收时,将Eden和Survival中存活的对象复制到另外一块Survival中,这样内存的浪费比例只占10%。
适用范围:垃圾回收频次较高的新生代
备注:如果新生代的内存空间不足,则由老年代分配担保(即将对象分配在老年代中)。
4、标记-整理算法
标记后,让所有存活的对象往一端移动,然后清理掉边界以外的内存。
Mark-Compact
适用范围:垃圾回收频次较低的老年代
5、分代算法
根据对象存活周期分配在不同的内存区域新生代:对象创建后,大部分即会死去,采用复制算法;
老年代:对象存活率高,且没有额外的担保空间,采用标记清理或标记整理算法
内存分配与回收策略
1、对象优先在Eden分配:
当Eden区没有足够的空间时,虚拟机发起一次Minor GC。下面展示一个案例:通过-Xms20M -Xmx20M -Xmn10M设置堆初始大小为20M,新生代10M,老年代10M;当分配第四个对象时,由于空间不足会发起一次MinorGC,通过-XX:+PrintGCDetails打印GC日志:
/**
*vm参数 -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 GCTest
*/
public class GCTest {
private static final int _1MB=1024*1024;
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3,allocation4;
allocation1=new byte[2*_1MB];
allocation2=new byte[2*_1MB];
allocation3=new byte[2*_1MB];
allocation4=new byte[4*_1MB];
}
}
运行结果:
[GC [PSYoungGen: 7307K->480K(9216K)] 7307K->6624K(19456K), 0.0072860 secs] [Times: user=0.01 sys=0.01, real=0.00 secs]
Heap
PSYoungGen total 9216K, used 7143K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 87% used [0x00000000ff600000,0x00000000ffcf9fc8,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 80% used [0x00000000fec00000,0x00000000ff400010,0x00000000ff600000)
PSPermGen total 21504K, used 2621K [0x00000000f9a00000, 0x00000000faf00000, 0x00000000fec00000)
object space 21504K, 12% used [0x00000000f9a00000,0x00000000f9c8f610,0x00000000faf00000)
2、大对象直接进入老年代
大对象:需要大量连续内存空间的对象,最典型的大对象就是那种很长的字符串及数组(如上面的例子中byte数组对象)。
虚拟机提供-XX:PretenureSizeThreshold参数设置在老年代进行分配
3、长期存活的对象进入老年代
每个对象都有一个年龄(age), 在Mark Word中
如果age > MaxTenuringThreshold(默认为15), 晋升老年代
4、动态对象年龄判断:
新生代——>老年代如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半, 年龄大于或等于该年龄的对象可以直接进入老年代。
5、方法区的回收(HotSpot虚拟机中将方法区放在永久代中,1.7已将字符串常量池从永久代中移除)
①废弃无用的常量
例如:常量池中的字符串 “abc” 不再被任何字符串引用, 可以清除掉
②无用的类回收条件:
1、该类的所有实例都被回收;
2、加载该类的Classloader 已经被回收;
3、该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
垃圾收集器
Hotspot7种垃圾收集器垃圾收集器是垃圾回收算法的具体实现,用户体验与运行效率之间的博弈在不同场景下都有需求,所以这些高度封装的收集器本质上并没有好坏,在不同场景下,他们都能独当一面。
以下是JDK1.7 update 14之后Hotspot虚拟机的七种收集器,分别作用于不同分代:
1、Serial收集器
算法:新生代采用复制算法,暂停所有用户线程,老年代采用标记-整理算法,暂停所有用户线程;
特点:①单线程运行(第一层意义:它是运行在一个CPU核或一个收集线程,第二层意义:Stop the World,回收时,所有用户线程必须停掉);②不用关心线程之间的交互,运行效率高,是client模式下默认的新生代收集器。
2、ParNew收集器
是Serial的多线程版本,可通过-XX:ParallelGCThreads参数来限定线程数量
3、Parallel Scavenge收集器
算法:复制算法;
特点:是一个新生代收集器,关注点在达到一个可控的吞吐量。有两个参数来控制:①-XX:MaxGCPauseMills最大垃圾收集停顿时间,②-XX:GCTimeRatio直接设置吞吐量大小
4、Serial Old收集器
是Serial收集器的老年代版本,应用于Client模式
算法:标记-整理
5、Parallel Old收集器
Parallel Scavenge收集器的老年代版本,之前Parallel Scavenge只能与Serial Old,无法与CMS配合工作,导致在server模式下,由于Serial Old性能的“拖累”,导致整体应用上吞吐量获得最大化的效果。在JDK 1.6中开始提供Parallel Old配合来解决这个问题
算法:多线程+标记-整理
6、CMS收集器(Concurrent Low Pause Collector)
CMS收集器基于标记-清除算法,应用于老年代。收集过程分为初始标记、并发标记、重新标记、并发清除四个阶段:
①初始标记、重新标记这两个步骤仍然需要Stop the World;
②初始标记阶段仅仅是对GC Roots能直接关联到的对象进行标记;
③并发标记进行GC Roots Tracing的过程,即按照引用链进行标记;
④重新标记是为了在并发标记期间因用户线程继续运行而导致标记变动的标记修正;
特点:
CMS收集器运行示意图以获取最短回收停顿时间为目标
互联网网站, B/S服务器端
容易产生碎片
7、G1收集器(Garbage-First)
面向服务端应用的垃圾收集器,成熟版基于JDK1.7;
可针对年轻代和老年代;
采用标记-整理算法
特点:
G1收集器运行示意图充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间;
G1收集器可收集新生代与老年代两种,不需要其他收集器配合就可以独立管理整个GC堆;
建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒
网友评论