Java虚拟机|JVM知识点汇总及简述->运行时数据区(创建对象内部各种结构的配合、运行时数据区面试题)、执行引擎篇(总结自尚硅谷JVM课程)

12 篇文章 0 订阅

六、创建对象内部各种结构的配合

1.对象实例化的几种方式及创建对象步骤说明

请添加图片描述

1.1 判断对象对应的类是否加载、链接、初始化

虚拟器遇到new的指令,会先去元空间的运行时常量池检查指令的参数去定位类的符号引用,同时还会检查这个符号引用是否已经被类加载器加载过了,如果已经被加载过了直接用。如果没有被加载则要经过双亲委派机制查询类文件是否存在,如果存在则要经过加载、链接、初始化的三部曲

1.2 为对象分配内存

首先计算对象所占的堆空间大小,如果是引用类型则直接分配引用变量的空间即可,四个字节。选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩缩整理功能决定。

指针碰撞(Bump the Pointer)
如果内存空间以规整和有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫做指针碰撞(Bump the Pointer)

1.3 设置对象的对象头

将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
在这里插入图片描述

1.4 执行init方法进行初始化(init方法就是我们所说的构造器、显式赋值)

在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。因此一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。

2.对象的内存布局

请添加图片描述

2.1 举例代码及图示说明
public class Customer{
	int id = 1001;
	String name;
	Account account;

	{
		name="匿名用户";
	}

	public Customer(){
		account = new Account();
	}
}

public Account{}

请添加图片描述

3.对象访问定位

请添加图片描述

3.1 句柄访问图示
  • 优点:

reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改。

请添加图片描述

3.2 直接访问图示(HotSpot采用)

请添加图片描述

4.直接内存

直接内存是直接申请的本地内存

  • 缺点:
  1. 分配回收成本高
  2. 不受JVM内存的管理
4.1 用法
  • NIO(Non-Blocking IO)

NIO是IO的进化版,意为非阻塞IO,能提高IO效率通过存在堆中的DirectByteBuffer操作Native内存,即绕过虚拟机内存的分配,直接与本机的实际内存交互

  • IO
4.2 内存溢出和内存设置
  • 由于使用本地内存所有在一定条件下也是会报内存溢出的,而且这个过程关于Java相关的内存监控软件是监控不到的

OutOfMemoryError:Direct buffer memory

  • 直接内存大小可以通过MaxDirectMemorySize设置,如果不指定,默认与堆的最大值-xmx参数值一致

5.成员变量(非静态)赋值过程

  1. 默认初始化
  2. 显式初始化 或 代码块中初始化
  3. 构造器中初始化
  4. 有了对象之后,通过“对象.属性“或”对象.方法”的方式对成员变量进行赋值。

6.关于面试

  1. 对象在JVM中是怎么存储的?
  • 对象在Java栈中的局部变量表中储存指向堆中的实例对象的地址,而堆中存放实例对象(对象头和实例数据)和指向方法区的对象完整结构的地址(类型指针),方法区存储对象完整的数据类型结构,即这种方式为直接引用。
  • 而对象头中包含运行时元数据(哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳)和类型指针(指向类元数据InstanceClass,确定该对象的类型);
  • 实例数据中存放着真正的有效信息,包括程序代码中的定义的各种类型的字段(包括从父类继承的各种字段)
  • 而这些数据存放的规则是:相同宽度的字段总是分配到一起,父类定义的变量会出现在子类之前,但是如果将+XX:CompactFields(对象实例子数据存储时,相同宽度的字段总是被分配到一起存储 ,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。)参数值为true(默认就为true),那子类之中较窄的变量也允许插入父类变量的空隙之中,以节省出一点点空间。
  1. 对象头信息里面有哪些东西?

对象头中包含运行时元数据(哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳)和类型指针(指向类元数据InstanceClass,确定该对象的类型),当然如果是数组还要存放数组的长度

七、关于运行时数据区的面试题

前言:

这些都是博主根据自己的理解敲上去的,如有不足或错误的地方希望大家在评论区指正出来

  1. 说一下JVM内存模型,有哪些区?分别干什么的?
  • Class文件到类加载器子系统再到运行时数据区,运行时数据区中又包括堆、虚拟机栈、方法区、程序计数器、本地方法栈,通过运行时数据区到执行引擎,此时还要调用加载类需要的本地方法接口
  • 类加载子系统:将class文件简单的进行校准和准备各种需要的引用等都需要加载进来
  • 堆:存放类的实例数据和对象头和类型指针等,字符串常量池也存放在这
  • 虚拟机栈:每个线程对应一个栈,每个方法对应一个栈帧,代码执行的流程和指令确认都在栈中完成
  • 方法区:存放着完成类的类型结构,以及运行时常量池
  • 程序计数器:是一个举足亲重的区域,在计算机组成原理中计数器十分重要的,它决定着代码执行的顺序,如果出错将会完蛋
  • 本地方法栈:由于早期的很多java核心内部组件都是有C或C++写的,所以需要给这些结构的调用留有存放的地方。以及对于操作系统的各种交互接口
  • 执行引擎:将经过运行时数据区的完整结构翻译成机器指令并执行
  1. Java8的内存分代改进?

对于JDK8的内存分代,我们要先说JDK6,在6中的结构字符串常量池和静态变量都存放在永久代中,JDK7后将两者都存放在了堆中,JDK8后将永久代改为元空间并使用本地内存。

  1. JVM内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个survivor区?

第一问是第一个问题回到了的
栈和堆的区别:一个是在栈中的局部变量表中存放对象的地址引用,这个引用指向堆中的实例对象,实例对象包括对象头、类型指针、实例数据
堆的结构:堆中分为Eden区、S0\S1区、老年代,还有逻辑意义上的永久代即元空间
两个S区的原因:主要目的就是为了存放对象的地址连续性考虑的,防止内存碎片化

  1. Eden和Survior的比例分配?

系统默认是8:1:1,即Eden区占8份,S0和S1各站1份。但如果UseAdaptiveSizePolicy这个参数是开着的,同时你没有对XX:SurvivorRatio这个参数显式的进行赋值,这时的内存区域大小都是由JVM根据本机内存自适应分配的

  1. JVM内存分区,为什么要有新生代和老年代?

因为存在不同的对象,每个对象的存活时间也不同,需要区别对待,如果把所有对象都存放在一个堆的大分区中是十分消耗资源的,无论是GC还是引用都是非常繁琐费时间;而分代就解决了很多对象“朝生夕死”的特效,在新生代中的对象大部分都是存活时间非常少的对象,而老年代中的对象都是存活时间非常长的对象。这样在进行GC时,就能对症下药,而不是出发Full GC

  1. 什么时候对象会进入老年代?
  • 当对象在S0/S1区到达一定阈值(每个对象有一个存活标记,即每轮GC后都会加一,当达到16就是阈值)时就会进入老年代。
  • 或者这个对象非常大,只有老年代能存下
  • 当S0/S1区中的对象非常多时,并且已经GC完后,会向老年代转移
  1. JVM内存为什么要分成新生代,老年代,持久代?

问题的前半段已经在第5个问题回答过,这里简单说一下为什么要分持久代,因为持久代也就是永久代即元空间,是存放对象所属类的完整类型结构,如果不分代,全部存放在一起,导致GC算法和对象引用都十分混乱,分门别类的分代可以避免这样提高效率

  1. 新生代中为什么要分为Eden和Survivor?

首先对于不同存活的对象我们需要加一区别,分为Eden和Survivor区的目的就是存放不同的存活时间,这里from区(S0区)与to区(S1)区单次只能用其一个,两个区是相互传递的,比如说第一次GC后,将存活的对象放入了S0区,第二次GC时就将Eden区的存活对象和S0中的存活对象全部移动到S1区中。其最大的目的就是为了使对象的地址是连续的,防止内存碎片化。

  1. jvm的永久代中会发生垃圾回收吗?

会,但如果都要回收永久代中的数据了说明这个程序代码效率很烂了,如果发生垃圾回收的话会触发Full GC,这样用户线程的暂停时间会非常长,影响用户体验

执行引擎

在这里插入图片描述

一、执行引擎概述

如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者

1.执行引擎的工作过程

  1. 执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
  2. 每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
  3. 当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。

请添加图片描述

二、Java代码编译和执行的过程

请添加图片描述

1.图片解释

  • 橙色图解

Javac来编译执行的“前端编译”,此时还未与JVM交互

  • 绿色图解

就是执行引擎所做的工作,也体现了Java半解释型半编译中的解释特性,具体解释的是执行引擎中的解释器、JIT解释器

  • 解释器:解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
  • 蓝色图解

编译过程, JIT编译器:JIT (Just In Time Compiler)编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。

三、机器码、指令、汇编、高级语言理解与执行过程

1.机器码、指令、汇编、高级语言

  • 机器码:由二进制1和0组成的序列

  • 指令:由于机器码可读性太差,所以将二进制0和1进行特定的序列排列成可读性较强的二进制序列

  • 指令集:由于不同的的CPU的实现方式不同衍生出来不同的指令集(主流为ARM架构和X86结构)

  • 汇编语言:由于指令可读性还是太差就此出现了汇编语言,进一步解决可读性的问题,在汇编语言中,用助记(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数的地址。

    但此时计算机已经不懂汇编语言了,所以汇编语言在编译时还要进一步编译成指令

  • 高级语言:为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言。高级语言比机器语言、汇编语言更接近人的语言当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码。完成这个过程的程序就叫做解释程序或编译程序。

2.执行过程

请添加图片描述

四、JIT编译器

1.对于JIT编译器的理解

主要进行后端编译工作,也就是“后起之秀”,前期响应速度比不上解释器,但经过热点代码查询完后对于JIT编译器的速度非常快的

2.何时JIT编译器才启动

是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。

3.热点代码及探测方式

3.1 热点代码

一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR(on Stack
Replacement)编译。

3.2 热点代码的阈值

JVM通过“热点探测功能(两种不同的计数器)”来寻找热点代码。

  1. 方法调用计数器(Invocation Counter):方法调用计数器用于统计方法的调用次数
  • 这个计数器就用于统计方法被调用的次数,它的默认阈值在client模式下是1500 次,在 Server模式下是10000 次。超过这个阈值,就会触发JIT编译。这个阈值是个相对概念,即一段时间内的热点代码,如果一段时间(半衰周期)内没有超过,则会触发“热度衰减”
  • 当一个方法被调用时,会先检查该方法是否存在被JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值。如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。

在这里插入图片描述

  1. 回边计数器(BackEdge counter) :回边计数器则用于统计循环体执行的循环次数
3.3 两种查找模式的切换

默认是两种混合
在这里插入图片描述

4.JIT的分类(C1、C2)

  1. -client:指定Java虚拟机运行在client模式下,并使用C1编译器;
    C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度
  2. -server:指定Java虚拟机运行在Server模式下,并使用C2编译器。
    C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高

注意:系统是64位系统默认就是server也就是C2编译器

五、面试

  1. 为什么说Java是半编译半解释型语言?

JDK1.0时代,将Java语言定位为“解释执行”还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值