JVM基础(比较全面)

JVM包含了哪几部分

JVM主要包括了4个部分:

  1. 类加载器(Class Loader)
  2. 运行时数据区(RunTime Data Area)
  3. 执行引擎(Execution Engine)
  4. 本地库接口(Native Interface)

类加载器 (Class Loader)

类加载器:主要功能就是将硬盘中的.class字节码文件加载到内存中,在这里也可以说成就是加载到运行时数据区(Runtime Data Area)。而这过程中要经历5个阶段:

  1. 加载:在硬盘的.class字节码文件与运行时数据区中间打开通道进行传输,将所有字节码文件在运行时数据区封装成一个Class对象。
  2. 验证:检查封装成的Class是否符合JVM虚拟机规范,是否会造成安全问题。
  3. 准备:为Class对象里面的静态变量开辟空间,赋予默认值。
  4. 解析:将符号引用替换为直接引用。
  5. 初始化:将静态变量赋予你给的值,而不是继续使用初始值。

其中2,3,4可以统称为“链接”。

运行时数据区 (Runtime Data Area)

运行时数据区:类加载器将字节码文件成功地在运行时数据区中封装成Class对象,更具体一点的地址是在运行时数据区的方法区中(Method Area)。运行时数据区共分为5个部分:

  1. 方法区(Method Area):线程间共享的内存区域,内存中存储加载的类型信息,常量以及静态变量。但是目前这个区域以及移到了堆中,这其实是一个逻辑上的概念。
  2. 堆(Heap):线程间共享的内存区域,同时也是最大的一块内存区域。所有的对象实例和数组都储存于此,而基本数据类型是直接存在JVM栈上的。
  3. JVM栈(JVM Stack):线程私有的区域,每个方法执行的时候,都会产生一个栈帧,这个栈帧记录了该方法的局部变量,方法出口,返回值等信息,随方法开始进栈,随方法结束出栈。该栈主要为字节码文件的方法服务,就是我们自己写的方法和Java实现的方法。
  4. 本地方法栈(Native Method Stack):与JVM栈基本相同,但是主要是服务于Native方法的,即那些不用Java语言实现的Java接口,比如说一个用C语言实现的Java接口就是在这个栈中运行的,这个在后面的执行引擎会讲到,这其实就突破了Java的语言限制。
  5. 程序计数器(Program Counter Register):线程私有的区域,其实就是记录线程下一条指令的位置,控制着循环,跳转,分支等各类基础功能,所以线程私有的话,就可以确保多线程情况下每一个线程都可以在线程来回切换的情况下,始终保持有序运行。

总结一下:方法区和堆服务于对象和类,JVM栈和本地方法栈服务于方法,程序计数器控制运行顺序。
在这里插入图片描述

执行引擎(解释器)

我们知道Java号称“一次编译,多地运行”,同样的.class字节码文件被加载到内存中,是怎么被各个平台所接受运行的呢。执行引擎就起到一个将字节码文件翻译成各个操作系统的指令集,然后操作系统就可以读懂啦。

本地库接口(Native Interface)

之前我们说过了本地方法栈(Native Method Stack),他的实现靠的就是本地库接口,他会记录本地方法栈中的本地方法名,然后需要用的时候就通知执行引擎调用具体的本地方法。可以理解成一个协调者的功能,本地方法栈里想实现一个Java方法功能,该库接口就通知执行引擎去获得具体的实现。


Java程序是如何运行的

大致的流程如下:

  1. Java源代码经过编译器编译成.class字节码文件
  2. .class字节码文件经过类加载器进入内存封装成Class对象
  3. Class对象经过Java虚拟机解释告诉操作系统如何执行
  4. 操作系统操作CPU进行执行

局部变量存储在哪里?
局部变量主要存储在JVM栈中,每个方法执行的时候,都会在产生一个“栈帧”压入JVM栈中,该栈帧里面直接包含了基本数据类型,以及引用数据类型的引用。每个JVM栈都是线程私有的,而且与线程的生命周期相同,所以线程消失时对应的栈也会消失。

说说Java代码编译过程
我们之前已经知道了JVM可以将.class文件解释成不同操作平台的指令集,但是我们是如何将java源代码文件编译成.class字节码文件的呢?首先我们都知道我们会使用javac来进行编译,而具体的过程就如下了:

  1. 词法分析:通过字节流找到源码中的一个个关键词和符号比如说if,else,for之类的,形成一个个的token。
  2. 语法分析:我们拿到了一个个的token(相当于词语),但是是不是能组成语法合理的句子就在这一步实现(是否符合Java的语法规范)比如说if后面是不是跟的是boolean或者for循环是不是计数的是int,我们会将关键词组合成一颗抽象语法树
  3. 语义分析:此时我们已经可以确保了“语法的正确”,但不代表语义的正确,而语义分析要做的就是打磨这颗语法树,补足没有的,简化复杂的,确保语义正确。
    比如说:“下午做操场”,语法上这句子是没错的,但是很显然省略了一些且语义不正确,如果改成“下午我们做运动”,就是最正确的了,对应到编译过程中,就是补足类的默认构造器呀,检查类初始值是否正确。同时我们的句子中存在一些“语法糖”,这对于字节码来说是不够直接的,所以我们要将这些语法糖还原成最本来的样子,解语法糖,确保语义平铺直叙,简单易懂。比如说switch关键字反编译之后就是这样的:
    来自https://juejin.cn/post/6844903917382270990
    最后就是通过字节码生成组件将这颗抽象语法树转换成符合规范的字节码文件即可。

介绍一下对象实例化的过程:
首先记住对象实例化在字节码文件中就是调用< init >方法的,你一个对象有几个构造器就有几个< init >方法。而< init >方法里就包括了非静态变量,非静态代码块和构造器(顺序就是对象实例化的执行顺序)。
非静态变量------->非静态代码块------->构造器。
但是我们知道静态变量和静态代码块早在类加载进内存的时候就存在方法区了,所以是更早实例的话,所以加入静态变量和代码块的就是:
静态变量------->静态代码块------->非静态变量------->非静态代码块------->构造器。
同时,我们也知道Java中,父类都是先于子类进行初始化的,所以加入父类的话就是:
父类静态变量------->父类静态代码块------->子类静态变量------->子类静态代码块------->父类非静态变量------->父类非静态代码块------->父类构造器------->子类非静态变量------->子类非静态代码块------->子类构造器。
只要记住:非静态变量------->非静态代码块------->构造器,其实就够了,静态优先和父类优先我们之前就知道了。
更清晰的图解如下:

在这里插入图片描述

JVM中的运行时数据区的堆(Heap)是如何进行GC的

首先我们可以把JVM中的堆分为两个部分:新生代和老年代
新生代又可以分为:Eden区,Survivor区(8:2)
Survivor区则又可以分为:Survive From区和Survive To区域(1:1)
老年代则比较简单,没有更复杂的分区。
新生代Minor GC采用的是标记-复制算法
老年代Major GC采用的是标记-整理算法
当一个新的对象实例化后,他就会出现在新生代的Eden区,然后Eden区满了之后,新生代(Eden区域和Survivor From区域)就会发生Minor GC,过程如下:

  1. 所有在Eden区域和Survivor From区域存活的对象年龄+1,不存活的直接回收了(存活与否根据可达性分析法来判断即可)
  2. 将符合老年代年龄标准的对象复制放入老年代,其余不符合标准但是仍然存活的对象复制放入Survive To区域。
  3. 清空Eden区域和Survivor From区域,调换Survive From和Survive To区域,即这次的Survive To区域成为下次的Survive From区域。
    而如果我们的对象足够幸运,活到了15岁(默认准入门槛是15),那么就会被放入老年代。
    老年代的对象比较稳定,Major GC触发的条件一般为从新生代放入对象时发现空间不够了才会触发Major GC。一旦触发就会执行标记-清除算法:
  4. 标记所有仍然存活的对象,然后回收死亡的对象。(非常简单)
    如果Major GC之后空间仍然不够,就会抛出Out of Memory异常。

双亲委派模型的过程:
类加载器收到一个类加载的请求,会执行以下步骤:

  1. 检查该类是否已经被加载,如果已经被加载,就直接返回已经加载好的文件
  2. 如果没有被加载过,直接抛给该加载器的父类,如果抵达了最顶层的父类发现没找到这个类,就会抛出ClassNotFound异常,然后返回下一层继续调用子类的findClass方法继续搜寻

垃圾回收机制

先来说说哪些垃圾是主要处理哪些部分的垃圾的,我们一般指的就是运行时数据区中的堆和方法区中的数据,因为这两个是动态的,我们只有在运行中才会不断地创建和销毁,堆中不断产生新的对象实例而方法区中则不断会有新的类的加载。

可达性分析法:
Java的垃圾回收机制主要使用的就是这种方法来回收垃圾,基本思路很简单,从GC Root出发,不断进行搜索,搜索走过的路程就为引用链,如果不存在引用链上就进行回收,注意!回收不意味着立即死亡而是分情况:

  1. 如果finalize方法被调用过了或者根本没有finalize方法,那不好意思,直接死吧。
  2. 如果不是上面这种情况则调用finalize方法。
    具有未被虚拟机调用过的Finalize方法会被加入一个队列,就叫F-queue,一个低优先度的线程会去执行这个队列里面的对象的Finalize方法,但是!方法是不会确保被执行完毕的。GC会适时地再次来这个队列中检查,不管方法执行完了与否,如果依然没有引用链可以抵达某个队列中的对象,那么该对象就死亡了。

说说GC Root大概都包括哪些
大致包含以下的几个:

  1. 虚拟机栈中的栈帧所存储的引用对象,比如进入方法创建的临时变量,或者是不同存活线程自己的引用对象。
  2. 静态引用的变量
  3. 常量引用的变量,比如String
  4. 本地方法栈引用的对象
  5. 虚拟机内部引用的对象,比如各种异常,NullPointerException

分代收集理论

当前的垃圾收集都一般遵循两个假说:

  1. 弱分代假说:大部分对象都是朝生夕死的
  2. 强分代假说:熬过了越多次垃圾回收的对象越难以被回收

这就有了我们现在的分为新生代(分Eden,SurviveFrom和SurviveTo三区)和老年代的内存结构,而他们对应的算法分别是标记-复制算法以及标记-整理算法。

标记-复制的优势就是因为假设新生代的很多对象都是存活时间很短的,标记-复制可存活对象到存活区,然后直接删除剩下非存活区里的所有对象,这样的好处就是,确保不存在内存空间碎片,而且删除的更有效率(因为我们假设只需要有少量的新生代对象需要复制)。

在这里插入图片描述

标记-整理则是建立在老年代的大部分对象都会存活很久,所以不会采取复制的操作(因为如果大部分都存活的话,就要复制大部分对象了),所以基本上前面与标记-清除算法相同,标记要回收的对象然后回收清除,最后将所有内存对象往内存空间的一端进行移动(确保没有空间碎片)

在这里插入图片描述

GC(垃圾收集)

什么时候会触发Full GC?
当老年代的剩余空间无法装下更多来自新生代的对象的时候,就会触发Full GC,如果Full GC触发之后,仍然装不下,那么就会抛出Out of Memory异常。而新生代触发GC的条件则是Eden区满了就会触发了。

触发Full GC会发生什么?
在Full GC期间,用户的应用程序全程暂停(Minor GC可没有这样的待遇噢)

为什么老年代不能使用标记-复制?
首先我们知道老年代使用的是标记-整理算法,其实准确点说就是标记-清除-整理。因为基于强分代假说,熬过多次GC的对象大概率不会被回收,那么就意味着如果我们使用复制的话,我们就会复制大部分老年代的对象,这个效率就很低了。而清除的话只需要标记几个要回收的对象即可,然后整体向内存空间一端进行移动。

为什么要设置两块Survivor区域?
主要是防止内存空间碎片化,因为这两块空间的职责是交替轮换的,而每次复制我们都可以确保复制进去的空间是连续的(其实这就有了一个“整理”的过程),但是如果只有一块的话,就有可能出现某个对象熬过初期的Minor GC然后在某次GC后被回收了,导致了内存空间碎片诞生。同时为什么不是三块四块,是因为内存空间就这么大,你要是开辟多块的话,每块就很容易满了。两块是兼顾了空间和减少空间碎片的选择。

说说什么是内存泄漏以及什么是内存溢出
内存泄漏(Memory Leak):给一个变量分配了一块内存空间,该变量已经不再被使用了,但是内存空间却没有被GC回收,这就出现了内存泄漏。
内存溢出(Out of Memory):程序申请要使用的空间大于系统所能提供的空间,这就是内存溢出了。


在这里插入图片描述
像是基本数据类型,方法的调用都是存在栈中的。
方法区存储的是静态类型变量,静态方法,常量(常量)
以及一般来说新New出来的对象都是放在新生代里的,但是也有可能出现New了一个非常大的对象,然后这个对象直接进入老年代,这个主要是因为你的这个大对象不断地执行复制,是非常消耗资源的,但是同时你放老年代其实也是有问题的,因为如果你这个大对象是临时的,你一直占着老年代就会容易触发Full GC,所以new大对象的时候,其实一般还是用作长久使用的比较好。
为什么新生代的Young GC耗时比较短呢?因为说白了,你复制完就可以清除,他stop the world,但是老年代则是必须等待清除完才可以

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值