JVM · Java虚拟机原理 · JVM上语言·框架· 生态系统

《深入理解JVM虚拟机》垃圾回收部分 读书笔记

2019-06-22  本文已影响4人  冬天只爱早晨

自动内存管理机制

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

运行时数据区域

程序计数器

“程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器”
“如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)“

Java虚拟机栈

“虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame[1])用于存储局部变量表、操作数栈、动态链接、方法出口等信息”
“每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程”
异常
“如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常”
“如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常”

本地方法中栈

“虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务”
“与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常”

java堆

“Java堆是被所有线程共享的一块内存区域”
“所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么“绝对”了。”

内存回收

“Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等”

内存分配

“线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)”

方法区

“方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来”

运行时常量池

“运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。”

直接内存

HotSpot虚拟机对象探究

对象的创建

如何分配内存

如何解决线程安全问题

注意:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值

对象的内存布局

对象头(Header)

1.png

虚拟机对象头 Mark Word

实例数据(Instance Data)

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响

对其填充(Padding)

对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用.由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

对象的访问定位

使用句柄

2.png

Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息

直接指针

Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址
[站外图片上传中...(image-840a38-1561171064861)]

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

对象已死?

引用计数法(并未采用)

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的
缺陷:遇到互相引用的对象时,无法通知GC收集器回收

可达性分析算法

通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的

[站外图片上传中...(image-6cfe1f-1561171064861)]

作为GC Roots的对象

再谈引用

生存还是死亡

回收方法区

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类

常量

就是没有任何String对象引用常量池中的"abc"常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个"abc"常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似

垃圾回收算法

标记清除法(Mark-Sweep)

“首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象”

不足

复制算法(Copying)

“它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉”
不足
“这种算法的代价是将内存缩小为了原来的一半,未免太高了一点”

标记整理算法(Mark-Compact)

“标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

分代收集算法

“根据对象存活周期的不同将内存划分为几块”(新生代和老年代)

HotSpot算法实现

枚举根节点

此操作必须要在整个执行系统"暂停"状态下执行,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果准确性就无法得到保证

STW(Stop The World)

“是导致GC进行时必须停顿所有Java执行线程(Sun将这件事情称为"Stop The World")的其中一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的”

引用对象

“虚拟机应当是有办法直接得知哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用”

安全点(SafePoint)

安全区域(Sage Region)

Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程重新被分配CPU时间。对于这种情况,就需要安全区域(Safe Region)来解决。
安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的Safepoint

垃圾收集器

2.png

Serial收集器

ParNew收集器

Parallel Scavenge收集器

SerialOld收集器

Parallel Old收集器

CMS收集器(Concurrent MarkSweep)

缺点

G1收集器(Garbage First)

特点

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合

它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)

步骤

内存分配与回收策略

分代回收名称

对象优先在Eden分配

“对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC”

大对象直接进入老年代

所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(笔者列出的例子中的byte[]数组就是典型的大对象)

调节参数
-XX:PretenureSizeThreshold
大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(复习一下:新生代采用复制算法收集内存)

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

对象在Eden出生并且经过一次MinorGC,如果能被移动到survivor中,则年龄为1,此后每经历过一次MinorGC年龄加一,默认为15则会进入老年代
调节参数
-XX:MaxTenuringThreshold=n
n为多少次MinorGC

动态对象年龄判断

如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄

空间分配担保

发生MinorGC前,虚拟机会检查老年代可分配的连续区域是否大于整个Eden中所有对象大小,如果满足则MinorGC是安全的,否则则看HandlePromotionFailure是否允许失败,如果允许“那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC”

“JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC”

虚拟机性能监控工具与故障处理工具

JDK命令行工具

jps:虚拟机进程管理工具

“可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)”
jps[options][hostid]
参数

jstat:虚拟机统计信息监视工具

监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程[1]虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有GUI图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具

jstat[option vmid[interval[s|ms][count]]]


9.png

jinfo:Java配置信息工具

实时地查看和调整虚拟机各项参数

jinfo[option]pid
jinfo-flag CMSInitiatingOccupancyFraction 1444

jmap:Java内存映像工具

用于生成堆转储快照(一般称为heapdump或dump文件)

jmap[option]vmid

10.png

jhat:虚拟机堆转储快照分析工具

与jmap搭配使用,来分析jmap生成的堆转储快照

jstack: java堆栈跟踪工具

生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)
jstack[option]vmid

HSDIS:JIT生成代码反汇编

JDK的可视化工具

JConsole:Java监视与管理控制台

基于JMX的可视化监视、管理工具

VisualVM:多合一故障处理工具

VisualVM(All-in-One Java Troubleshooting Tool)是到目前为止随JDK发布的功能最强大的运行监视和故障处理程序,并且可以预见在未来一段时间内都是官方主力发展的虚拟机故障处理工具。官方在VisualVM的软件说明中写上了"All-in-One"的描述字样,预示着它除了运行监视、故障处理外,还提供了很多其他方面的功能

调优案例与奇淫技巧

一分多合理利用资源

建立5个32位JDK的逻辑集群,每个进程按2GB内存计算(其中堆固定为1.5GB),占用了10GB内存。另外建立一个Apache服务作为前端均衡代理访问门户。考虑到用户对响应速度比较关心,并且文档服务的主要压力集中在磁盘和内存访问,CPU资源敏感度较低,因此改为CMS收集器进行垃圾回收。部署方式调整后,服务再没有出现长时间停顿,速度比硬件升级前有较大提升

上一篇下一篇

猜你喜欢

热点阅读