本文将涉及JVM的自动内存管理机制,虚拟机执行子系统等多个JVM知识领域,算是对《深入理解java虚拟机——JVM高级特性与最佳实践》的总结和补充。
Part1 自动内存管理机制
1.1java内存区域介绍
JVM运行时数据区:
1)程序计数器:(Program Counter Register)
一块较小的内存空间,用于记录当前线程所执行字节码的行号。字节码解释器就是通过改变这个计数器的值选取下一条需要执行的字节码指令。
java多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。在任何一个时刻,一个处理器(对应多核处理器的一个内核)都只会执行一条线程中的指令。为了线程切换后可以正确回到位置,每条线程都需要一个程序计数器。各线程的程序计数器互不影响,独立存储,我们称这类内存区域为“线程私有”内存。
线程执行Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,计数器值为空(Undefined),此内存区是jvm中唯一没有OutOfMemeryError的区域。
2)虚拟机栈:(VM Stacks)
内存中存储局部变量(基本数据类型,对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)),操作数,动态链接等信息的区域。线程私有,生命周期和线程一致。局部变量内存空间在编译期完成分配,进入方法在帧中分配多大局部变量空间是确定的,方法运行期不修改局部变量表大小。
两种异常:
StackOverflowError异常:线程申请的栈深度超过虚拟机允许的深度;
OutOfMemoryError异常:扩展栈时无法申请到足够的内存。
栈帧:(Stack Frame)
每个方法执行时创建,用于存储局部变量,操作数,动态链接等,一个方法调用到完成对应一个栈帧在虚拟机中入栈到出栈的过程。
Native Method Stack:
为JVM执行本地方法服务,作用与VM Stack类似。
3)堆(Heap):
线程共享,存放对象实例的内存,垃圾回收主战场。可以是物理上不连续的内存,逻辑上连续即可。无法扩展时OutOfMemoryError。
4)方法区(Method Area):
线程共享,用于存储被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据的内存区域。
5)运行时常量池(Runtime Constant Pool):
存放编译期间生成的各种字面量和符号引用。
1.2对象的创建
HotSpot虚拟机输入new之后:
类加载检查→为对象分配内存,初始化内存空间为零→对象设置
PS:
1)为对象分配内存:规整内存用指针碰撞,不规整用空闲列表。选择方式由JVM是否有压缩整理功能决定。
2)分配安全问题:同步分配动作或者同步指定TLAB(本地分配缓存),在TLAB上分配内存(TLAB分配时即初始化为零)。
3)对象设置:设置对象头,对象头分为存储自身运行时信息(哈希码,GC分代年龄,锁状态等)和类型指针。
1.3对象内存布局
HotSpot虚拟机中对象内存分三块:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
对齐填充仅起占位符作用。
1.4对象的访问定位
java需要通过栈上reference数据操作栈上对象。访问方式有两种:
1)句柄:指针的指针,用于物理地址变化的地址索引。划出一块内存作为句柄池,存放对象句柄地址,对象被移动时不修改reference。
2)直接指针:速度快
测试经验(Memory Analyzer):
1)如果是建立过多线程导致内存溢出,在不能减少线程数或更换64位虚拟机的情况下,只能通过减少最大堆和减少栈容量来换取更多线程。
2)方法区溢出常见于CGLib字节码增强,大量jsp或动态产生Jsp文件的应用,对于方法区的测试,常用思路是运行时产生大量的类去填满方法区,直到溢出。
Part2 垃圾收集(GC)器和内存分配策略
2.1引用算法:
1)引用计数法(Reference Counting):对象加个引用计数器,每引用加1,引用失效减1,为0时对象不可再使用。
特点:实现简单,效率高,但难以解决对象间相互循环引用
2)可达性分析算法(Reachability Analysis):通过一系列GCRoots的对象作为起点进行搜索,路径叫引用链。GCRoots不可达的对象是可回收对象。
2.2引用
1)强引用:代码中普遍存在,类似“Object obj=new Object()”这类引用,只要强引用存在,GC永远不会回收这些被引用对象。
2)软引用:有用非必需对象,在内存溢出异常前回收,SoftReference类实现
3)弱引用:非必需对象,存活到下一次垃圾回收前,WeakReference类实现
4)虚引用:一个对象有没有虚引用完全不影响其生存时间,唯一目的用于对象回收通知。
2.3对象的死亡
一个对象的死亡至少要经历两次标记:如果可达性分析后发现没有与GCRoots相连接的引用链,会被第一次标记并筛选:该对象是否覆盖了finalize()方法,finalize()是否被虚拟机调用过,这两种情况都是没必要回收的。若有必要回收就对对象执行finalize()方法,对象被放入F-Queue队列,并稍后由一个虚拟机建立,低优先级的Finalizers线程去执行它。但方法执行过慢的话会被中断。每个对象只能执行一次finalize()。
2.5回收方法区
1)无用的类:(需满足以下几个条件)
①该类实例均被回收
②加载该类的ClassLoader被回收
③该类对应的java.lang.Class对象没有在任何地方被引用
2)永久代垃圾主要回收包括:废弃常量和无用的类。回收废弃常量类似于回收堆中对象。
2.6垃圾回收算法
1)标记——清除(Mark-Sweep)
缺点:效率低;标记清除后空间碎片多
2)复制算法
转移活对象到另一块内存区
缺点:回收时空间代价较高
3)标记-整理算法(Mark-Compact)
标记后让对象移到内存区一端再清除
4)分代收集(Generational Collection)
根据对象周期划分内存。新生代每次回收大量对象死去,用复制算法;老生带对象存活率高,没有额外空间担保,用“标记-清理”或“标记-整理”。
2.7安全点
程序只能在安全点(Safepoint)执行GC。程序只有在方法调用,循环跳转,异常跳转等长时间执行才会产生Safepoint.。
在GC发生时让所有线程跑到最近的安全点上停下的方法:
1)抢占式中断(Preemptive Suspension):GC发生时中断所有线程,检查到某线程不在安全点上就恢复该线程让它跑到安全点再暂停
2) 主动式中断(Voluntary Suspension):GC要中断时,仅设置一个标志让线程轮询(和安全点或创建对象区域重叠),为真时挂起
安全区域(Safe Region):
指一段代码中引用关系不会发生变化,可以在任意地方GC。
2.8垃圾收集器
1)Serial收集器:主要用于运行与Client模式下的虚拟机,方式是stop the world。优点简单高效,缺点收集时要暂停其他所有工作线程。
2)ParNew收集器:相比Serial收集器多了多线程优点
3)CMS收集器:老年代收集器。标记-清理方式。第一个并发,实现了垃圾回收线程与用户线程同步。无法处理浮动垃圾。
4)Parallel Scavenge收集器:新生代收集器,CMS等收集器关注缩短回收时间,而它关注可控吞吐量(运行用户代码/(运行用户代码时间+垃圾收集时间))
5)Serial Old收集器
6)G1收集器:面向服务端,分代收集,标记-整理,垃圾收集停顿时间可控,并发并行,优先回收最有价值区域。
2.9其他
1)长期存活对象或大对象直接进入老年代(空间分配担保)
2)新生代老年代没有绝对界限,是动态调整的