JVM总结

本文是个人学习总结,内容来自网志明老师的《深入理解Java虚拟机》和个人整理。
本文所说的JVM均指HotSpot虚拟机;

什么是JVM?JVM的功能?

JVM的介绍:

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

JVM的功能:

1、JVM可以将.class文件转换为针对不同平台(Windows,Linux)的机器码,从而保证一套Java代码可以在不同的平台上运行。保证了Java的跨平台性。
2、自动内存管理机制,JVM帮助Java程序员去管理内存空间,不容易出现泄漏和内存溢出的问题(GC垃圾回收)。

也正是因为Java程序员把控制内存的权力交给了Java虚拟机,一旦出现内存泄漏和溢出方面的问 题,如果不了解虚拟机是怎样使用内存的,那排查错误、修正问题将会成为一项异常艰难的工作。

JVM运行时内存区域

在这里插入图片描述

这是Java1.8之前的示意图,在Java1.8之后,将方法区移除,并在本地内存中开辟一块空间,称为元空间(Meta-space)用来存储原本存储在方法区中的数据;
这样做的原因何在呢?笔者会在下文解释;

在五个区域中,方法区和堆是所有线程共享的资源,其他三个则是线程私有的;可以从上图的颜色进行区分;

Java对象的创建流程:

类加载检查 -> 分配内存-> 初始化零值 -> 设置对象头 -> 执行init方法

类加载检查:

当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到 一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

分配内存:

对象所需要的内存大小在类加载完成后便完全确定;
假设Java堆中内存是绝对规整的,则在使用和空闲的内存之间有一个指针作为分界器,分配时把指针移动所需内存大小的距离,这种方式称为指针碰撞
当内存不规整时,虚拟机需要维护一个表,记录那些块是空闲的,那些块是被占用的,找到一块足够大的内存给对象使用,称为空闲列表;当不规整的时候,CG可以使用空间压缩整理来是内存规整化;
在堆中会为每个线程创建一个私有的空间分配缓冲区(Thread Local Allocation Buffer,TLAB),当对象需要实例时直接在TLAB中创建,可以避免多线程分配空间时出现的问题,在TLAB分配完后在同步锁定分配新的缓存区;
虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来 设定。

初始化零值:

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

设置对象头:

Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到 类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才 计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟 机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

执行Init方法:

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视 角看来,对象创建才刚刚开始——构造函数,即Class文件中的()方法还没有执行,所有的字段都 为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说(由字节 码流中new指令后面是否跟随invokespecial指令所决定,Java编译器会在遇到new关键字的地方同时生成 这两条字节码指令,但如果直接通过其他方式产生的则不一定如此),new指令之后会接着执行 ()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。

方法区(Method Area)

(1)用于存储被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等,是线程共享的区域;
(2)运行时常量池是方法区的一部分,用于存放各种字面量和符号引用;

堆(Heap)

堆是一棵由数组构成的二叉树,堆总是满足两个条件:
(1)堆中的某个结点总是不小于(大根堆)或不大于(小根堆)其父结点的值;
(2)堆总是一棵完全二叉树;

(1)Java堆是JVM所管理的内存中最大的一块,被所有的线程共享;
(2)堆的唯一目的是存放对象实例,几乎所有的对象实例在这里分配内存;
(3)堆是GC进行垃圾回收的区域;
(4)堆虽然是线程共享的,但是也会划分出每个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB),用于提升对象分配的效率;
(5)堆的大小是可扩展的,通过参数-Xmx和-Xms设定,如果在Java堆中没有内存完成实例分配,并且堆也无法再 扩展时,Java虚拟机将会抛出OutOfMemoryError异常。

虚拟机栈(VM Stack)

栈是一种先进后出的数据结构,类似往一个水杯里放石头,后放的石头出去前面放的石头才能出去;
(1)VMS是Java方法运行时的线程内存模型,是线程私有的;
(2)每个方法被执行时,都会在VMS内部同步创建一个栈帧,栈帧中包括:局部变量表、操作数栈、动态连接、方法出口等信息;
(3)我们平时说的用于存储引用对象的栈其实是栈帧中的局部变量表,
(4)HotSpot虚拟机不可动态扩展,当线程申请栈空间失败时会出现OOM异常;
局部变量表中存储了:
1、编译期可知的各种Java虚拟机基本数据类型(boolean、byte、char、short、int、 float、long、double);
2、对象引用(reference类型,它并不等同于对象本身,可能是一个指向对象起始 地址的引用指针,也可能是指向一个代表对象的句柄或者其他与此对象相关的位置);
3、returnAddress 类型(指向了一条字节码指令的地址);

本地方法栈(Native Method Stack)

NMS与VMS的功能类似,不过VMS服务的是Java方法(字节码),而NMS为本地方法(Native)服务,NMS是线程私有的;

程序计数器(PC)

(1)PC是一块较小的内存空间,用于记录当前虚拟机运行到的字节码文件的行号,在多线程情况下,CPU会调度不同线程的指令执行。在线程切换时依赖PC来确定执行的位置,因此PC是线程私有的;
(2)PC是程序执行顺序的控制器,分支,循环,跳转,异常处理,线程恢复等都依赖PC控制;
(3)如果线程执行的是一个Java方法,则PC记录的是当前执行的虚拟机字节码指令的地址;
(4)如果线程执行的是一个Native方法,则PC记录为空(Undefined);

对象的内存布局:

在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例 数据(Instance Data)和对齐填充(Padding)。
对象头具体包括两部分:
一是用于存储对象自身的运行时数据,如哈 希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
二是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针 来确定该对象是哪个类的实例。

实例数据
实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字 段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。

对齐填充:
这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作 用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是 任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者 2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

对象的定位:

Java程序通过栈上的引用对象去操作堆上的具体对象,但是这个对堆内对象的定位是如何完成的呢?
事实上对象的定位也是JVM实现的,主要有句柄和直接指针两种方式:

句柄
Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就 是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。
在这里插入图片描述
直接指针
Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关 信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问 的开销
在这里插入图片描述
HotSpot虚拟机主要采用直接指针的方式进行访问;

GC内存回收:

在JVM内存模型中,PC,VMS,NMS是随着进程的创建和销毁同步进行的,栈中的栈帧也是随着方法的进入和退出进行有条不紊的进栈和出栈操作,当方法结束或进程结束时,内存自然也就同步回收了,所以这三个区域不需要进行内存的回收。

而Java堆和方法区的内容是动态创建的,每个方法需要的内存大小不一样,这两个区域才是内存回收需要关注的区域;

判断对象的生死:

当一个对象不在被任何途径使用时,我们说对象已经死亡,死亡的对象空间需要被回收。
如何判断一个对象是否死亡?

引用计数法:

在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可 能再被使用的。
但是单纯的引用计数不能解决对象之间互相循环引用的问题,比如:A引用B,B引用A,两个对象实际上不能再被访问,但由于他们相互引用,导致计数器不为0,无法回收。
主流的Java虚拟机都没有采用此方法。

可达性分析算法

是主流程序语言才有的算法,这个算法的基本思路就是通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过 程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

在这里插入图片描述
可以作为GC Roots的对象包括:
·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
·在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。 ·所有被同步锁(synchronized关键字)持有的对象。 ·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

不可达的对象并非“非死不可”

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。

如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的 队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize() 方法。

如果对 象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己 (this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集 合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。

废弃常量:

假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池了。

废弃类:

需要同时满足下面三个条件:
·该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
·加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP的重加载等,否则通常是很难达成的。
·该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

方法区回收:

方法区垃圾收集 的“性价比”通常也是比较低的:在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾收集通常 可以回收70%至99%的内存空间,相比之下,方法区回收囿于苛刻的判定条件,其区域垃圾收集的回 收成果往往远低于此。 方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。

内存分配和回收:

分代收集理论:

其建立在两个假说之上:
弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消 亡。

这两个分代假说共同奠定了多款常用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分 出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区 域之中存储。显而易见,如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那 么把它们集中放在一起,每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对 象,就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块, 虚拟机便可以使用较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有 效利用。

把分代收集理论具体放到现在的商用Java虚拟机里,设计者一般至少会把Java堆划分为新生代 (Young Generation)和老年代(Old Generation)两个区域。顾名思义,在新生代中,每次垃圾收集 时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。

对象的跨代引用问题:

新生代中的对象是完全有可 能被老年代所引用的,为了找出该区域中的存活对象,不得不在固定的GC Roots之外,再额外遍历整 个老年代中所有对象来确保可达性分析结果的正确性,反过来也是一样。

跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极 少数。

这其实是可根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对象,是应该倾 向于同时生存或者同时消亡的。举个例子,如果某个新生代对象存在跨代引用,由于老年代对象难以 消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时 跨代引用也随即被消除了。
依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录 每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称 为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会 存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数 据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。

部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,
其中又分为:
■新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
■老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单 独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
■混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收 集器会有这种行为。
·整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

在这里插入图片描述
对象的内存分配,从概念上讲,应该都是在堆上分配(而实际上也有可能经过即时编译后被拆散 为标量类型并间接地在栈上分配[1])。在经典分代的设计下,新生对象通常会分配在新生代中,少数 情况下(例如对象大小超过一定阈值)也可能会直接分配在老年代。对象分配的规则并不是固定的, 《Java虚拟机规范》并未规定新对象的创建和存储细节,这取决于虚拟机当前使用的是哪一种垃圾收 集器,以及虚拟机中与内存相关的参数的设定。

优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起 一次Minor GC。如果经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象 年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程 度(默认为15),就会被晋升到老年代中。
为了能更好地适应不同程序的内存状况,HotSpot虚拟机并不是永远要求对象的年龄必须达到- XX:MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于 Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX: MaxTenuringThreshold中要求的年龄,这称为动态对象的年龄判定

大对象直接进入老年代

HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区 之间来回复制,产生大量的内存复制操作。

JVM的内存分配流程:

(1)新生对象进入Eden;
(2)经过第一次MinorGC后存活的对象进入Survivor01,年龄+1;
(3)新生对象进入Eden;
(4)经过一次MinorGC(对Eden和Survivor01进行CG),幸存的对象都会复制到Survivor02,进行排序,且年龄加1;
在这里插入图片描述
(5)如果出现大对象Eden无法存储时直接在老年代分配;
(6)年龄到达一定程度时(默认15岁)的对象将进入老年代;
(7)当老年代满时会进行MajorGC;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值