Java虚拟机


1、JVM是什么?

JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的
引入Java虚拟机后,Java程序在不同平台上运行时不需要重新编译。
Java使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java编译程序只需要生成在Java虚拟机上运行的字节码文件,就可以在各种平台上不加修改地运行。

2、JVM的主要组成部分及其作用?

JVM

JVM包含两个子系统和两个组件,两个子系统为类加载和执行引擎;两个组件为运行时数据区、本地接口。

  • 类加载(Class Loader):根据给定的全限定类名(如java.lang.Object)来装在class文件到运行时数据区域的方法区。
  • 执行引擎(Execution Engine):执行class中的指令。
  • 运行时数据区域(Runtime Data Area):JVM的内存,也就是堆区和栈区。
  • 本地接口(Native Interface):与本地方法库交互,是与其它编程语言交互的接口。

各部分作用:

首先编译器将Java代码转换为字节码,类加载器再将字节码加载到内存中,
将其放在运行时数据区的方法区内,而字节码文件只是JVM的一套指令集规范,
并不能直接交给底层操作系统区执行,因此需要特定的命令解析器即执行引擎,
将字节码翻译成底层系统指令,再交由CPU去执行,
而这个过程需要调用其他语言的本地库接口来实现整个程序的功能。

3、运行时数据区?

JVM在执行Java程序时,会把其所管理的内存划分成多个区域,每个区域都有不同用途,每个区域的创建和销毁时间也不同。
不同虚拟机的运行时数据区可能略微有所不同,但都会遵从Java虚拟机规范,Java虚拟机规范规定的区域分为以下5个部分:

  • 程序计数器:记录当前线程所执行的字节码的行号,字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
    JVM的多线程是通过线程轮流切换并分配处理器来实现的,所以为了保证个线程指令的安全顺序执行,每条线程都有独立的私有的程序计数器
  • 虚拟机栈:与程序计数器一样,虚拟机栈也是线程私有的,且生命周期与线程相同。在栈中,每执行一个方法前都会创建一个栈帧(栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构),来存储局部变量表、操作数栈、动态链接以及方法出口等信息,每个方法从调用到完成,就对应栈中的一个栈帧的压栈到弹出的过程
  • 本地方法栈:虚拟机栈是服务Java方法的,而本地方法栈是为虚拟机调用本地方法服务的。
  • 所有线程共享一块内存区域,在虚拟机开启的时候创建,它是JVM内存里最大的一块内存,用来存放对象实例。
  • 方法区:JVM虚拟机规范中把方法区描述为堆的一个逻辑部分,与堆一样,是所有线程共享的内存区域,用于存储已经被虚拟机加载的类信息、常量、静态变量,即时编译器编译后的代码缓存等数据。

JVM的内存模型中一共有两个栈,分别是:虚拟机栈和本地方法栈,两个栈的功能类似,都是方法运行过程的内存模型,并且两个栈的内部构造相同,都是线程私有。只不过虚拟机栈描述的是Java方法运行过程的内存模型,而本地方法栈式描述Java本地方法运行过程的内存模型。

JVM的内存模型一共有两个堆,分别是:原本的堆和方法区。方法区本质上属于堆的一个逻辑部分,堆中存放对象,方法区中存放类信息、常量、静态变量、即时编译器的代码。

堆是Java中最大的一块内存区域,也是垃圾收集器主要的工作区域。
程序计数器、虚拟机栈、本地方法栈是线程私有的,它们的生命周期和所属的线程一样,而堆、方法区是线程共享的,在JVM中只有一个堆、一个方法区,并且在JVM启动时创建,JVM停止时才销毁。

4、直接内存?

直接内存并不是虚拟机运行时数据区的一部分,也不是JVM虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError出现。

比如NIO(non-blocking io)使用本地库函数直接分配堆外内存,然后通过一个DirectByteBuffer对象作为这块内存地引用,可以大幅度减轻堆的负担,提高虚拟机性能。

5、JVM中堆栈的区别?

  • 物理地址:堆的物理地址分配对象是不连续的,性能慢;栈的物理地址分配是连续的,性能快。
  • 内存分配:堆分配内存是在运行期间确认的,大小不固定;栈分配的内存大小要在编译期就确认,大小是固定的。
  • 存放内容:堆存放的是对象的实例和数组,站存放的是局部变量、操作数栈、返回结果。
  • 程序可见度:堆对于整个程序共享可见,栈是线程私有的,生命周期与所属线程相同。

6、Java会存在内存泄露吗?

内存泄漏是指不再被使用的对象或者变量一直占据在内存中,理论上说,Java是有GC垃圾回收机制的,也就是说不再被使用的对象,会被GC自动回收掉,自动从内存中清除。

但是即使这样,Java仍存在着内存泄露的情况,原因是:长生命周期的对象持有短生命周期的对象的引用很有可能发生内存泄漏,尽管短生命周期对象已经不再需要,但因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。

7、对象是如何创建的?

(1)当虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须执行相应的类加载
(2)类加载通过后,已经确定了对象所需内存的空间大小,接下来为对象分配内存空间:若Java堆中的内存是绝对规整的,使用指针“碰撞方式”方式分配内存;若不是规整的,使用“空闲列表”方式分配内存。

分配内存时,还需要考虑并发问题,有两种方式解决并发:
1)对分配内存空间的动作进行同步,即CAS同步处理;
2)把内存分配的动作按照线程划分在不同的空间中进行,每个线程在Java堆中预先分配一小块内存,
即本地线程分配缓冲TLAB,TLAB使用完后,分配新的TLAB时才需要同步锁定。

(3)内存分配完成后,需要初始化对象信息,主要涉及属性默认值,对象头信息以及执行构造函数

8、垃圾回收机制?

在Java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会出发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

9、GC是什么?为什么要GC?

GC(Garbage Collection)就是垃圾回收的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。

10、如何可以判断对象是否可以回收?

垃圾回收器在垃圾回收之前,需要判断对象是否存活,只有死亡的对象才能被GC回收,常用的两种方式是:引用计数法和可达性分析

  • 引用计数法
给对象添加一个引用计数器,每当有一个对象引用时+1,当引用失效时-1,任何时刻计数器为0的对象就是可以被回收的对象。
引用计数器实现虽然简单高效,但很难解决对象之间循环引用的问题。
  • 可达性分析
通过一系列称为GC Roots的对象作为起始点,从这些点向下搜索,可达的对象都是存活的,不可达的对象可被回收。
GC Roots包括以下对象:
虚拟机栈中局部变量表中引用的对象
本地方法栈中JNI(Java Native Interface)引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象

11、方法区的回收?

永久代指的是内存的永久保存区域,主要存放Class和元数据的信息。jdk1.8中,永久代已经被移除,被一个称为元空间的区域所取代。
元空间和永久代之间的最大区别在于:元空间并不在虚拟机中,而是使用本地内存。

方法区主要存放永久代对象,而永久代对象的回收率要比新生代低很多,所以在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载的功能。
类卸载的条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:

  • 该类的所有实例都已经被回收,此时堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的Class对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

12、引用类型?

无论是通过引用计数法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判断对象的回收都与引用有关。
Java提供了4中强度不同的引用类型。

  • 强引用new一个新的对象方式来创建强引用。被强引用关联的对象不会被回收。
  • 软引用使用SoftReference类来创建软引用。被软引用关联的对象只有在内存不够的情况下才会被回收。
  • 弱引用使用WeakReference类来创建弱引用。被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收之前。
  • 虚引用:使用PhantomReference来创建虚引用。无法通过虚引用得到一个对象,为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。

13、垃圾回收算法有哪些?

  • Mark-Sweep(标记-清除)算法:该算法分为标记和清除两个阶段,标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记对象所占用的空间。
    优点:实现简单,不需要对象进行移动。
    缺点:标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。
  • Copying(复制)算法:为了解决标记清除算法的缺陷,复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完,就将还存活着的对象复制到另一块上面,然后再把已使用的内存空间一次清理掉,这样就不容易出现内存碎片问题。
    优点:实现简单、运行高效,不用考虑内存碎片。
    缺点:可用的内存大小缩小为原来的一般,对象存活率高时会频繁地进行复制。
  • Mark-Compact(标记-整理)算法:在新生代中可以使用复制算法,但老年代中对象存活率较高,这样会有较多地复制操作,导致效率变低。标记清除算法应用在老年代时,容易产生大量内存碎片。因此出现了标记-整理算法,也称压缩算法。该算法标记阶段和标记清除算法一样,但是在完成标记后,它不是直接清除可回收对象,而是将存活对象都移向一端,然后清理掉端边界以外地内存。
    优点:不会产生内存碎片。
    缺点:仍需要进行局部对象移动,一定程度上降低了效率。
  • Generation Collection(分代收集)算法:它是目前大部分JVM地垃圾回收器采用地算法,核心思想是根据对象存活的生命周期将内存划分为若干个不同区域,不同区域采用适当的收集算法。
    一般分为新生代和老年代。
    新生代使用复制算法;老年代使用标记-清除或标记整理算法。

14、JVM都有哪些垃圾收集器?

如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
下图展示了7中作用于不同分代的收集器:
回收器
其中用于新生代的垃圾收集器包括:

  • Serial收集器:新生代单线程收集器,标记和清理都是单线程,简单高效;
  • ParNew收集器:新生代并行收集器(并行指的是多个垃圾回收线程),实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
  • Parallel Scavenge收集器:新生代并行收集器,追求高吞吐量(指的是CPU用于运行用户程序的时间占总时间的比值),高效利用CPU。

用于老年代的垃圾收集器包括:

  • Serial Old收集器:Serial收集器的老年代版本;
  • Parallel Old收集器:Parallel Old收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器:并发收集器(垃圾回收线程与用户线程并发执行),并且以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间,但是会产生内存碎片。

用于回收整个Java堆的垃圾收集器:

  • G1(Garbage First)收集器:Java堆并行收集器,基于标记-整理算法实现,不会产生内存碎片。G1回收的范围是整个Java堆,包括新生代、老年代。

15、触发GC的条件?

  • 从年轻代空间回收内存被称为Minor GC,当年轻代空间不足时,就会触发Minor GC。
  • 在CMS收集器中,当老年代满时会触发Major GC,且目前只有CMS收集器会有单独收集老年代的行为。
  • Full GC用来回收老年代和新生代,触发条件由:调用System.gc()、老年代空间不足、通过Minor GC后进入老年代的空间大于老年代可用内存、JDK1.7及以前的永久代空间不足等。

16、JVM加载class文件的原理机制?

Java中所有的类都需要由类加载器装载到JVM中才能运行。
类加载器本身也是一个类,它的工作就是把class文件从硬盘读到内存中。
在写程序时,我们几乎不需要关系类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,比如反射就需要显示的加载所需要的类。
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类加载到JVM中,至于其他类则是在需要的时候才加载。

17、类加载的过程?

类加载可以分为加载、验证、准备、解析、初始化几个步骤。
(1)加载过程完成以下三件事:

  • 通过类的完全限定名称获取定义该类的二进制字节流。
  • 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
  • 在内存中生成一个代表该类的Class对象,作为方法区中该类各种数据的访问入口。

(2)验证:目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,
(3)准备:为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配,实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次。
(4)解析:将常量池的符号引用替换为直接引用的过程,其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持Java的动态绑定。
(5)初始化:到初始化阶段,才开始真正执行类中定义的Java程序代码,此阶段是执行<clinit>()方法的过程。该方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有变量的赋值动作和静态代码块中的语句合并产生的。此过程不包括构造器中的语句

18、类初始化顺序?

1)父类静态变量/静态初始化块-子类静态变量/静态初始化块;
2)父类变量/初始化块-父类构造器;
3)子类变量/初始化块-子类构造器。

19、什么是类加载器?类加载器有哪些?

实现通过类的权限限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有以下4种类加载器:

  • 启动类加载器(Bootstrap ClassLoader),使用C++语言编写,用来加载Java核心类库JAVA_HOME/lib,它无法被应用程序直接使用。
  • 扩展类加载器(Extension ClassLoader),使用Java编写,且父类加载器是Bootstrap,用来加载扩展库里面的类JAVA_HOME/lib/ext
  • 系统类加载器(Application ClassLoader),根据Java的类路径(CLASSPATH)来加载Java类,

20、什么是双亲委派模型?

应用程序由三种类加载器互相配合从而实现类加载,除此之外还可以加入自己定义的类加载器。
下图展示了类加载器之间的关系,称为双亲委派模型。该模型要求除了顶层的启动类加载器之外,其他的类加载器都要有自己的父类加载器,这里的父子关系一般通过组合关系来实现,而不是继承关系。
在这里插入图片描述
工作过程:

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器
去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,
只有当父类加载器反馈无法加载这个请求时,子加载器才会尝试自己去加载。

好处:

使得Java类随着它的类加载器一起具有一种带优先级的层次关系,从而使得基础类得到统一。

参考
JVM
http://www.cyc2018.xyz/Java
如有侵权,请联系作者删除

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值