持续输出面试题之JVM性能调优
什么是JVM?
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。
Java类加载过程
java类加载需要经历一下7个过程:
1.加载
加载是类加载的第一个过程,在这个阶段,将完成一下三件事情:
- 通过一个类的全限定名获取该类的二进制流。
- 将该二进制流中的静态存储结构转化为方法去运行时数据结构。
- 在内存中生成该类的 class对象,作为该类的数据访问入口。
2.验证
验证的目的是为了确保 Class文件的字节流中的信息不可危害到虚拟机。
在该阶段主要完成以下四种验证:
-
文件格式验证
:验证字节流是否符合Clas文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型 -
元数据验证
:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。 -
字节码验证
:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等·符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。
3.准备
准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
//在准备阶段vlue初始值为0,在初始化阶段才会变为123。
public static int value=123;
4.解析
该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。
5.初始化
初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中的定义的java程序代码。
6.使用
7.卸载
深拷贝和浅拷贝
- 浅拷贝(shallowCopy):增加了一个指针指向已存在的内存地址。
- 深拷贝(deepCopy):增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存。
- 浅复制:指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
- 深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
常量池是什么
可以理解为class文件之中的资源仓库,它是class文件结构中与其他项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一,同时它还是class文件中第一个出现表类型的数据项目.
由于常量池的数量是不固定的,所以在常量池入口需要放置一项u2(即2个字节)类型的数据,代表常量池容量计数值(constant-pool-count)(从1开始,将0表示不引用任何常量).
常量池中主要存放两大类常量:
- 字面量(Literal):比较接近于Java语言层面的常量概念,如文本字符串,声明为final的常量值.
- 符号引用(Synbolic Reference):包括如下三类常量:
①类和接口的全限定名(Fully Qualified Name)
②字段的名称和描述符(Descriptor)
③方法的名称和描述符
常量池在class文件的什么位置?
image.pngSystem.gc()和Runtime.gc()会做什么事情?
用来提示JVM要进行垃圾回收。但是,立即开始还是延迟进行垃圾回收取决于JVM。
Java内存分配是怎样的?
- 寄存器:我们无法控制。
- 静态域: static定义的静态成员。
- 常量池:编译时被确定并保存在.class文件中的(final)常量值和和一些文本修饰的符号引用(类和接口的全限定名,字段的名称和描述符,方法和名称和描述符)。
- 非RAM存储:硬盘等永久存储空间。
- 堆内存:new创建的对象和数组,由Java虚拟机自动垃圾回收器管理,存取速度慢。
- 栈内存:基本类型的变量和对象的引用变量(堆内存空间的访问地址),速度快,可以共享,但是大小与生存期必须确定,缺乏灵活性。
新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?
新生代回收器:Serial
、ParNew
、Parallel Scavenge
老年代回收器:Serial Old
、Parallel Old
、CMS
整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
简述分代垃圾回收器是怎么工作的?
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 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)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
说一下 JVM 调优的工具?
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole
和 jvisualvm
这两款视图监控工具。
- jconsole:用于对 JVM 中的内存、线程和类等进行监控;
- jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
Java堆的结构是什么样子的?什么是堆中的永久代(Perm Gen space)?
JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。
堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。
finalize()方法什么时候被调用?析构函数finalization)目的是什么?
垃圾回收器(garbage collector)决定回收某对象时,就会运行该对象的 finalize方法,但是在Java中很不幸,如果内存总是充足的,那么垃圾回收可能永远不会进行,也就是说 finalize可能永远不被执行,显然指望它做收尾工作是靠不住的。它最主要的用途是回收特殊渠道申请的内存。Java程序有垃圾回收器,所以一般情况下内存问题不用程序员操心。但有一种 JNI(Java Native Interface)调用non-java程序(C或C++), finalize()的工作就是回收这部分的内存。
什么是分布式垃圾回收(DGC)?它是如何工作的?
DGC叫做分布式垃圾回收。RMI使用DGC来做自动垃圾回收。因为RMI包含了跨虚拟机的远程对象的引用,垃圾回收是很困难的。DGC使用引用计数算法来给远程对象提供自动内存管理。