1、JVM是什么,主要作用是什么?
JVM是Java虚拟机(Java Virtual machine的缩写),JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需要生成在Java虚拟机上运行到字节码,就可以在在不同平台上运行,是Java可以一次编写,到处运行的保证。
2、描述一下JVM的内存区域?
JVM在执行Java程序的过程中,会把它管理单内存分为若干个不同的区域,这些区域有些是线程私有的,有些是线程共享的,Java的内存区域也叫运行时数据区,具体划分为:
- 虚拟机栈:Java虚拟机栈是线程私有的数据区,Java虚拟机栈的生命周期和线程相同;虚拟机栈也是局部变量的存储位置;方法在执行过程中,会在虚拟机栈中创建一个栈帧(stack frame),栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息;每个方法执行的过程就对应了一个入栈和出栈的过程。
- 本地方法栈:本地方法栈也是线程私有的数据区,本地方法栈存储的是Java中使用native关键字修饰的方法(这些方法用来调用底层的C/C++方法)。
- 程序计数器:也叫PC寄存器,也是程序私有的数据区,程序计数器用于存储线程的指令地址,用于判断线程的分支、循环、跳转、异常、线程切换和恢复等功能,这些都是通过程序计数器来完成。
- 方法区:方法区是各个线程共享的内存区域,用于存储虚拟机加载到类信息(如成员变量、成员方法、构构造函数等)、常量、静态变量、即时编译器编译后的代码等数据。
- 堆:堆是线程共享的内存区域,是JVM中最大的一块内存区域,所有的对象实例都会分配在堆上;堆在逻辑上分为新生代、老年代和永久代,不过在物理上,只有新生代和老年代,因为永久代具体实现为了方法区(永久代在jdk8中改为了元空间);新生代又分为伊甸区(Eden)、幸存者0区(survivor 0)和幸存者1区(survivor 1)。
- eden区:占用8/10的新生代空间;
- surivivor 0 和 survivor 1:各占1/10的新生代空间;
- 新生代:占用1/3的堆空间
- 老年代:占用2/3的堆空间;
- 运行时常量池:这块区域是方法区的一部分,通常被称为非堆,它并不邀请常量一定只有在编译期才能产生,也就是并非编译期间将常量方法放在常量池中,运行期间也可以将新的常量放入常量池中,String的intern方法就是一个典型的例子;
3、描述一下Java中的类加载机制?
JVM负责把描述类的数据从.class文件加载到系统内存中,并对类的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称之为Java的类加载机制。
一个类从被加载到虚拟机内存开始,到卸载出内存,一般会经历下面这些过程:
加载、链接、初始化、使用和卸载,这五个阶段的顺序是确定;而其中的链接过程又分为具体到验证、准备、解析三步,这三个阶段的顺序是不确定的,通常交互进行,解析阶段通常会在初始化之后再开始,这时为了支持java语言的运行时绑定特性(也被称为)动态绑定。
下面来具体说下这些过程:
- 加载:加载是整个类加载过程的第一个阶段,在这个阶段,JVM需要完成三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流;
- 将这个字节流表示的一种存储结构转换为运行时数据区中方法区的数据结构;
- 在内存中生成一个Class对象,这个对象就代表了这个数据结构的访问入口;
数组的加载不需要通过类加载器来创建,它是直接在内存中分配,但是数组的元素类型最终还是要看靠类加载器来完成加载。
- 验证:加载之后的下一个阶段就是验证,在加载阶段,会在内存中生成一个Class对象,这个对象示访问其代表数据结构的入口,所以这一步的验证工作就是确保Class文件的字节流中的内容符合《Java虚拟机规范》的要求,保证这些信息被当做代码运行后,不会威胁到虚拟机的安全。
验证阶段主要分为四个阶段的检验(具体到验证信息这里就不说了):
- 文件格式验证;
- 元数据验证;
- 字节码验证;
- 符号引用验证;
验证阶段对虚拟机来说非常重要,如果能够通过验证,就说明你的程序在运行时不会产生任何意外影响。
- 准备:准备阶段是为类中的变量分配内存并设置其初始值的阶段,这些变量所使用的内存都应当在方法区中进行分配,在JDK7之前,HotSpot使用永久代来实现方法区,是符合这种逻辑概念的,而在JDK8之后,变量会随着Class对象一起存放在Java堆中;
—>这里要注意,JDK8中,Class对象是存放在堆中的,而不是方法区。类的元数据才是存放在方法区的。元数据并不是类的Class对象,Class对象示加载到最终产品,而类的方法代码,变量名、方法名,访问权限,返回值等等才是方法在方法区的;(在一个JVM实例的内部,类型信息被存储在一个称为方法区的内存逻辑区中。类型信息是由类加载器在类加载时从类文件中提取出来的。类(静态)变量也存储在方法区中。)
在准备阶段,为变量的分配的值是初始值,通常情况下,比如int分配0,引用类型分配null;还存在一些特殊情况,如果类字段属性中存在常量属性,比如 public static final int value = 666; 那么这时值就会设为666.
- 解析:解析阶段是JVM将常量池内的符号引用替换为直接引用的过程;
- 符号引用:符号引用以一组符号来描述所引用的目标,符号引用可以使任何形式的字面量,只要使用时能够无歧义的定位到目标即可,符号引用和虚拟机的布局无关。
- 直接引用:直接引用可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。直接引用和虚拟机的布局是相关的,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的。如果有了直接引用,那么直接引用的目标一定被加载到了内存中。
或者这样理解:
在编译的时候一个每个 Java 类都会被编译成一个 class 文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
解析也分为四个步骤:类或接口的解析;字段解析;方法解析;接口方法解析;
- 初始化:初始化是类加载过程的最后一个步骤,在之前的阶段中,都是JVM占主导作用,但是到了初始化这一步,却把主动权移交给应用程序。
(类只有在被首次主动使用时才会初始化,后续的使用不会再一次初始化。 这也说明了为什么静态代码块只会执行一次:因为静态代码块是属于类的,而类只会初始化一次)
只有以下情况会触发类的初始化:
(1)当遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,比如new一个类,读取一个静态字段(未被final修饰)、或调用一个类的景天方法时。
- 当jvm执行new指令时会初始化类;即当程序创建一个类的实例对象时;
- 当jvm执行getstatic指令时会初始化类;即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池);
- 当jvm执行putstatic指令时会初始化傀儡;即程序给类的静态变量赋值时;
当jvm执行invokestatic指令时会初始化类;即程序调用类的惊天方法时;
(2)使用java.lang.reflect包的方法对类进行反射调用时,如Class.forname(“…”),newInstance()等等,这时如果类没有初始化,需要触发其初始化;
(3)初始化一个类,如果其父类还没初始化,那么先触发其父类的初始化;
(4)当虚拟机启动时,用户需要定义一个要执行的主类(包含main方法的那个类),虚拟机会初始阿虎这个类;
(5)MethodHanle和VarHandle可以看作时轻量级的反射调用机制,而想要使用这两个调用,就必须先使用findStaticVarHandle来初始化要调用的类;
补充: (6)当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法时),如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化;使用:这没什么好说的,就是初始化之后的代码由JVM来动态调用执行;
- 卸载:当代表一个类的Class对象不再被引用,那么Class对象的生命周期就结束了,对应的在方法区中的数据也会被卸载。但需要注意,JVM自带的类加载器装载的类,是不会被写在的,由用户自定义的类加载器加载到类是可以卸载的。
4、在JVM中,对象是如何创建的?
我们通常使用new关键字来创建对象,当虚拟机遇到一个创建对象的指令的时候,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用所代表的类是否已经被加载、解析和初始化。如果发现这个类没有经过上面类加载的过程,那么就执行相应的类加载过程。
类加载完成后,虚拟机会为新生对象分配内存,对象所需的大小在类加载完成后便可以确定。
接下来,JVM还会对对象进行一些必要的设置,比如确定对象是哪个类的实例、对象的hashcode、对象的gc分代年龄信息等,这些信息存放在对象的对象头(Object Header)中。
如果上面的工作都做完,那么从虚拟机的角度来说,一个新的对象就创建完毕了,但是对于我们来说,对象的创建才刚刚开始,因为类的构造函数,也就是Class文件中的
5、JVM什么时候会进行垃圾回收?
满足以下情景之一就会触发垃圾回收:
- 当伊甸区或者幸存者区不够用了;
- 当老年代不够用了
- 当方法区不够用了
通过System.gc()通知jvm进行一次垃圾回收,但是具体到执行还要看JVM,另外尽量不要再代码中使用这个,毕竟GC一个还是很消耗资源的;
6、JVM如何判断一个对象需要被回收(或者说如何判断对象是否存活/如何判断对象跟是否为垃圾)?
在jvm中判断一个对象是否可以被回收,最重要的是判断这个兑现是否还在被使用,只要没有被使用,那么这个对象就可以被回收;
主要有两种算法:引用计数法和可达性算法。
引用计数法就是在对象中添加一个字段作为引用计数器,每当有一个地方引用这个对象时,计数器的值就加一,当引用失效时,计数器的值就减一,计数器为0的对象就是可以被回收的对象。
这个算法实现比较简单,效率比较高,但是需要额外的空间区存储引用计数器,同时无法解决对象之间的循环引用问题,所以目前主流的JVM用的是另外一个算法:可达性算法。
可达性算法的基本思路可以理解为一颗多叉树,首先确定一系列不能回收的对象,作为GC roots,比如说虚拟机栈里面的一个引用对象,以及本地方法栈里面的一些对象等,以GC roots作为起始节点,从这些节点开始向下搜索,去寻找它的直接或者间接的引用对象,当遍历完以后,如果发现一些对象是不可达的,那么就认为这些对象已经没有引用了,说明这些对象时可以被回收的。7、垃圾回收有哪些算法?
首先,垃圾回收总体上来说是一种分代收集算法,在次数上,频繁收集新生代,较少收集老年带,基本上不会去收集元空间。jvm在进行GC时,并不是每次都对新生代、老年代、元空间一起回收,大部分都是回收新生代。
然后具体的垃圾收集算法主要有三种:
(1)标记-清除算法:这是最基础的算法,主要思想就是先标记出所有需要回收的独享,然后同一回收掉所有被标记的对象。
这种算法主要有两个缺点:- 执行效率不稳定;如果堆中包含大量对象,而且其中大部分是需要被回收的,这时就必须进行大量的标记和清除动作,也就是说执行效率和对象数量成反比;
- 会有内存空间碎片化的问题;标记-清除之后会产生大量的不连续的内存碎片,空间碎片太多可能会导致当程序运行过程之需要分配较大对象时,因无法找到足够的连续内存,而不得不提前触发领一次垃圾收集动作。
后续的标记-复制算法和标记-整理算法都是在标记-清除算法的基础上做的改进。
(2)标记-复制算法:主要思想是将可用内存按容量划分为大小相等的两块,每次只试用期中一块。当正在使用的这块内存用完了,就把还存活着的对象复制到另一块上,然后再把已使用的这一块内存空间一次性全部清理掉。
这种算法也有两个比较明显的问题:
- 不适用于对象存活率较高的情况(即一般不适用于老年代)
- 可用空间内存缩小了一半。 (不过堆内存,分为Eden区和Survivor 0区及Survivor 1区解决了这个问题)
(3)标记-整理算法:主要思想是让所有存活的对象都向内存空间的一端移动,然后直接清理掉边界意外的内存。这种移动式的算法相对于标记-清除算法来说,吞吐量更高,但是速度相对较慢,因为移动对象需要全程暂停用户应用程序。
8、能不能自己写一个类叫java.lang.String?JVM的双亲委派模型?
JVM中内置了三种类加载器,类加载器之间是有层级关系的。
首先,是启动类加载器Bootstrap ClassLoader,它是最顶层的类加载器,由C++实现,负载加载%JAVA_HOME%/lib目录下的jar包和类,或者被-Xbootclasspath参数指定的路径中的所有类;
然后,是拓展类加载器Extension ClassLoader,它是第二层的类加载器,由java编写,主要负责加载拓展类,如%JRE_HOME%/lib/ext 目录下的 jar 包和类,或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。
最后,是应用程序类加载器App ClassLoader,这是面向我们用户的类加载器,负责加载当前应用classpath下的所有jar包和类;
然后还有用户可以自己定义类加载器,但是自定义的加载器也是位于层级关系的最下层;
->基于此层级关系,类加载又有双亲委派机制:
系统中的类加载器在工作时,会默认使用双亲委派机制。当一个类在加载的时候,系统会首先判断它是在JVM中表示两个class对象是否为同一个类存在两个必要条件:否被加载过,已经被加载过的类会直接返回,否则才会尝试加载; 加载的时候,首先会把请求委派给上层的加载类处理,每一层的加载器都会这样,把请求委派给自己的上层加载器进行处理;因此所有的请求最终都会委派给顶层的启动类加载器进行处理,只有当顶层的加载器无法处理,才会自己处理。
也正因此,我们自己写的java.lang.String类,在加载的时候,实际会被启动类加载器加载,
而启动类加载器会找到jdk中已经定义的Stirng,因此还是会使用jdk中的String,我们自己写这个类是没有意义的。