JVM是如何让java代码被机器执行的?
Java 源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件, 而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。
JVM线程与系统线程的关系?
Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓 冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。 Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。当线程结束时, 会释放原生线程和 Java 线程的所有资源。
JVM系统线程主要有哪几个?
虚拟机线程 (VM thread) | 这个线程等待 JVM 到达安全点操作出现。这些操作必须要在独立的线程里执行,因为当 堆修改无法进行时,线程都需要 JVM 位于安全点。这些操作的类型有:stop-the- world 垃圾回收、线程栈 dump、线程暂停、线程偏向锁(biased locking)解除。 |
周期性任务线程 | 这线程负责定时器事件(也就是中断),用来调度周期性操作的执行。 |
GC 线程 | 这些线程支持 JVM 中不同的垃圾回收活动。 |
编译器线程 | 这些线程在运行时将字节码动态编译成本地平台相关的机器码。 |
信号分发线程 | 这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。 |
JVM内存分为哪些区域?每个区域包含什么?
JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法栈】、线程共享区 域【JAVA 堆、方法区】、直接内存。
什么是程序计数器( 线程私有),它会 OutOfMemoryError吗?
一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的 程序计数器,这类内存也称为“线程私有”的内存。不会OutOfMemoryError
什么是虚拟机栈( 线程私有)?
是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成 的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
什么是本地方法栈( 线程私有)?
虚拟机栈为执行 Java 方法服务, 而本地方法栈则 为 Native 方法服务,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。
简单介绍一下JVM中的堆和方法区
- 堆是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行 垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以 细分为: 新生代( Eden 区 、 From Survivor 区 和 To Survivor 区 )和老年代。
- 方法区即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java 堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存, 而不必为方法区开发专门的内存管理器。
JVM新生代 Eden 区、ServivorFrom、ServivorTo 三个区的意思
- Eden 区:Java 新对象的出生地(如果新创建的对象占用内存很大,则直 接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
- ServivorTo:保留了一次 MinorGC 过程中的幸存者。
- ServivorFrom:上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
新生代 MinorGC采用什么算法?执行过程是怎么样的?
MinorGC 采用复制算法。首先,把 Eden 和 ServivorFrom 区域中 存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代 区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);然后,清空Eden 和 ServicorFrom 中的对象;最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为 下一次 GC 时的 ServicorFrom 区。
MajorGC采用什么算法?执行过程是怎么样的?
MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长,因为要扫描再回收。MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的 时候,就会抛出 OOM(OutOfMemory)异常。
java8后JVM永久代做了哪些改变?
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
JVM中是如何确定对象数据是垃圾的?
- 引用计数法:一个对象如果没有任何与之关 联的引用,即他们的引用计数都为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
- 可达性分析:如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
简单介绍一下标记清除算法( Mark-Sweep )
最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。
简单介绍一下复制算法( copying )
为解决 Mark-Sweep 算法内存碎片化而被提出的算法。按内存容量将内存划分为等大小 的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用 的内存清掉,这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原 本的一半。且存活对象增多的话,Copying 算法的效率会大大降低。
简单介绍一下标记整理算法(Mark-Compact)
结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
简单介绍一下分代收集算法
分代收集法是目前大部分 JVM 所采用的方法,其核心思想是根据对象存活的不同生命周期将内存 划分为不同的域,一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。大部分 JVM 的 GC 对于新生代都采取复制算法,因为新生代中每次垃圾回收都要 回收大部分对象,即要复制的操作比较少,但通常并不是按照 1:1 来划分新生代。一般将新生代 划分为一块较大的 Eden 空间和两个较小的 Survivor 空间(From Space, To Space),每次使用 Eden 空间和其中的一块 Survivor 空间,当进行回收时,将该两块空间中还存活的对象复制到另 一块 Survivor 空间中。而老生代因为每次只回收少量对象,因而采用标记整理算法。当新生代的 Eden Space 和 From Space 空间不足时就会发 生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后 将 Eden Space 和 From Space 进行清理。如果 To Space 无法足够存储某个对象,则将这个对象 存储到老生代。在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。当对象 在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老生代 中。
JAVA 四中引用类型是哪四种,简单介绍一下
- 强引用:在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引 用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之 一。
- 软引用:软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
- 弱引用:弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象 来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
- 虚引用:虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
GC 垃圾收集器有几种,分别是什么?
- Serial 垃圾收集器,特点:CPU利用率最高,停顿时间即用户等待时间比较长。适用场景:小型应用通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
- ParNew 垃圾收集器,参数控制:-XX:+UseParNewGC ParNew收集器 -XX:ParallelGCThreads 限制线程数量
- Parallel Scavenge 收集器,采用多线程来通过扫描并压缩堆 特点:停顿时间短,回收效率高,对吞吐量要求高。 适用场景:大型应用,科学计算,大规模数据采集等。 通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
- Serial Old 收集器
- Parallel Old收集器
- Concurrent mark sweep(CMS)收集器,优点:并发收集、低停顿 缺点:产生大量空间碎片、并发阶段会降低吞吐量,特点:响应时间优先,减少垃圾收集停顿时间 适应场景:大型服务器等。 通过JVM参数 -XX:+UseConcMarkSweepGC设置
- G1收集器,特点:支持很大的堆,高吞吐量,通过JVM参数 -XX:+UseG1GC 使用G1垃圾回收器
JVM 类加载机制分为哪五个部分,都做些什么?
JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这 五个过程。
- 加载:加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对 象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既 可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理), 也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)
- 验证:这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 准备:准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。
- 解析:解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的常量
- 初始化:初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。
简单介绍一下JVM三种类加载器?
- 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过- Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类
- 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通 过 java.ext.dirs 系统变量指定路径中的类库。
- 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。
什么是类加载?
- 类加载指的是将类 Class 文件读入内存,并为之创建一个 java.lang.Class 对象, class 文件被载入到了内存之后,才能被其它 class 所引用
- jvm 启动的时候,并不会一次性加载所有的 class 文件,而是根据需要去动态加载
- java 类加载器是 jre 的一部分,负责动态加载 java 类到 java 虚拟机的内存
- 类的唯一性由类加载器和类共同决定(双亲委派机制)
如何自定义实现自己的类加载器?
JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader 实现自定义的类加载器。
JVM 通过双亲委派模型进行类的加载,那么是如何加载的?
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。
常见参数配置
-XX:+PrintGC 每次触发GC的时候 打印相关日志 -XX:+UseSerialGC 串行回收 -XX:+PrintGCDetails 更详细的GC日志 -Xms 堆初始值 -Xmx 堆最大可用值 -Xmn 新生代堆最大可用值 -XX:SurvivorRatio 用来设置新生代中eden空间和from/to空间的比例. -XX:NewRatio 配置新生代与老年代占比 1:2 含以-XX:SurvivorRatio=eden/from=den/to **总结:在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,** **这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率。**
谈谈你对JAVA 模块化开发的理解
什么是字节码?采用字节码的好处是什么?
- JVM可以理解的代码就叫做 字节码(即扩展名为 .class 的文件)
- Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
在Java语言里,可作为GC Roots对象的包括如下几种:
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中JNI的引用的对象
为什么经常会说 Java 是编译与解释共存的语言。
JVM引进了 JIT 编译器,JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。
Java 语言为什么可以“一次编译,随处可以运行”?
JVM是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它都会给出相同的结果。字节码和不同系统的 JVM 实现,是 Java 语言“一次编译,随处可以运 行”的关键所在。
java GC发生在会么时候,对什么东西,做了什么事情?
- 程序员不能具体控制时间,系统在不可预测的时间调用System.gc()函数的时候;当然可以通过调优,用NewRatio控制newObject和oldObject的比例,用MaxTenuringThreshold 控制进入oldObject的次数,使得oldObject 存储空间延迟达到full gc,从而使得计时器引发gc时间延迟OOM的时间延迟,以延长对象生存期。
- 超出了作用域或引用计数为空的对象;从gc root开始搜索找不到的对象,而且经过一次标记、清理,仍然没有复活的对象。
- 如新生代做的是复制清理、from survivor、to survivor是干啥用的、老年代做的是标记清理、标记清理后碎片要不要整理、复制清理和标记清理有有什么优劣势等
常用的 JVM 调优的参数都有哪些?
- -Xms2g:初始化推大小为 2g;
- -Xmx2g:堆最大内存为 2g;
- -XX:NewRatio=4:设置年轻的和老年代的内存比例为 1:4;
- -XX:SurvivorRatio=8:设置新生代 Eden 和 Survivor 比例为 8:2;
- –XX:+UseParNewGC:指定使用 ParNew + Serial Old 垃圾回收器组合;
- -XX:+UseParallelOldGC:指定使用 ParNew + ParNew Old 垃圾回收器组合;
- -XX:+UseConcMarkSweepGC:指定使用 CMS + Serial Old 垃圾回收器组合;
- -XX:+PrintGC:开启打印 gc 信息;
- -XX:+PrintGCDetails:打印 gc 详细信息。
调优总结:初始堆值和最大堆内存内存越大,吞吐量就越高。 最好使用并行收集器,因为并行收集器速度比串行吞吐量高,速度快。 设置堆内存新生代的比例和老年代的比例最好为1:2或者1:3。减少GC对老年代的回收。
简述分代垃圾回收器是怎么工作的?
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。 新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
- 把 Eden + From Survivor 存活的对象放入 To Survivor 区;
- 清空 Eden 和 From Survivor 分区;
- From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。 老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?
- 新生代回收器:Serial、ParNew、Parallel Scavenge
- 老年代回收器:Serial Old、Parallel Old、CMS
- 整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
说一下 JVM 有哪些垃圾回收算法?
- 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
- 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
- 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
- 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
说一下类装载的执行过程?
类装载分为以下 5 个步骤:
- 加载:根据查找路径找到相应的 class 文件然后导入;
- 验证:检查加载的 class 文件的正确性;
- 准备:给类中的静态变量分配内存空间;
- 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
- 初始化:对静态变量和静态代码块执行初始化工作。
说一下堆栈的区别?
- 功能方面:堆是用来存放对象的,栈是用来执行程序的。
- 共享性:堆是线程共享的,栈是线程私有的。
- 空间大小:堆大小远远大于栈。
说一下 JVM 运行时数据区?
不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范规定的区域分为以下 5 个部分:
- 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;
- Java 虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
- 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
- Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;
- 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
说一下 JVM 的主要组成部分?及其作用?
- 类加载器(ClassLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Native Interface)
组件的作用: 首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。