深入理解Java虚拟机之内存管理(读书笔记)

 2019-12-22 10:39  阅读(643)
文章分类:JVM

本文为本人精心整理的周志明老师的《深入理解Java虚拟机》一书中的Java内存管理方面的内容。

同时发布到了我的知乎文章https://zhuanlan.zhihu.com/p/26986448中,转载请注明作者和原文地址,谢谢!

Java内存区域与内存溢出异常

2019120001176\_1.png

Java虚拟机运行时数据区

1.运行时数据区域

1.1程序计数器

作用:一块小内存,可看作当前线程所执行的字节码的行号指示器,改变这个计数器的值来选取下一条需要执行的字节码指令。

特点

线程隔离,每条线程都有一个独立的程序计数器。

线程在执行Java方法,计数器记录正在执行的虚拟机字节码指令的地址

线程在执行Native方法,计数器值为空

此内存区域是唯一一个在Java虚拟机规范中没有规定OutOfMemoryError的区域

1.2Java虚拟机栈

作用:描述的是Java方法执行的内存模型,每个方法在执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成,对应一个栈帧在虚拟机栈中入栈到出栈的过程。

特点:

线程隔离

生命周期与线程相同

局部变量表存放了编译期可知的基本数据类型、对象引用类型和returnAddress类型,所需内存空间在编译期完成分配,方法运行期间不会改变。

StackOverflowError:线程请求的栈深度大于虚拟机允许的深度

OutOfMemoryError:虚拟机栈动态扩展时无法申请到足够的内存

1.3本地方法栈

**作用:**为虚拟机使用的Native方法服务,

特点:

有的虚拟机把虚拟机栈和本地方法栈合二为一。也有虚拟机栈的异常。

1.4Java堆

**作用:**唯一作用就是存放对象实例,几乎所有的对象实例都在这里分配内存。

特点:

线程共享

虚拟机启动时创建

垃圾收集器管理的主要区域

OutOfMemoryError:堆中没有内存完成实力分配,堆也无法扩展

1.5方法区

**作用:**用于存储已被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码

特点:

线程共享

被称为“永久代”

OutOfMemoryError:当方法区无法满足内存分配需求时,抛出异常

1.6运行时常量池

作用:是方法区的一部分,用于存放编译期生成的各种字面量和符号引用

特点:

除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。

具备动态性,运行期间也可能将新的常量放入池中(String类的intren()方法)

OutOfMemoryError:受方法区内存的限制,常量池无法再申请到内存时

1.7直接内存

作用:NIO中可以使用Native函数库直接分配堆外内存,然后通过一个存储在堆中的DirectByteBuffer对象作为内存的引用进行操作。

特点:

OutOfMemoryError:设置虚拟机参数时可能忽略直接内存,使得各个内存区域总和大于物理内存限制,动态扩展时出现这个异常

2对象探秘

2.1对象的创建

1.虚拟机遇到new指令,首先检查这个指令的参数能否在常量池中定位到一个类的符号引用,检查这个符号引用代表的类是否已被加载、解析和初始化过。若没有,先执行相应的类加载过程。

2.类加载检查通过后,虚拟机为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定。(分配内存方法:指针碰撞、空闲列表)(对象创建的并发安全性:法一对分配内存空间的动作进行同步处理,法二把内存分配的动作按照线程划分在不同的空间之中进行,称为本地线程分配缓冲TLAB)

3.虚拟机将分配到的内存空间初始化为零值。

4.虚拟机对对象进行必要的设置,信息存放在对象的对象头之中。

5.执行new指令后接着执行init方法,把对象按照程序员的意愿进行初始化。

2.2对象的内存布局

分为三块:对象头、实例数据、对齐填充。

对象头包括两部分:

一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向编程ID、偏向时间戳。

另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

**实例数据:**是对象真正存储的有效信息,程序代码定义的各种类型的字段内容

**对齐填充:**不是必然存在的,起着占位符的作用,要求对象起始地址必须是8字节的整数倍

2.3对象的访问定位

通过栈上的reference数据来操作堆上的具体对象。主流对象访问方式有句柄和直接指针。

句柄:堆中划出一块内存作为句柄池,reference中存放对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。

直接指针:堆对象要定义访问类型数据的指针

3 实战OutOfMemory异常

3.1Java堆溢出

-Xmx

-Xms

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

解决:通过内存映像分析工具对堆转储快照进行分析,分清出现内存泄漏(memory Leak)还是内存溢出(Memory Overflow)。

内存泄漏——找出泄漏对象的类型信息及GC Roots引用链的信息,定位出泄漏代码的位置

内存溢出——检查虚拟机堆参数,看代码中是否某些对象生命周期过长,持有状态时间过长,减少程序运行期的内存消耗

3.2虚拟机栈和本地方法栈溢出

栈容量设置-Xss参数设定

3.3方法区和运行时常量池溢出

-XX:PermSize

-XX:MaxPermSize

运行时常量池溢出:PermGen space(说明运行时常量池属于方法区)

方法区的异常测试思路是运行时产生大量的类填满方法区,直到溢出

3.4本机直接内存溢出

DirectMemory容量 -XX:MaxDirectMemorySize指定

如果没指定,则和Java堆最大值(-Xmx指定)一样。

垃圾收集器与内存分配策略

1.概述

为什么栈不用GC,堆需要GC?

程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭。因此这几个区域的内存分配和回收具备确定性,方法结束或者线程结束时,内存自然就跟着回收了。

而Java堆只有程序处于运行期间才知道会创建哪些对象,这部分内存的分配和回收都是动态的。

2.对象已死吗

Python判断对象是否存活?

引用计数法(多一个引用+1,引用失效-1),但Java不能用引用计数法,因为不能解决对象之间相互循环引用的问题。

Java中什么方法判断对象是否存活?

可达性分析算法:通过一系列GC Roots对象作为起始点,向下搜索,搜索走过的路径称为引用链(Reference Chain),但一个对象到GC Roots不可达时,此对象不可用了。

可作为GC Roots的对象:

虚拟机栈(栈帧中的本地变量表)中引用的对象

方法区中类静态属性引用的对象

方法区中常量引用的对象

本地方法栈中JNI(本地方法)引用的对象

Java的引用有哪些方式?

强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)

强引用:Object o = new Object();只要强引用还在,GC不回收掉被引用对象。

软引用:描述还有用但不是必需的对象,内存溢出之前,会把软引用对象列进回收范围之中进行第二次回收,如果第二次还没足够内存,才会抛出内存溢出异常。SoftReference类实现软引用。

弱引用:被弱引用关联的对象只能生存到下一次GC之前,GC工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。WeakReference实现。

虚引用:无法通过虚引用获得一个对象实例。设置虚引用的目的是能在这个对象被GC回收时得到一个系统通知。PhantomReference实现。

可达性分析算法不可达的对象一定会死吗?

不是的。如果对象与GC Roots不可达,对象则会被第一次标记,筛选要不要执行finalize()方法(若对象没覆盖此方法或此方法已被虚拟机调用过,则不被回收)。如果要执行此方法,放对象到F-Queue队列中,之后由虚拟机Finalizer线程触发它。稍后GC对F-Queue中对象第二次标记(若对象想拯救自己,只要与引用链上对象建立关联即可)(任何对象的finalize()方法只会被系统自动调用一次)

方法区有GC吗?

有的,方法区回收废弃常量和无用的类。

怎样算废弃常量和无用的类?

废弃常量:没有任何对象引用常量池中的该常量。

无用类:

Java堆中不存在该类的实例;

加载该类的ClassLoader对象已被回收;

该类对应的java.lang.Class对象没被引用,也没有反射访问该类的方法;

大量使用反射、动态代理、CGLib、动态生成JSP、OSGI这种频繁自定义ClassLoader的场景需要虚拟机有卸载功能,保证永久代不会溢出。

3.垃圾收集算法

标记–清除算法:(效率不高,有内存碎片)

标记需要回收的对象,标记完成后统一回收被标记的对象。

复制算法:(新生代采用)

内存分为两块,每次只使用一块,一块满了,将存活对象复制到另一块上面,再把使用过的那块清理。(不用考虑内存碎片,但代价为内存缩小一半)

一块Eden80%,两块Survivor每个10%。每一次使用Eden和一块Survivor,回收时Eden和Survivor存活的对象一起复制到另一块没用到的Survivor上。

当Survivor空间不够用时(存活对象多余10%),则需要老年代内存进行分配担保(Handle Promotion)。

标记–整理算法:(老年代采用)

先标记,然后让存活对象向一端移动,然后清理掉边界以外的内存

4.HotSpot的GC算法实现策略

枚举根节点:

GC Roots主要在全局引用(常量、类静态属性)和执行上下文(栈帧中本地变量表)中,因为现在方法区都比较大,逐个检查引用很费时间,枚举根节点必须停顿。

因此HotSpot中使用OopMap这种数据结构得知哪些地方存放着对象引用,类加载完成时,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来。

安全点:(Safe Point)

为每条指令都生成OopMap成本太高,只在安全点记录了这些信息(程序执行时只有到了安全点才能暂停,执行GC)。

而且GC发生时要让所有线程跑到最近的安全点上再停顿下来。采用抢先式中断(很少使用)和主动式中断(都在采用这个)。

安全区域:(Safe Region)

线程sleep了或者Blocked了,无法响应JVM的中断请求,此时需要安全区域。安全区域是指一段代码片段中引用关系不会发生变化,在此区域任何地方GC都是安全的,相当于扩大了的安全点。

5.垃圾收集器

内存回收如何进行是由JVM采用的GC收集器决定的。

2019120001176\_2.png

5.1Serial收集器

新生代——复制算法

这个收集器是一个单线程的收集器,它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

**它是虚拟机运行在Client模式下的默认新生代收集器。**简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

5.2ParNew收集器

新生代——复制算法

**ParNew收集器其实就是Serial收集器的多线程版本,它是许多运行在Server模式下的虚拟机中首选的新生代收集器,**除了Serial收集器外,目前只有它能与CMS收集器配合工作。

可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

ParNew收集器也是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

5.3Parallel Scavenge收集器

新生代——复制算法

是并行的多线程收集器。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:

控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数

直接设置吞吐量大小的-XX:GCTimeRatio参数

5.4Serial Old收集器

老年代——标记整理算法

Serial Old是Serial收集器的老年代版本,单线程收集器。

5.5Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。

5.6CMS收集器

CMS(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。该收集器是基于“标记—清除”算法实现的,整个过程分为4个步骤,包括:初始标记(CMS initial mark),并发标记(CMS concurrent mark),重新标记(CMS remark),并发清除(CMS concurrent sweep)。

5.7G1收集器

最先进的,面向服务器端应用。

优点:并行并发、分代收集、空间整合、可预测的停顿

G1将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

6内存分配策略与回收策略

对象优先分配在Eden上

Eden没足够空间时,虚拟机发起一次Minor GC(新生代GC)。

大对象直接进入老年代

需要大量连续内存空间的java对象,例如很长的字符串或数组。

长期存活的对象将进入老年代

虚拟机给每个对象定义了一个Age计数器。

Eden出生,并且第一次MinorGC后存活,复制到另一个Survivor,Age设为1。在Survivor中每熬过一次MinorGC,Age+1。若Age>=15,晋升为老年代。

动态对象年龄判断

Survivor空间中相同年龄所有对象的大小的总和>Survivor空间的一半,Age大于或等于这个年龄的对象就可以直接进入老年代(无须等到15岁)。

空间分配担保

在MinorGC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,若成立,则MinorGC确保安全。

如果小于,看是否允许担保失败(允许,则尝试性的MinorGC一次。若不允许,则FullGC)。

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 深入理解Java虚拟机之内存管理(读书笔记)

相关推荐