八股文-JVM
是什么?有什么用?谁发明的?什么时候发明的?
Java虚拟机,用来运行Java程序,有很多个版本的虚拟机,比如HotSpot,最开始是SUN公司开发人员,和Java一起发布,现在被Oracle收购了
计算机不能直接运行吗?为啥要中间多一层虚拟机?
计算机可以执行机器代码,很多编程语言也是编译成机器码,j交给计算机直接运行的,不过Java为了实现一次编写代码,可以跨平台执行,所以加了一层虚拟机,同时虚拟机也简化了我们的编码,比如自动内存管理、垃圾回收、编译优化(解释执行/即时编译)
特性
跨平台、安全、内存管理、性能优化(即时编译-JIT)、多线程支持
运⾏时数据区
方法区、堆、栈(Java方法栈、本地方法栈)、程序计数器、
类加载过程
类加载是指,把编译好的class文件加载到运行时数据区,需要经过以下过程:
1、加载:加载是指查找字节流,并且据此创建类的过程。加载需要借助类加载器,在 Java 虚拟机中,类加载器使用了双亲委派模型,即接收到加载请求时,会先将请求转发给父类加载器。
2、链接:是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。链接还分验证、准备和解析三个阶段。其中,解析阶段为非必须的。分配内存、符号引用解析成为实际引用
3、初始化:类加载的最后一步是初始化,便是为标记为常量值的字段赋值,执行 < clinit > 方法,会加锁保证类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。
类加载器和双亲委派机制
启动类加载器(C++实现):启动类加载器负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)。
其他类加载器:都是 java.lang.ClassLoader 的子类,这些类加载器需要先由另一个类加载器,比如说启动类加载器,加载至 Java 虚拟机中,方能执行类加载。双亲委派模型。每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。
扩展类加载器(extension class loader)和应用类加载器(application class loader),均由 Java 核心类库提供。
扩展类加载器的父类加载器是启动类加载器。它负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)。
应用类加载器的父类加载器则是扩展类加载器。它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。
Java 9有略微调整。
自定义的类加载器:自己实现的类加载器,可以满足一些自定义需求,比如对代码进行加解密
为啥要有双亲委派机制
保证一个类只被加载一次:因为父类加载了,子类就无需加载了,这样只会被加载一次
保证核心类库的安全性:防止子加载器去篡改核心类库的加载,比如写给自定义加载器把一些工具类给改了
解耦:类似与网络分层,各自做好自己的职责就行
解释执行/即时编译
解释执行:即逐条将字节码翻译成机器码并执行;
即时编译(JIT):即将一个方法中包含的所有字节码编译成机器码后再执行。
前者的优势在于无需等待编译,而后者的优势在于实际运行速度更快。HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。
理论上讲,即时编译后的 Java 程序的执行效率,是可能超过 C++ 程序的。这是因为与静态编译相比,即时编译拥有程序的运行时信息,并且能够根据这个信息做出相应的优化。
JRE、JDK、JVM关系?
JVM:Java虚拟机,是一个可以执行 Java 字节码的虚拟计算机。
JRE:Java 运行时环境,是运行 Java 应用程序所需的最小环境。包括了 Java 虚拟机(JVM)、核心类库和支持文件。
JDK: Java 开发工具包,是用于开发 Java 应用程序的工具集合,包括了 Java 运行时环境(JRE)、Java 编译器(javac)、Java 虚拟机(JVM)和各种工具(如 javadoc、javap 等)。
关系:
JDK 包含了 JRE 和开发工具。
JRE 包含了 JVM 和运行 Java 程序所需的类库。
总结:
如果你需要开发 Java 程序,需要安装 JDK。
如果你只需要运行 Java 程序,安装 JRE 就足够了。
JVM 是 JRE 的核心部分,负责执行 Java 程序。
垃圾回收
内存有限,对运⾏时数据区中的数据进⾏管理和回收。垃圾收集器分为串行、并行、并发。回收机制不同,吞吐量和响应时间不同。比如回收时间,各个阶段采用单线程还是多线程
Serial、Parallel、CMS、G1、ZGC。核⼼的算法有 3 个:标记-清除、标记- 整理、复制
JVM调优
主要也是围绕吞吐量和响应时间,对应响应时间有要求的,那就尽量减少STW时间,反正就是通过一下工具看运行时,YGC和FGC次数,以及每次STW时间,通过不断调整和观测来设置一个合理的值
实际工作中一方面是调整JVM参数,另一方面是修改代码。
参数问题可能有以下一些情况:
1、堆内存设置太小,不满足程序的正常运行,比如一个线程调用一个方法可以会占用1M,内存,并发数为1000,堆内存设置500M的话,一下就满了,这个时候可能会出现频繁垃圾回收或者OOM
2、堆内存新生代和老年代比例不合理,大部分都是临时对象的程序,新生代可以调大。
3、还有一些参数可以指定STW最大时间,这时JVM会根据实际情况调整部分内存大小
4、更换垃圾回收器,参考:https://blog.csdn.net/2301_79437276/article/details/140291840
一次GC流程
堆分为新生代和老年代,新生代分为E区和S区(S0、S1)。
新对象放入E区,大对象也可能直接放入老年代,E区满了会进行Young GC,存活下来的对象放入S区,S区里面存活下来的对象会进行一次负责,在S0和S1之间每复制一次,对象存活的分代年龄+1,当达到15时放入老年代,老年代满了会进行Full GC
为啥是15呢?
因为分代年龄存储在对象头里面,采用4bit存储,最大值只支持15
参考
https://www.cnblogs.com/tomakemyself/p/14147989.html