当前位置 : IT培训网 > IT培训 > 交流分享 > 当一名优秀的程序员需要知道JVM知识吗

当一名优秀的程序员需要知道JVM知识吗

时间:2019-10-16 14:50:08  来源:编程网  作者:IT培训网  已有:名学员访问该课程
标签(Tag):   程序员(504)
程序计数器:程序计数器是一块比较小的内存空间,主要用在基础的字节码指令、线程切换等功能上,java虚拟机的多线程是通过线程轮流切换来分配处理器时间的,也就是所谓的 时间片轮转调度法 。

你真的了解JVM吗?你真的适合当一名程序员吗,知道当程序员需要了解哪些知识点吗?

对于java程序员小白来说(没错,是我),jvm总是笼罩着一层神秘的面纱的,java是如何分配内存的,又是如何回收内存的呢?有人说内存管理是一道墙,墙里面的人想出去,墙外面的人想进去。而我们java程序员,就是硬着头皮进去的那群人...

学习的目的很简单

-----知道jvm是什么东西,它是干什么的,以及它是怎么干的

-----知其为何,何其为之

-----夸夸其谈

-----装逼

把书读薄,把知识丰满。

本文旨在记录虚拟机学习过程中的一些理解和知识点,如果有幸能帮助到你,或许可以给你提供帮助,乐意之至。

jvm分区

由java虚拟机规范规定,将java虚拟机管理的内存分为以下几个运行时的数据区域(这只是规范上面的分区,具体不同的虚拟机会有不同的实现)。

当一名优秀的程序员需要知道JVM知识吗_www.cnitedu.cn

其中方法区和堆(黄色块)是 所有线程共享 的区域,虚拟机栈,本地方法栈和程序计数器这三块是 线程私有。

的区域,各个线程之间互不影响,独立存储。

区域功能

程序计数器:程序计数器是一块比较小的内存空间,主要用在基础的字节码指令、线程切换等功能上,java虚拟机的多线程是通过线程轮流切换来分配处理器时间的,也就是所谓的 时间片轮转调度法 ,那么当线程切换的时候如何恢复到正确的执行位置,就需要每个线程自己“记住”,这就是程序计数器一个很重要的作用。

java虚拟机栈:也叫做java方法栈,也就是通俗意义上大家讲的堆栈堆栈 的栈了。每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表、操作数栈、动态链接等信息,方法调用的时候入栈,方法结束的时候出栈。

本地方法栈:本地方法栈与虚拟机栈的功能很相似,虚拟机栈执行的是java方法,本地方法栈执行的是Native方法。

java堆:上面讲过,堆是被所有线程共享的一块内存区域。堆用来存放对象的实例,几乎所有的对象实例和数组都在堆上分配,所以堆也是内存中 最大 的一块。

方法区:方法区也是所有线程共享的区域,它通常用来存储加载的类信息、常量、静态变量等数据。有很多人也把它叫做 永久代 ,事实上这是两个东西,方法区是一个具体的 实现 ,而方法区是java虚拟机规范当中定义的一个分区 概念 ,仅仅是因为HotSpot虚拟机采用了分代的思想来进行GC,并且用永久代来实现了规范中的方法区,所以很多人混淆了两者。但是这个称唿可以在一定程度上反映这个区域的GC是比较少的,或者说GC条件是比较苛刻的。

java堆

堆是内存池中最大的一块区域,也是jvm垃圾回收最主要的区域,那么我们就来梳理一下堆的知识。

对象的创建过程:前面讲到,堆是用来存放对象实例的,那么一个对象到底是如何创建又是如何放入堆中的呢?

a). java中通常使用一个new关键字来创建对象,当虚拟机接收到new指令时,需要检查这个类是否已经被加载、解析和初始化,如果没有的话,需要进行相应的类加载过程。类加载的过程是分为多个阶段的,其中 初始化阶段 ,虚拟机严格规定了有且只有5种情况,属于对一个类进行 主动引用 ,必须立即堆类进行“初始化”,除此之外的所有引用都是被动引动,不会触发初始化操作。

b).虚拟机为对象分配内存,如果堆里面的内存是规整的,已经使用的内存放一边,空闲的内存放另一边,这种分配方式称为“ 指针碰撞 ”;如果内存不是规整的,那么虚拟机就要维护一个列表,记录哪些内存块是可用的,分配的时候需要从列表里面找到一块足够大的内存空间划分给对象,称为“ 空闲列表 ”

当一名优秀的程序员需要知道JVM知识吗_www.cnitedu.cn

c).内存分配之后,将分配到的内存空间初始化为零值

d).此时在虚拟机看来,一个新对象已经创建完成了,java执行对应的init方法,按照程序进行初始化,得到一个真正可用的对象

对象的内存布局:对象可以分为3块区域,对象头,示例数据和对齐填充,对象头又由 Mark Word 和 类型指针组成

a).Mark Word 用于存储对象自身运行时数据(HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等)

b). 类型指针 即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

对象的定位:前面提到,java方法中对象的引用是存放在栈上的,而对象的实例是存放在堆上的。目前主流的访问方式有使用 句柄 和 直接指针 两种

a).句柄:java堆中划分出一块内存作为句柄池,reference中存储的是对象的句柄地址,句柄中含有两个地址,一个指向堆中这个对象的实例的地址,一个指向方法区中这个对象类型信息的地址

当一名优秀的程序员需要知道JVM知识吗_www.cnitedu.cn

b).直接指针:reference中直接存储对象的实例地址,然后通过取到对象头中的类型指针来确定方法区中这个对象类型信息的地址。

当一名优秀的程序员需要知道JVM知识吗_www.cnitedu.cn

c).比较:采用句柄的最大好处就是对象的实例地址在发生改变的时候,只需要更新句柄中指向对象实例的数据指针,不需要修改reference本身,但是可以看到相对比直接指针访问,句柄多了一次指针定位,这使得它的速度更慢。

java堆溢出:常见的就是OOM异常,导致堆溢出的原因通常有两个, 内存泄露 和 内存溢出。

a).内存泄漏:对象已经可以回收,但是却没有被回收。 那么问题来了,怎么判断一个对象是不是可以被回收或者说应该被回收呢?通常虚拟机是采用 可达性算法 来判定的,这个后面我会继续展开一下。

b).内存溢出:内存中的对象确实都是必须存活的,换而言之就是堆的容量已经成为了程序的瓶颈了,一方面我们可以尝试调大堆的内存,另一方面可以优化我们的代码,检查是否真正需要这么多对象,是否存在对象生命周期过长、持有状态时间过长的情况;总结起来就是“开源节流”。

堆内存的分配和回收:我们着重讲一下虚拟机在堆上的垃圾收集,在线程私有的内存分区中,内存会随着方法结束或者线程的结束而回收,所以这部分没有太多的操作空间,而堆和方法区是被所有线程所共享的一块区域,也是我们有必要深入了解的区域。

a).判断对象的存活:在内存泄漏的知识点中提到过 可达性算法 这一概念,在可达性算法中,有一个 GC Roots 的概念,这个算法的基本思想就是如果一个对象到 GC Roots没有任何引用链相连(也就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的,他们会被判定为是可回收的对象;

除此之外,还有另一个更简单的方法,给每个对象添加一个引用计数器,如果有一个地方引用它,计数器加1,引用失效的时候,计数器减1,计数器的值为0时这个对象就是可回收的,这是 引用计数算法 的基本思想,但是引用计数算法无法解决循环引用的问题,可以看到如下图的4、5、6三个对象,存在相互循环的引用,导致这三个对象的引用计数器不为0,但是这三个对象的的确确是属于需要回收的范畴的,这也是很多主流虚拟机放弃使用引用计数算法的原因。

当一名优秀的程序员需要知道JVM知识吗_www.cnitedu.cn

在java中,可以作为GC Roots的对象有4种:

1).虚拟机栈(栈帧中的本地变量表)中引用的对象 2).方法区中类静态属性引用的对象 3).方法区中常量引用的对象 4).本地方法栈中JNI(即一般说的Native方法)引用的对象

b).对象引用:java中定义了四种引用,引用强度从强到弱依次是 强引用 、 软引用 、 弱引用 和 虚引用 。

当一名优秀的程序员需要知道JVM知识吗_www.cnitedu.cn

c).回收对象:如果一个对象是GC Roots不可达的,也 不一定 会被回收,如果这个对象 覆盖 了finalize()方法并且这个finalize()方法是 第一次 被虚拟机调用,那么此时会执行对象的finalize()方法,如果在方法中,它重新与引用链中的任意一个对象建立了关联 ,那么它就可以逃过被回收的命运。

垃圾收集算法

上面介绍了堆上对象从创建到回收的过程,那么下面我们就来了解一下虚拟机到底是用什么样的方式来回收对象。

标记-清除算法:将回收过程分为“标记”和“清除”两个阶段,首先标记出需要回收的对象, 然后标记完成以后统一回收所有被标记的对象,这是最基础的收集算法,主要有两个比较大的缺陷, 一是效率低;二是产生大量不连续的内存碎片,这些空间无法被较大对象利用起来 复制算法:将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这块的内存用完了, 就将存活的对象复制到另外一块上,然后将这块的内存全部清理掉; 这样复制到另一块上的已使用内存是规整的,再分配时就可以使用前面提到过的“指针碰撞法” 但是我们可以发现这种做法每次只能使用一半的内存,付出的代价未免太大。 标记-整理算法:标记的过程与“标记-清除”算法一样,后续将所有存活的对象向一端移动,然后清理掉边界外的内存 分代收集算法:准确来说这不是一种算法,而是根据虚拟机中不同对象的存活周期不同,将内存进行分代, 一般是分为新生代(Young)和老年代(Old),新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低, 比较适合用复制算法,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。 在新生代中经历了多次(具体看虚拟机配置的阀值,默认15次)GC后仍然存活下来的对象会进入老年代中。 老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢, 老年代没有额外的空间进行分配担保,所以比较适合用“标记-清理”或者“标记-整理”算法进行回收

当一名优秀的程序员需要知道JVM知识吗_www.cnitedu.cn

上面说到,新生代是使用复制算法来回收内存的,复制算法最致命的缺陷就是会浪费一半的内存,由于新生代中对象的特点就是“朝生夕死”,所以并不需要将按照1:1的比例来划分。 HosSpot虚拟机将新生代分为Eden区和Survivor区,默认为8:1,同时survivor有两个,所以整体的比例应该是8:1:1,也就是说新生代中的可用空间是90%。但我们无法保证每次回收都只有不多于10%的对象存活下来,那么当survior区的空间不足时,会依赖老年代来进行分配担保,直白的讲就是把survivor区中放不下的对象放到老年代中。

那么为什么要设置两个survivor呢?如果只有一个可以嘛?(化身为面试官了嗷) 答案肯定是不可以嘛。。结合复制算法的思想,我们可以想到一块survior区必然是要保持为空的,以便我们将存活的对象复制过去。假设只有一块空白surviorA,当eden区满了的时候,触发第一次minorGC,我们将eden区中存活到达一定时间的对象复制到surviorA中,很快第二次触发minorGC,eden区有部分对象要进入survivorA区中,而surviorA区本身也有一部分对象要被回收,此时就会在survior中产生 内存碎片 ,根据复制算法的思想,我们希望得到的是规整的内存空间;如果是两个survior区的话,此时就可以将eden区和存放有对象的surviorA区中存活的对象都复制到空白surviorB区中,然后清空前面提到的eden区和surviorA区,此时原先存放有对象的surviorA变成了空白survior区,等待下一次minorGC存放对象。

我们经常会听到MinorGC和FullGC或者说MajorGC这种说法,那么它们具体代表的含义你真的清楚嘛?

MinorGC:也叫做新生代GC,顾名思义就是发生在新生代的垃圾收集动作。MinorGC非常频繁,同时回收的速度也很快 FullGC/MajorGC:老年代GC,指的是发生在老年代的GC,MajorGC经常会伴随着至少一次的MinorGC,同时MajorGC的速度一般要比MionrGC慢10倍以上。

总结

以上内容梗概基本来源于《深入理解java虚拟机》这本书的前三章,也是笔者重点阅读的章节,属于比较基础和理论的部分,其中结合了笔者自身的理解和粗浅认识,如果有偏颇之处,望读者不吝指出。(太长的篇幅容易产生阅读抵触~ 哈哈哈)后面有机会的话,会填坑一下虚拟机的类加载机制和java内存模型和线程部分。

学编程不容易,当一名程序员更难,如果你觉得自己有能力做好一名程序员,那就从学好编程开始吧,选择郑州IT培训机构,当一名程序员我们都可以!

顶一下
(0)
0%
踩一下
(0)
0%

IT培训0元试听 每期开班座位有限.0元试听抢座开始! IT培训0元试听

  • 姓名 : *
  • 电话 : *
  • QQ : *
  • 留言 :
  • 验证码 : 看不清?点击更换请输入正确的验证码

在线咨询在线咨询

温馨提示 : 请保持手机畅通,咨询老师为您
提供专属一对一报名服务。

------分隔线----------------------------
------分隔线----------------------------

推荐内容