第二部分 自动内存管理
第2章 Java内存区域与内存溢出异常
运行时数据区域,如图所示。
1.运行时数据区
1.程序计数器
2.虚拟机栈
在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;
- 如果Java虚拟机栈容量可以动态扩展(Hotspot不支持),当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。Hotspot不支持,但如果申请时就失败,仍会出现OOM异常,
3.本地方法栈
有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。
与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出StackOverflowError和OutOfMemoryError异常。
4.堆
如果在Java堆中没有内存完成实例分配,并且堆也无法再扩展时,Java虚拟机将会抛出OutOfMemoryError异常。
5.方法区
逻辑概念。
用于存储已被虚拟机加载的:
- 运行时常量池
- 类型信息、
- 静态变量、常量、
- 即时编译器编译后的代码缓存
- 其他数据。
方法区实现:
- JDK6,是用永久代实现。
- JDK7,把永久代的符号引用移至native heap。运行时常量池、字符串常量池、static静态变量移到Java堆(Class对象也在Java堆)。Intern 字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上。
- JDK8,把永久代中剩余的内容(主要是类型信息,即类的元数据信息)全部移到元空间,完全废弃永久代。元空间大小设置参数:
-XX:MetaspaceSize
。元空间存储初始加载的类的描述信息, 意味着它只存储原始类的信息。 - Class对象是根据类型信息产生的。方法区是一个逻辑区域。JDK6及之前,方法区确实是由永久代实现。JDK7及以后,就纯粹是一个逻辑上的表述了。因为1.7就字符串常量池等就移到堆上了。
6.运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。
主要用来存储编译期的Class文件中的常量池表、运行期间的直接引用、运行期间的新常量。
字符串常量池是再运行时常量池上?
7.直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
在JDK 1.4中新加入了NIO(New Input/Output)类,它可以使用Native函数库直接分配堆外内存。
2.HotSpot虚拟机对象
1.对象的创建
JVM遇到一条字节码new指令时,做以下动作:
- 先检查是否有该类,再检查该类是否初始化:
- 为新生对象分配内存
- 设置对象头信息
- 按构造函数初始化对象
2.对象的内存布局
可以划分为三个部分:
- 对象头(Header):1.对象自身的运行时数据。2.类型指针
- *实例数据(Instance Data)
- 对齐填充(Padding)。
3.对象的访问定位
主流的访问方式有两种:
- 使用句柄:Java堆中划分出一块内存来作为句柄池
- 直接指针:reference中存储的直接就是对象地址。
Hotspot主要使用直接指针方式访问。
3.模拟OOM和栈溢出异常
溢出总结
- 堆溢出:OOM:List无限新增对象。
- 栈溢出:OOM:无线申请线程。StackOverflowError:递归无限申请新方法。
- 方法区(包含运行时常量池)溢出:大量加载类信息(运行时创建新类)。或大量String.intern()。
- 本机直接内存溢出:直接用
Unsafe::allocateMemory()
来大量申请直接内存。
一般由直接内存导致的OOM,Heap Dump文件中看不到什么明显的异常。如果Dump文件很小,程序中又直接或间接用到了DirectMemory
(典型的间接使用就是NIO),那么就要考虑下是不是直接内存溢出。
堆OOM排查:
内存泄漏:垃圾收集器无法回收。
内存溢出:内存不够用。如果不是内存泄漏,换句话说就是内存中的对象确实都是必须存活的,那就应当检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
第3章 垃圾收集器与内存分配策略
1.如何回收对象
垃圾回收需要完成的三件事情:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
确定哪些对象需要回收主要是两种算法:
- 引用计数算法:循环引用问题。主流的Java虚拟机都没有用它来实现。
- 可达性分析算法:通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索引用链。如方法堆栈中使用到的 参数、局部变量、临时变量等,Java类的引用类型静态变量,字符串常量池(String Table)里的引用等。
各种引用:
- 强引用:最传统的引用定义。
- 软引用:发生内存溢出异常前,才会考虑回收它们。SoftReference类实现软引用。
- 弱引用:只能生存到下一次GC发生。WeakReference类来实现软引用。
- 虚引用:虚引用的唯一目的只是为了能在对象被GC时收到一个系统通知。虚引用不会对对象的生存时间有任何影响,也无法通过虚引用来取得一个对象实例。PhantomReference类来实现。
怎么干死对象?
可达性分析算法判定不可达,此时对象还没死。要干掉一个对象,至少要经历两次标记过程:
- 第一次标记:在进行可达性分析后发现没有与GC Roots相连接的引用链。
- 第二次标记:无需执行finalize()方法,即对象没有覆盖finalize()方法,或者finalize()方法已经被JVM调用过。
2.垃圾回收算法
1.分代收集理论
这个理论实际上是经验法则,建立在两个分代假说上:
- 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
- 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
2.标记-清除算法
一般用于老年代。
3.标记-复制算法
一般用于新生代。
改进后的算法:Eden + Survivor + 老年代担保:新生代分为一块较大的Eden空间和两块较小的Survivor空间(Hotspot默认8:1:1)。另外为了防止回收时Survivor装不下所有的对象,老年代提供了担保,多余装不下的对象直接进入老年代。
4.标记-整理算法
算法:
- 标记:标记过程仍然与“标记-清除”算法一样。
- 整理:清除时不是直接清除,而是让所有存活对象向内存空间一侧移动,然后清除边界外的内存。
标记整理算法,移动对象时,用户应用程序必须暂停,但是加快了整体的访问速度(因为没有复杂的内存访问方式)。
标记清除算法,虽然降低了延迟(因为不用Stop the world),但是也降低了吞吐量(因为内存访问更耗时了,总体上是更耗时的)。
基于以上考虑,HotSpot虚拟机里面关注吞吐量的Parallel Scavenge收集器是基于标记-整理算法的,而关注延迟的CMS收集器则是基于标记-清除算法的。
还有一种“和稀泥式”解决方案:平时多数时间都采用标记-清除算法,内存碎片太多,就用用标记-整理算法收集一次。基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法。
另外标记整理算法不一定要“Stop the world”:最新的ZGC和Shenandoah收集器使用读屏障(Read Barrier)技术实现了整理过程与用户线程的并发执行,稍后将会介绍这种收集器的工作原理。然后“标记-清除”算法其实也是需要停顿用户线程来进行标记、清除,只不过这个时间开销很小。
3 Hotspot算法细节
1.根节点枚举
所有的GC收集器,在“枚举根节点”这一步都需要暂停用户线程(在一个快照中)
OopMap的概念:
在HotSpot中,对象的类型信息里有记录自己的OopMap,记录了在该类型的对象内什么偏移量上是什么类型的数据。所以从对象开始向外的扫描可以是准确的;这些数据是在类加载过程中计算得到的。
2.安全点
那么我们只有在特定的位置生成OopMap了,这些位置就是安全点(Safepoint)。那么也就是说,只能在安全点才能进行GC,因为只有安全点有OopMap信息,枚举需要根据OopMap来判断哪些变量是引用类型,从而快速找到对象。
3.安全区域
安全区域。安全区域是指在这一段代码片段之中,引用关系不会发生变化。那么这个区域里任何地方都可以GC。
4.记忆集与卡表
为了解决跨代引用,GC收集器会在新生代建立记忆集(Remembered Set)。
卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。
5.写屏障
卡表实现了记忆集,缩小了GC Roots扫描范围。那么卡表元素如何维护呢?例如它们何时变脏、谁来把它们变脏等?
HotSpot通过写屏障来实现编译执行时卡表的维护。写屏障可以看作在虚拟机层面对“引用类型字段赋值”这个动作的AOP环形切面。
6.并发的可达性分析
- 把原本存活的对象错误标记为已消亡:这个无法接受。
以下是示意:
要解决对象消失问题,只需破坏上面两个条件中的一个就行,包括两种解决方案,选一个就行:
- 增量更新:破坏第一个条件,黑色指向白色后,它自己要变回灰色。
- 原始快照:破坏第二个条件,只按原始快照扫描,后面删除的线都不认。
4.经典垃圾收集器
1.综述
- 串行:新生代Serial收集器(标记复制) + 老年代Serial Old收集器(标记整理)。
- 并行:新生代ParNew收集器(标记复制)。Serial收集器的多线程版本。除使用多线程GC之外,其余行为和控制参数与Serial完全一致。
- 吞吐量优先:新生代Parallel Scavenge收集器(标记复制)。“标记-复制”算法。没有复杂的内存分配制度。老年代Parallel Old收集器(标记整理)
- CMS(Concurrent Mark Sweep):老年代收集器(标记-清除),首次实现了让垃圾收集线程与用户线程(基本上)同时工作。
CMS等收集器关注的是GC时减少用户线程停顿时间,更适用于交互多的程序。而Parallel Scavenge关注的则是吞吐量可控,因此可以最高效率地利用处理器资源,适合在后台运算而不需要太多交互的分析任务。
2.CMS
跟其他收集器比起来,CMS复杂一些。整个过程分为4个步骤:
- 初始标记(CMS initial mark): 仅仅标记一下GC Roots能直接关联到的对象,速度很快。用户停顿。
- 并发标记(CMS concurrent mark) :耗时较长,但无需停顿,可与用户线程并发运行。
- 重新标记(CMS remark):修正并发标记期间,因用户程序继续运作而导致标记变动的那部分对象。(黑灰白理论)用户停顿。
- 并发清除(CMS concurrent sweep) :清理删除掉标记阶段判断的已经死亡的对象,标记清除,无需移动存活对象。
需要停顿的初始标记和重新标记的停顿时间都很短,而耗时最长的并发标记和并发清除阶段都可以与用户线程一起工作。所以CMS的优点是并发收集,低停顿。
示意图:
3.Garbage First收集器
里程碑式的成果,开创了面向局部收集的设计思路和基于Region的内存布局形式。JDK 8 Update 40后全面成熟。
JDK 9发布后,,成为服务端模式下的默认垃圾收集器。
G1的设计目标是“停顿时间模型”(Pause Prediction Model),即支持指定在M毫秒的时间片内,GC上花费的时间大概率不超过N毫秒。
具体设计思路……
未完待续:…
Shenandoah收集器
ZGC收集器
各种性能监控工具
类加载过程
双亲委派模型
内存模型
线程安全/锁优化/偏向锁
2、3、4、7、12、13章。
Spring:
IOC原理
Bean生命周期
循环依赖
AOP原理
SpringBoot:
自动配置原理
分布式:
分布式锁
分布式事务
设计模式
海量数据算法
转载请注明来源