“用力活着用力爱哪怕肝脑涂地,不求任何人满意只要对得起自己”
文章目录
JVM主要组成部分有哪些?
- 类加载器:将
Java
代码转换成字节码
- 运行时数据区:将
字节码
加载到内存中 - 执行引擎:将
字节码
转化为底层的系统指令
- 本地库接口:将底层
系统指令
交给CPU
执行
谈谈你对运行时数据区的理解?
Java虚拟机在执行Java程序的时候会把所管理的内存划分为若干个不同的数据区域。
- 程序计数器:是线程
私有
的内存区域,每条线程都有一个独立的计数器,字节码解释器在工作的时候需要改变计数器的值来选取下一条需要执行的字节码指令,程序的循环、分支、异常处理
都需要依赖计数器来完成 - Java虚拟机栈:是线程
私有
的内存区域,其描述的是Java方法执行的内存模型,每个方法在执行的同时会创建一个栈帧
,栈帧存储了局部变量表等信息,每一个方法被调用直至完成的过程就对应着一个栈帧从虚拟机栈中入栈到出栈
的过程 - 本地方法栈:与虚拟机栈所发挥的作用非常相似,虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机执行
本地方法
服务 - Java堆:是被所有线程所
共享
的一块内存区域,在虚拟机启动时创建,作用是存储对象实例,几乎所有的对象实例
都在这里分配内存 - 方法区:是线程
共享
的内存区域,主要用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码
等数据 - 运行时常量池:是方法区的一部分,存放了编译器生成的各种
字面量
与符号引用
堆和栈的区别是什么?
- 堆是线程
共享
的,存放的是对象实例
- 栈是线程
私有
的,存放的是数据类型、对象的引用
Java对象的大小是怎么计算的?
一个空的Object对象所占用的空间是8byte+4byte
,8byte
是本身对象的大小,4byte
是Java栈中保存对象引用所需要的空间;如果有其他属性的话需要加上基本数据类型的大小,但是总是8的整数倍
对象的访问定位的两种方式?
Java程序通过栈上的reference
数据来操作堆上的具体对象
句柄访问
:Java在堆中划分出一块区域作为句柄池
,reference
存放的是对象的句柄地址
,句柄中存放的是对象实例数据
与类型数据
的地址;优点是对象被移动时只会改变句柄中的实例数据指针,reference
本身不需要被修改
直接指针访问
:reference
存放的直接就是对象的地址;优点是速度快
,节省了一次指针定位的时间开销
谈谈对类文件结构的理解,由哪几部分组成?
魔数
:class文件的头四个字节,作用是确定这个文件能否被虚拟机所接受版本号
:紧接着魔数的四个字节存储的是class文件的版本号,前两个字节是次版本号,后两个字节是主版本号常量池
:主要存放的是字面量
和符号引用
,字面量属于Java语言层面
的概念,包括文本字符串、被声明为final的常量值等;符号引用属于编译原理层面
的概念,比如类和接口的全限定名、字段的名称和描述符等访问标志
:用于识别一些类或者接口的层次的访问信息,比如这个class是类还是接口;是否是public等等类索引、父类索引和接口索引集合
:类索引用于确定这个类
的全限定名,父类索引用于确定这个类的父类
的全限定名,接口索引用来描述这个类实现了哪些接口
字段表集合
:用于描述类或者接口中声明的变量
方法表集合
和属性表集合
谈谈对类加载机制的了解?
Java虚拟机
把描述类的数据从class文件
加载到内存
,并对数据进行校验、转换解析和初始化
,最终形成可以被虚拟机直接使用的Java类型,这个过程叫做虚拟机的类加载机制
主要分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载
,其中验证、准备、解析这三个阶段统称为连接
类加载各个阶段的作用是什么?
加载
:通过一个类的全限定名
来获取定义此类的二进制字节流
;将这个字节流
所代表的静态存储结构转化为方法区的运行时数据结构
;在内存中生成一个代表这个类的java.lang.class对象
作为方法区这个类的各种数据的访问入口
验证
:目的是确保class文件
的字节流中包含的信息符合虚拟机的要求,保证这些信息被当作代码运行后不会危害虚拟机的自身安全
准备
:正式为类中的变量分配内存
并设置类变量初始值
解析
:将常量池内的符号引用
替换为直接引用
,(直接引用是可以直接指向目标的指针
、相对偏移量
或者能间接定位到目标的句柄
)
初始化
:真正的开始执行类中编写的Java程序代码
,将主导权移交给应用程序
类和类加载器的关系?
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中
的唯一性
,每一个类加载器,都有一个独立的类名称空间
,如果要比较两个类是否相等
,只有这两个类是由同一个类加载器加载
的前提下才更有意义,否则即使这两个类来源于同一个class文件,但是是由不同的类加载器加载的,那么这两个类就必定不相等
谈谈对双亲委派模型的理解?
双亲委派模型
要求除了顶层的启动类加载器之外,其余的类加载器都应有自己的父类加载器
工作过程
:如果一个类收到了类加载的请求
,它首先不会自己去完成这个请求,而是交给父类加载器
去完成,因此所有的加载请求都最终会传到最顶层的启动类加载器
中,只有当父类加载器
反馈自己无法完成这个请求时,子加载器
才会尝试自己去完成加载
好处
:Java类
随着类加载器一起具备了带有优先级
的层次关系,无论哪一个类加载器要加载这个类,都会交给顶层的启动类加载器
去完成,因此object类在各类加载器环境中都能保证是同一个类
;如果没有使用双亲委派模型
,都由各个类加载器去自行加载,如果自己也写了一个object类
,那么程序中就会出现多个object类
,Java类型体系中最基础的行为也就无法保证,应用程序就会变得混乱
打破双亲委派模型:继承java.lang.ClassLoader
并重写findClass()
方法
谈谈对Java中引用的了解?
强引用
:类似 Object obj = new Object()
这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
软引用
:用来描述一些有用但是非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常
之前,会将这些对象进行回收,如果回收之后还是没有足够的内存,那么就会抛出内存溢出异常
弱引用
:也是用来描述非必需对象的,强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器开始工作时,无论当前内存是否够用
,都会回收掉只被弱引用关联的对象
虚引用
:是最弱
的一种引用关系
谈谈对synchronized的理解?
- 解决的是多个线程之间访问资源的同步性,它可以保证被它修饰的方法或代码块在
任意时刻
只能有一个线程
执行 - 早期版本中,
synchronized
属于重量级锁
,如果要挂起或者阻塞某一个进程都需要操作系统帮忙完成,而操作系统实现线程中的切换需要从用户态
切换到内核态
,这个状态切换耗时比较长,所以早期synchronized
效率比较低;而后期JVM对其做了大量的优化,引入了偏向锁、轻量级锁、自旋锁、锁消除
synchronized
同步代码块的实现使用的是monitorenter
和monitorexit
指令,monitorenter
指向同步代码块的开始位置,monitorexit
指向同步代码块的结束位置;当执行monitorenter
指令时,线程试图获取锁,当计数器为0
则可以成功获取,获取后将锁计数器设为1
;相应的在执行monitorexit
指令后,将锁计数器设为0
,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗?
偏向锁
:如果一个线程是首次进入某个方法,会使用CAS
做标记,并且记录线程ID
,退出的时候不会改变方法的状态;当这个线程再次进入时,会判断该方法的状态,如果这个方法已经被标记有线程在执行并且线程ID
是自己的,那么就会直接进入这个方法轻量级锁
:如果偏向锁失败,虚拟机不会立即升级为重量级锁
,而是升级为轻量级锁
,它提升性能的依据是对于绝大部分锁,在整个同步期间内是不存在竞争的,如果发生了竞争,那么就会升级为重量级锁自旋锁
:轻量级锁
失败后,虚拟机为了避免线程真实地在操作系统
层面挂起,还会进行一项称为自旋锁
的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长
,虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起锁消除
:它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据
不可能存在竞争
,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
谈谈你对Java内存模型(JMM)的理解?
主内存与工作内存
:JMM
规定了所有的变量都存储在主内存
中。每条线程都有自己的工作内存
,线程的工作内存
中保存了被该线程使用的变量的主内存副本,线程对变量的操作(读取、赋值)都必须在工作内存
中进行,而不能直接读写主内存
中的数据。
JMM
保证了原子性、可见性、有序性
原子性
:在同一个时刻只能有一个线程对数据进行修改
可见性
:当一个线程修改了共享变量的值时,其他线程能立即得知这个修改
有序性
:使用synchronized和volatile两个关键字以及Happens-before原则来保证线程之间操作的有序性。
Happens-before
原则是指如果一个操作happens-before
另一个操作,那么第一个
操作的执行结果对于第二个
操作来说是可见的,而且第一个操作的执行顺序排在第二个操作之前;两个操作存在happens-before
关系,并不意味着Java平台的具体实现按照这个顺序来执行,如果重排序
之后的结果
不会被改变,这种排序就是合法
的
整理面经不易,觉得有帮助的小伙伴点个赞吧~感谢收看!