深入理解Java虚拟机:JVM高级特性与最佳实践
第2章 Java内存区域与内存溢出异常
运行时数据区域
程序计数器
当前线程所执行的字节码的行号指示器,线程私有
Java虚拟机栈
线程私有,描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储信息(局部变量表等)
局部变量表存放了各种基本数据类型、对象引用类型。
- 线程请求的栈深度大于虚拟机所允许的深度,抛出
StackOverflowError异常 - 虚拟机扩展时无法申请到足够的内存,抛出
OutOfMemoryErro异常
虚拟机栈为虚拟机执行Java方法(字节码)服务
- 线程请求的栈深度大于虚拟机所允许的深度,抛出
本地方法栈
本地方法栈为虚拟机使用到的Native方法服务
Java堆
所有线程共享,存放对象实例,垃圾收集器管理的主要区域
新生代 : 老年代 = 1:2
新生代(1/3堆空间)
Eden区 : from Survivor : to Survivor = 8:1:1
- Eden (8/10新生代空间)
- From Survivor (1/10新生代空间)
- To Survivor (8/10新生代空间)
老年代(2/3堆空间)
方法区
线程共享
- 运行时常量池
永久区从jdk7开始逐步移除,在jdk8中存储在元空间中
第3章 垃圾收集器与内存分配策略
- 哪些内存需要回收
- 什么时候回收
- 怎么回收
判断对象是否存活
引用计数算法
很难解决对象之间相互循环引用的问题
可达性分析算法
通过一系列GC Roots对象作为起始点,从这些节点开始向下搜索。从GC Roots到某个对象不可达时,证明此对象不可达
引用
强引用
Object obj = new Object()强引用存在,不会被垃圾收集器回收软引用
发生内存溢出异常之前,被列进回收范围
弱引用
无论当前内存是否足够,垃圾收集器都会回收
虚引用
最弱的引用关系
宣告对象死亡
如果对象进行可达性分析后发现不可达,被第一次标记并进行一次筛选。筛选条件为此对象是否有必要执行finalize()方法。对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,视为没有必要执行。
如果对象被判定有必要执行finalize()方法,对象被放置在队列中,稍后被执行。”执行”指虚拟机触发这个方法,但并不承诺等待它运行结束。
如果对象重新与引用链上任何一个对象建立关联,第二次标记时将被移除出即将回收的集合;否则被真的回收。
垃圾收集算法
标记-清除算法
最基础的收集算法
不足
- 标记和清楚两个过程效率都不高
- 产生大量不连续的内存碎片,导致分配较大对象时提前出发另一次垃圾收集动作
复制算法
内存按容量划分为大小相等的两块,每次只是用其中的一块。当一块内存用完,将还存活的对象复制到另外一块上面,再把已使用过的内存空间一次清楚掉。每次都是对整个半区进行内存回收。
缺点:内存缩小为原来的一半
新生代对象大多数生命周期短,所以将内存分为一块较大的Eden空间和两块较小的Survivor空间。每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一块Survivor空间上,最后清楚掉Eden和刚才使用过的Survivor空间。
虚拟机默认Eden和Survivor大小比例是8:1。
Survivor区域有两块的原因:避免只有一块survivor区域中垃圾回收后产生碎片化
标记-整理算法
标记与标记-清除相同,后续让所有的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法
- 新生代:只有少量对象存活,选用复制算法
- 老年代:对象存活率高,使用标记-清除或者标记-整理
垃圾收集器
新生代垃圾收集器
Serial收集器
单线程,进行垃圾收集时,必须暂停其他所有工作线程;复制算法
ParNew收集器
Serial收集器的多线程版本
Parallel Scavenge收集器
可控制的吞吐量
老年代垃圾收集器
Serial Old收集器
Serial收集器的老年代版本,单线程,标记-整理算法
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,多线程,标记-整理算法
CMS收集器 Cuncurrent Mask Sweep
标记-清除算法,目标为获取最短回收停顿时间
- 初始标记:标记GC Roots能直接关联到的对象,速度快
- 并发标记:GC Roots Tracing
- 重新标记:修正并发标记期间因用户程序继续运作而到导致标记产生变动的那一部分对象
- 并发清除
初始标记、重新标记需要Stop The World;总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
缺点
- 对CPU资源敏感
- 无法处理浮动垃圾
- 大量空间碎片产生
G1收集器
- 并行与并发
- 分代收集
- 空间整合:从整体来看基于标记-整理算法,从局部(两个Region之间)来看基于复制算法
- 可预测的停顿
注意
- 并行:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
- 并发:用户线程与垃圾收集线程同时执行,用户程序在继续运行,而垃圾收集程序运行于另一个CPU上
内存分配与回收策略
对象优先在新生代Eden区分配,Eden区没有足够空间分配时,虚拟机发起一次Minor GC
大对象(需要大量连续内存空间的Java对象)直接进入老年代
长期存活的对象进入老年代
对象在Survivor区每次经过一次Minor GC,年龄增加一岁。增加到一定程度(默认15岁),晋升到老年代
动态对象年龄判断
Survivor空间中相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象可以直接进入老年代
空间分配担保
第7章 虚拟机类加载机制
虚拟机的类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
类加载的过程
加载
通过一个类的全限定名获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转换成方法区的运行时数据结构,在内存中生成一个代表这个类的
java.lang.Class对象,作为方法区这个类的各种数据的访问入口验证
确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
准备
正式为类变量(被static修饰的变量)在方法区中分配内存并设置类变量的初始值(基本数据类型的零值,
ConstantValue除外)解析
虚拟机将常量池内的符号引用替换为直接引用
初始化
开始执行类中定义的Java程序代码(字节码)
类加载器
双亲委派模型
三种系统提供的类加载器
- 启动类加载器:C++语言实现,是虚拟机自身的一部分
- 拓展类加载器:
\lib\ext,开发者可以直接使用扩展类加载器 - 应用程序类加载器
类加载器之间的层次关系称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类都应当有自己的父亲加载器。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父亲加载器去完成。所有的加载请求最终应该传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求时,子类才会尝试自己去加载。
破坏双亲委派模型
- 重写
findClass()方法