1、JVM的主要组成部分有哪些
JVM主要分为下面几部分
-
类加载器:负责将字节码文件加载到内存中
-
运行时数据区:用于保存java程序运行过程中需要用到的数据和相关信息
-
执行引擎:字节码文件并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎将字节码翻译成底层系统指令
-
本地库接口:会被执行引擎调用参与字节码的翻译
在这里面最主要的部分是运行时数据区,它又由五部分构成,分别是:堆、方法区、栈、本地方法栈、程序计数器
- 堆是对象实例存储的主要区域
- 方法区可以认为是堆的一部分,用于存储已被虚拟机加载的信息,比如常量、静态变量等等
- 栈是程序方法运行的主要区域,栈里面存的是栈帧,栈帧里面存的是局部变量表、操作数栈、动态链接、方法出口等信息
- 本地方法栈与栈功能相同,区别在于本地方法栈执行的是本地方法,即一个Java调用非Java代码的接口
- 程序计数器主要存放的是当前线程所执行的字节码的行号,用于记录正在执行的字节码指令的地址
2、堆栈的区别是什么
堆和栈都是JVM的主要组成部分,不同点在于:
- 栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的
- 堆会GC垃圾回收,而栈不会
- 栈内存是线程私有的,而堆内存是线程共有的
- 两者异常错误不同,栈空间不足:java.lang.StackOverFlowError,堆空间不足:java.lang.OutOfMemoryError
3、JVM的类加载器有哪些
类加载器的主要作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。根据各自加载范围的不同,主要划分为四种类加载器:
-
启动类加载器(BootStrap ClassLoader):用于加载JAVA_HOME/jre/lib目录下的类库
-
扩展类加载器(ExtClassLoader):用于加载JAVA_HOME/jre/lib/ext目录中的类库
-
应用类加载器(AppClassLoader):用于加载classPath下的类,也就是加载开发者自己编写的Java类
-
自定义类加载器:开发者自定义类继承ClassLoader,实现自定义类加载规则
4、什么是双亲委派模型
双亲委派模型是Java中的一种类加载机制。
在双亲委派模型中,类加载器之间形成了一种层次继承关系,从顶端开始依次是:启动类加载器->扩展类加载器->应用类加载器->自定义类加载器
当一个类加载器需要加载某个类时,它首先会委派给其上层类加载器去尝试加载该类。如果父类加载器无法加载该类,子类加载器才会尝试加载。
这种层次关系形成了一个从上到下的委派链。
双亲委派模型的主要目的是保证Java类的安全性和避免类的重复加载。当一个类加载器收到加载请求时,它会首先检查自己是否已经加载了该类。
如果已经加载,则直接返回该类的Class对象;如果未加载,则将加载请求委派给父类加载器。
父类加载器也会按照同样的方式进行检查,直到顶层的启动类加载器。如果顶层的启动类加载器无法加载该类,那么子类加载器会尝试自己加载。
这样可以避免同一个类被不同的类加载器加载多次,确保类的唯一性。
双亲委派模型的优势在于能够保证类的一致性和安全性。
通过委派链的机制,可以避免恶意代码通过自定义的类加载器加载替换系统核心类,从而提高了Java程序的安全性。
此外,通过双亲委派模型,可以实现类的共享和重用,减少内存占用和加载时间,提高了系统的性能。
5、说一下类加载器的执行过程
类从被加载到虚拟机内存中开始,直到卸载出内存为止,整个生命周期包括了7个阶段:加载、验证、准备、解析、初始化、使用、卸载
- 加载: 这个阶段会在内存中生成一个代表这个类的java.lang.Class对象
- 验证: 这个阶段的主要目的是为了确保Class文件包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
- 准备: 这个阶段正式为类变量分配内存并设置类变量的初始值,注意这里的初始值指的是默认值,而不是代码=后的实际值
- 解析: 这个阶段将符号引用替换为直接引用,比如方法中调用了其他方法,方法名可以理解为符号引用,而直接引用就是使用指针直接引用方法
- 初始化: 这个阶段是执行类构造器方法的过程,是类加载的最后一步,到了这一步Java虚拟机才开始真正执行类中定义的Java程序代码(字节码)
- 使用: 这个节点程序在运行
- 卸载: 这个阶段类Class对象被GC
6、怎么判断对象是否可以被回收
在堆中存放着几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件事就是要确定哪些对象是要回收的
JVM认为不被引用的对象就是可以被回收的对象,而它确认对象是否还在被引用的算法主要有两种:引用计数法和可达性分析算法
-
引用计数法
在对象头处维护一个counter,每增加一次对该对象的引用,计数器自加,如果对该对象的引用失联,则计数器自减
当counter为0时,表明该对象已经被废弃,不处于存活状态,
但是此方法存在问题,假设两个对象相互引用始终无法释放counter,则永远不能GC
-
可达性分析算法
通过一系列为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链
当一个对象到GC Roots没有任何引用链相连时,则证明该对象是不可用的
可以作为GC Roots的对象一般有栈中引用的对象 、方法区中类静态属性引用的对象以及
7、JVM的垃圾回收算法有哪些
目前JVM中的垃圾回收算法主要有四个,分别是:标记清除算法、标记-整理算法、复制算法和分代收集算法
-
标记清除算法是将垃圾回收分为2个阶段,分别是标记和清除
它会先使用根据可达性分析算法找到垃圾资源进行标记,然后对这些标记为可回收的内容进行垃圾回收
这种算法的主要不足有两个:
-
效率问题,标记和清除阶段都要遍历多有对象,并且在GC时,需要停止应用程序,对于交互性要求比较高的应用而言这个体验是非常差的
-
空间问题,对象被回收之后会产生大量不连续的内存碎片,当需要分配较大对象时,由于找不到合适的空闲内存而不得不再次触发垃圾回收动作
-
-
标记整理算法也是将垃圾回收分为2个阶段,分别是标记和整理清除
它的第一阶段也是会先将存活的对象先标记出来
不一样的地方在于第二阶段,它会将所有存活的对象向前移动放在一起,然后将无用空间回收,这样就会出现连续的可用空间了
所以它解决了空间碎片问题,但是效率低的问题依旧存在
-
复制算法,将原有的内存空间一分为二,每次只用其中的一半
在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将当前内存空间清空,交换两个内存的角色,完成垃圾的回收。
这种算法的缺点在于分配2块内存空间,在同一个时刻,只能使用一半,内存使用率较低
-
分代收集算法,它会将整个堆内存分成几部分空间,每个空间中放入不同类型的对象,然后各自适合的算法回收
在JDK8时,堆被分为了两份:新生代和老年代,默认空间比例为1:2
对于新生代,内部又被分为了三个区域:Eden区,S0区,S1区,,默认空间比例为8:1:1
它的基本工作机制是:
当创建一个对象的时候,这个对象会被分配在新生代的Eden区,当Eden区要满了时候,触发MinorGC
当进行MinorGC后,此时在Eden区存活的对象被移动到S0区,并且当前对象的年龄会加1,清空Eden区
当再一次触发MinorGC的时候,会把Eden区中存活下来的对象和S0中的对象,移动到S1区中,这些对象的年龄会加1,清空Eden区和S0区
当再一次触发YoungGC的时候,会把Eden区中存活下来的对象和S1中的对象,移动到S0区中,这些对象的年龄会加1,清空Eden区和S1区
对象的年龄达到了某一个限定的值(默认15岁),那么这个对象就会进入到老年代中,除此之外,大对象也会直接放入老年代空间
当老年代满了之后,触发FullGC**。**FullGC同时回收新生代和老年代
在上述过程中,新生代中的对象存活率比较低,所以选用复制算法;老年代中对象存活率高,所以使用标记-整理算法
小细节:
-
当对新生代产生GC:MinorGC,老年代代产生GC:Major GC ,新生代和老年代产生FullGC
-
Minor GC非常频繁,一般回收速度也很快,Major GC一般会伴随一次Minor GC,Major GC的速度要慢很多,一般要比Minor GC慢10倍
-
占用内存较大的对象,对于虚拟机内存分配是一个坏消息,虚拟机提供了一个-XX:PretenureSizeThreshold让大于这个设置的对象直接存入老年代
-
虚拟机给每个对象定义了一个Age年龄计数器,对象在Eden中出生并经过第一次Minor GC后仍然存活,年龄+1,此后每熬过一次Minor GC则年龄+1,
当年龄增加到一定程度(默认15岁),就会晋升到老年代。可通过参数设置晋升年龄 -XX:MaxTenuringThreshold
8、JVM的垃圾回收器都有哪些
JVM中常见的一些垃圾回收器有:
-
新生代回收器:Serial、ParNew、Parallel Scavenge
-
老年代回收器:Serial Old、Parallel Old、CMS
-
整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低
老年代回收器一般采用的是标记-整理的算法进行垃圾回收