文章目录
- 一、 Java 内存区域
- 1.1 说一下 JVM 的主要组成部分及其作用?
- 1.2 说一下 JVM 运行时数据区
- 1.3 详细的介绍下 程序计数器 ?
- 1.4 详细介绍下 java 虚拟机栈?
- 1.5 一个方法调用另一个方法,会创建很多栈帧吗?递归的调用自己会创建很多栈帧吗?
- 1.6 你能给我详细的介绍Java堆吗?
- 1.7 能不能解释一下本地方法栈?
- 1.8 能不能解释一下方法区
- 1.9 什么是JVM字节码执行引擎
- 1.10 你听过直接内存吗?
- 1.11 深拷贝和浅拷贝
- 1.12 说一下堆栈的区别?
- 1.13 队列和栈是什么?有什么区别?
- 1.14 Java会存在 内存泄漏 吗?请说明为什么?
- 二、HotSpot虚拟机
- 2.1 对象的创建
- 2.2 为对象分配内存
- 2.3 处理并发安全问题
- 2.4 对象的访问定位
- 2.4.1 句柄访问
- 2.4.2 直接指针
- 2.5 Java会存在内存泄漏吗?请简单描述
- 三、垃圾收集器
- 3.1 简述Java垃圾回收机制
- 3.2 GC是什么?为什么要GC
- 3.3 垃圾回收的优点和原理,并说出2种回收机制
- 3.4 垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知 虚拟机 进行垃圾回收?
- 3.5 Java 中都有哪些引用类型?
- 3.6 怎么判断对象是否可以被回收?
- 3.7 在Java中,对象什么时候可以被垃圾回收
- 3.8 说一下 JVM 有哪些垃圾回收算法?
- 3.8.1 标记-清除算法
- 3.8.2 复制算法
- 3.8.3 标记-整理算法
- 3.8.4 分代收集算法
- 3.9 说一下 JVM 有哪些垃圾回收器?
- 3.10 详细介绍一下 CMS 垃圾回收器?
- 3.10.1 三色标记算法
- 3.11 新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?
- 3.12 介绍一下10种垃圾回收器
- 3.12.1 Serial收集器
- 3.12.2 Serial Old收集器
- 3.12.3 ParNew收集器
- 3.12.4 Parallel Old
- 3.12.5 Parallel Scavenge
- 3.12.6 CMS
- 3.12.7 G1(Garbage First,物理上不分代,逻辑上分代)
- 3.12.8 ZGC
- 3.12.9 Shenandoah
- 3.12.10 java 11 新的Epsilon垃圾收集器
- 3.13 简述分代垃圾回收器是怎么工作的?
- 3.14 堆内存 逻辑分区 (分代)模型及对象分配逻辑
- 3.14.1 GC相关的概念
- 3.14.2 一个对象的生命之旅(对象分配过程)
- 3.14.3 什么情况下,对象会分配在栈上
- 3.14.4 对象什么时候进入老年代
- 3.14.5 对象的分配总结图
- 3.14.6 JVM参数查看
- 四、虚拟机类加载机制
- 4.1 描述一下JVM加载 class文件 的原理机制
- 4.2 什么是类加载器,类加载器有哪些?
- 4.3 说一下类装载的执行过程?
- 4.4 什么是双亲委派模型?
- 五、JVM调优
- 5.1 说一下 JVM 调优的工具?
- 5.2 Java虚拟机参数类型
- 5.3 常用的 JVM 调优的参数都有哪些?
一、Java内存区域
1.1 说一下 JVM 的主要组成部分及其作用?
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为 Runtime data area(运行时数据区)、Native Interface(本地接口)。
- Class loader(类装载) :根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
- Execution engine(执行引擎) :执行classes中的指令。
- Native Interface(本地接口) :与native libraries交互,是其它 编程语言 交互的接口。
- Runtime data area(运行时数据区域) :这就是我们常说的JVM的内存。
作用 :首先通过 编译器把 Java 代码转换成 字节码 ,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能 。
Java程序运行机制步骤
1>首先利用 IDE 集成开发工具编写Java源代码, 源文件 的后缀为.java;
2>再利用编译器将源代码编译成字节码文件,字节码文件的后缀名为.class;
3>运行字节码的工作是由解释器(java命令)来完成的。
从上图可以看,java文件通过编译器变成了.class文件,接下来类加载器又将这些.class文件加载到JVM中。
其实可以一句话来解释:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang .Class对象,用来封装类在方法区内的数据结构。
1.2 说一下 JVM 运行时数据区
Java 虚拟机 在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖 线程 的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域:
不同虚拟机的运行时数据区可能略微有所不同,但都会遵从 Java 虚拟机规范, Java 虚拟机规范 规定的区域分为以下 5 个部分:
- 1、程序计数器(Program Counter Register)
- 1>作用
- 记录当前线程所执行到的字节码的行号。字节码解释器工作的时候就是通过改变这个 计数器 的值来选取下一条需要执行的字节码指令。
- 2>意义
- JVM的 多线程 是通过线程轮流切换并分配处理器来实现的,对于我们来说的并行事实上一个处理器也只会执行一条线程中的指令。所以, 为了保证各线程指令的安全顺利执行,每条线程都有独立的私有的程序计数器(为什么要线程计数器?因为线程是不具备记忆功能) 。
- 3>存储内容
- 当线程中执行的是一个Java方法时,程序计数器中记录的是正在执行的线程的虚拟机字节码指令的地址。
- 当线程中执行的是一个本地方法时,程序计数器中的值为空。
- 4>可能出现异常
- 此内存区域是唯一一个在JVM上不会发生内存溢出异常( OutOfMemory Error)的区域。
- 2、Java 虚拟机栈(Java Virtual Machine Stacks)
- 1>作用
- 描述Java方法执行的内存模型。 每个方法在执行的同时都会开辟一段内存区域用于存放方法运行时所需的数据,成为栈帧,一个栈帧包含如:局部变量表、操作数栈、动态链接、方法出口等信息 。
- 2>意义
- JVM是基于栈的,所以 每个方法从调用到执行结束,就对应着一个栈帧在虚拟机栈中入栈和出栈的整个过程 。
- 3>存储内容
- 局部变量表(编译期可知的各种基本数据类型、引用类型和指向一条字节码指令的returnAddress类型)、操作数栈、动态链接、方法出口等信息。
- 值得注意的是: 局部变量表所需的内存空间在编译期间完成分配。在方法运行的阶段是不会改变局部变量表的大小的 。
- 4>可能出现的异常
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
- 如果在动态扩展内存的时候无法申请到足够的内存,就会抛出OutOfMemoryError异常。
- 3、本地方法栈(Native Method Stack)
- 1>作用
- 为JVM所调用到的Nativa即本地方法服务。(虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;Native 关键字修饰的方法是看不到的,Native 方法的源码大部分都是 C和C++ 的代码)
- 2>可能出现的异常
- 和虚拟机栈出现的异常很相像。
- 4、Java 堆(Java Heap)
- 1>作用
- 所有线程共享一块内存区域,在虚拟机开启的时候创建。
- 2>意义
- 1、存储对象实例,更好地分配内存。
- 2、垃圾回收(GC)。堆是垃圾收集器管理的主要区域。更好地回收内存。
- 3>存储内容
- 存放对象实例,几乎所有的对象实例都在这里进行分配。堆可以处于物理上不连续的内存空间,只要逻辑上是连续的就可以。
- 值得注意的是:在 JIT编译器 等技术的发展下,所有对象都在堆上进行分配已变得不那么绝对。有些对象实例也可以分配在栈中。
- 4>可能出现的异常
- 实现堆可以是固定大小的,也可以通过设置配置文件设置该为可扩展的。 如果堆上没有内存进行分配,并无法进行扩展时,将会抛出OutOfMemoryError异常。
- 5、方法区(Methed Area)
- 1>作用
- 用于存储运行时常量池、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 2>意义
- 对运行时常量池、常量、静态变量等数据做出了规定。
- 3>存储内容
- 运行时常量池(具有动态性)、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 4>可能出现的异常
- 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
1.3 详细的介绍下程序计数器?
- 程序计数器是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(行号)
- 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。
例子 :在java中最小的执行单位是线程,线程是要执行指令的,执行的指令最终操作的就是我们的电脑,就是 CPU。在CPU上面去运行,有个非常不稳定的因素,叫做调度策略,这个调度策略是时基于时间片的,也就是当前的这一 纳秒 是分配给那个指令的。
假如线程A在执行任务a,线程B抢夺线程A的时间片,就会打断了线程A,线程A就会挂起,当线程B结束任务后,线程A要重新执行任务a,就需要程序计数器来帮其恢复被打断时的状态。
1.4 详细介绍下Java虚拟机栈?
- Java虚拟机是线程私有的,它的 生命周期 和线程相同。
- 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量,操作数栈,动态链接,出口等。
栈帧 (Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用开始到执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
每一个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
1.5 一个方法调用另一个方法,会创建很多栈帧吗?递归的调用自己会创建很多栈帧吗?
会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面。
递归的话也会创建多个栈帧,就是在栈中一直从上往下排下去。
1.6 你能给我详细的介绍Java堆吗?
- java堆(Java Heap)是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
- 在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
- java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。
- 从内存回收角度来看java堆可分为:新生代和老生代。
- 从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
- 无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。
- 根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
1.7 能不能解释一下本地方法栈?
- 本地方法栈很好理解,他很栈很像,只不过方法上带了 native 关键字的栈字。
- native关键字的方法是看不到的,必须要去 oracle 官网去下载才可以看的到,而且native关键字修饰的大部分源码都是C和C++的代码。
- 同理可得,本地方法栈中就是C和C++的代码。
1.8 能不能解释一下方法区
- 方法区是所有线程共享的内存区域,它用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 它有个别命叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。
1.9 什么是JVM字节码执行引擎
- 虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。
- “虚拟机”是一个相对于“物理机”的概念,虚拟机的字节码是不能直接在物理机上运行的,需要JVM字节码执行引擎- 编译成机器码后才可在物理机上执行。
1.10 你听过直接内存吗?
- 直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现。
- 我的理解就是直接内存是基于物理内存和Java虚拟机内存的中间内存。
1.11 深拷贝和浅拷贝
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
浅复制 :仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深复制 :在计算机中开辟一块新的内存地址用于存放复制的对象。
1.12 说一下堆栈的区别?
- 1、物理地址
- 堆的物理地址分配对对象是不连续的,因此 性能慢些 。
- 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的, 所以性能快 。
- 2、内存分别
- 堆因为是不连续的,所以 分配的内存是在运行期确认的,因此大小不固定 , 一般堆大小远远大于栈 。
- 栈是连续的,所以 分配的内存大小要在编译期就确认,大小是固定的 。
- 3、存放的内容
- 堆存放的是对象的实例和数组。因此该区更关注的是数据的存储。
- 栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。
- PS:静态变量放在方法区,静态的对象还是放在堆。
- 4、程序的可见度
- 堆对于整个应用程序都是共享、可见的 。
- 栈只对于线程是可见的。所以也是线程私有,他的生命周期和线程相同 。
1.13 队列和栈是什么?有什么区别?
队列和栈都是被用来预存储数据的。
操作的名称不同。队列的插入称为入队,队列的删除称为出队。栈的插入称为进栈,栈的删除称为出栈。
可操作的方式不同。队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。
操作的方法不同。队列是先进先出(FIFO),即队列的修改是依先进先出的原则进行的。新来的成员总是加入队尾(不能从中间插入),每次离开的成员总是队列头上(不允许中途离队)。而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中最新的元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除。
1.14 Java会存在内存泄漏吗?请说明为什么?
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。
但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
二、HotSpot虚拟机
2.1 对象的创建
Java 中提供的几种对象创建方式:
方式 |
解释 |
使用new关键字 |
调用了构造函数 |
使用Class的newInstance方法 |
调用了构造函数 |
使用 Constructor 类的newInstance方法 |
调用了构造函数 |
使用clone方法 |
没有调用构造函数 |
使用反序列化 |
没有调用构造函数 |
下面是对象创建的主要流程:
虚拟机遇到一条new指令时,先检查常量池是否已经加载相应的类,如果没有,必须先执行相应的类加载。类加载通过后,接下来分配内存。若Java堆中内存是绝对规整的,使用“指针碰撞“方式分配内存;如果不是规整的,就从空闲列表中分配,叫做”空闲列表“方式。划分内存时还需要考虑一个问题-并发,也有两种方式: CAS同步处理,或者本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。然后内存空间初始化操作,接着是做一些必要的对象设置(元信息、哈希码…),最后执行方法。
2.2 为对象分配内存
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java堆是否规整,有两种方式:
- 指针碰撞:如果Java堆的内存是规整,即所有用过的内存放在一边,而空闲的的放在另一边。分配内存时将位于中间的指针指示器向空闲的内存移动一段与对象大小相等的距离,这样便完成分配内存工作。
- 空闲列表:如果Java堆的内存不是规整的,则需要由虚拟机维护一个列表来记录那些内存是可用的,这样在分配的时候可以从列表中查询到足够大的内存分配给对象,并在分配后更新列表记录。
- 选择哪种分配方式是由 Java 堆是否规整来决定的,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
2.3 处理并发安全问题
对象的创建在虚拟机中是一个非常频繁的行为,哪怕只是修改一个指针所指向的位置,在并发情况下也是不安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决这个问题有两种方案:
- 对分配内存空间的动作进行同步处理(采用 CAS + 失败重试来保障更新操作的原子性);
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。通过-XX:+/-UserTLAB参数来设定虚拟机是否使用TLAB。
2.4 对象的访问定位
Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。对象的访问方式取决于 JVM 虚拟机的实现。目前主流的访问方式有 句柄 和 直接指针 两种方式。
2.4.1 句柄 访问
Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造如下图所示:
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
2.4.2 直接指针
如果使用直接指针访问,引用 中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。
优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。HotSpot 中采用的就是这种方式。
2.5 Java会存在内存泄漏吗?请简单描述
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中。理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。
但是,即使这样,Java也还是存在着内存泄漏的情况,java导致内存泄露的原因很明确:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
三、垃圾收集器
3.1 简述Java垃圾回收机制
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
3.2 GC是什么?为什么要GC
GC 是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。
3.3 垃圾回收的优点和原理,并说出2种回收机制
java语言最显著的特点就是引入了垃圾回收机制,它使java程序员在编写程序时不再考虑 内存管理 的问题。
由于有这个垃圾回收机制,java中的对象不再有“作用域”的概念,只有引用的对象才有“作用域”。
垃圾回收机制有效的防止了内存泄露,可以有效的使用可使用的内存。
垃圾回收器通常作为一个单独的低级别的线程运行,在不可预知的情况下对内存堆中已经死亡的或很长时间没有用过的对象进行清除和回收。
程序员不能实时的对某个对象或所有对象调用垃圾回收器进行垃圾回收。
垃圾回收有分代复制垃圾回收、标记垃圾回收、增量垃圾回收。
3.4 垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。
通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当GC确定一些对象为”不可达”时,GC就有责任回收这些内存空间。
可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。
3.5 Java 中都有哪些引用类型?
- 强引用:发生 gc 的时候不会被回收。
- 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
- 弱引用:有用但不是必须的对象,在下一次GC时会被回收。
- 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。
3.6 怎么判断对象是否可以被回收?
垃圾收集器在做垃圾回收的时候,首先需要判定的就是哪些内存是需要被回收的,哪些对象是「存活」的,是不可以被回收的;哪些对象已经「死掉」了,需要被回收。
一般有两种方法来判断:
1> 引用计数器法 :为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;同时,这种方式一方面无法区分软、虛、弱、强引用类别。
2> 可达性分析算法 :这个算法的基本思路是通过一系列的称为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即从GC Roots到这个对象不可达)时,则证明该对象是不可用的。
Java中,可作为GC Roots的对象包括下面几种:
1>虚拟机栈(栈帧中的本地变量表)中引用的对象;
2>方法区中静态属性引用的对象;
3>方法区中常量引用的对象;
4>本地方法栈中的JNI(即Native方法)引用的对象。
3.7 在Java中,对象什么时候可以被垃圾回收
当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。
3.8 说一下 JVM 有哪些垃圾回收算法?
- 标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
- 复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
- 标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
- 分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。
3.8.1 标记-清除算法
标记无用对象,然后进行清除回收。
标记-清除算法(Mark-Sweep)是一种常见的基础垃圾收集算法,它将垃圾收集分为两个阶段:
1> 标记阶段 :标记出可以回收的对象。
2> 清除阶段 :回收被标记的对象所占用的空间。
标记-清除算法之所以是基础的,是因为后面讲到的垃圾收集算法都是在此算法的基础上进行改进的。
优点:实现简单,不需要对象进行移动。
缺点: 标记、清除过程效率低,产生大量不连续的内存碎片 ,提高了垃圾回收的频率。
标记-清除算法的执行的过程如下图所示:
3.8.2 复制算法
为了解决标记-清除算法的效率不高的问题,产生了复制算法。它把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾收集时,遍历当前使用的区域,把存活对象复制到另外一个区域中,最后将当前使用的区域的可回收的对象进行回收。
优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
缺点: 可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制 。
复制算法的执行过程如下图所示:
3.8.3 标记-整理算法
在新生代中可以使用复制算法,但是在老年代就不能选择复制算法了,因为老年代的对象存活率会较高,这样会有较多的复制操作,导致效率变低。标记-清除算法可以应用在老年代中,但是它效率不高,在内存回收后容易产生大量内存碎片。因此就出现了一种标记-整理算法(Mark-Compact)算法,与标记-整理算法不同的是, 在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑的排列在一起,然后对端边界以外的内存进行回收 。回收后,已用和未用的内存都各自一边。
优点:解决了标记-清理算法存在的内存碎片问题。
缺点: 仍需要进行局部对象移动,一定程度上降低了效率 。
标记-整理算法的执行过程如下图所示:
3.8.4 分代收集算法
当前商业虚拟机都采用分代收集的垃圾收集算法。分代收集算法,顾名思义是根据对象的存活周期将内存划分为几块。一般包括年轻代、老年代 和 永久代,如图所示:
3.9 说一下 JVM 有哪些垃圾回收器?
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。
- Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
- ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
- Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
- Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
- Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
- G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。
3.10 详细介绍一下 CMS 垃圾回收器?
CMS 是英文 Concurrent Mark-Sweep 的简称,是以牺牲吞吐量为代价来获得最短回收停顿时间的垃圾回收器。对于要求服务器响应速度的应用上,这种垃圾回收器非常适合。在启动 JVM 的参数加上“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器。
CMS 使用的是标记-清除的算法实现的,所以在 gc 的时候回产生大量的内存碎片,当剩余内存不能满足程序运行要求时,系统将会出现 Concurrent Mode Failure,临时 CMS 会采用 Serial Old 回收器进行垃圾清除,此时的性能将会被降低。
3.10.1 三色标记算法
将对象分成三种类型:
1、黑色:跟对象,或该对象与它的子对象都被扫描过(对象被标记了,且它的所有field也被标记完了)
2、灰色:对象本身被扫描,但还没扫描完该对象中的子对象(它的field还没有被标记或标记完)
3、白色,未被扫描对象,扫描完所有对象之后,最终为白色的为不可达对象,即垃圾对象(对象没有被标记到)
三色标记会产生两种问题:错标和漏标:
- 1、错标
- 在该情况中,D不是垃圾,但也会被清除掉。
- CMS的解决方案是Incremental Update,对应到上面的例子中,就是把A重新标记成灰色。
- 2、漏标
- Incremental Update会产生漏标,如下图所示,解决方法是重新标记一遍。
3.11 新生代垃圾回收器和老年代垃圾回收器都有哪些?有什么区别?
新生代回收器:Serial、ParNew、Parallel Scavenge
老年代回收器:Serial Old、Parallel Old、CMS
整堆回收器:G1
新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。
3.12 介绍一下10种垃圾回收器
先看图:
3.12.1 Serial收集器
Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器;
JDK1.3.1前是HotSpot新生代收集的唯一选择;
Serial收集器到JDK1.7为止,它依然是JAVA虚拟机运行在Client模式下的默认新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。所以,Serial收集器对于运行在来说是一个很好的选择。
Serial收集器 运行示意图如下:
stw : stop-the-word 停所有工作线程;
该回收器使用复制算法,,随着内存越来越大,执行效率太慢了,单线程就没法使用了 后面就有了多线程的版本- ParNew收集器。
3.12.2 Serial Old收集器
Serial Old 和Serial 相比就是应用在老年代的垃圾收集器,也是单线程,但是算法不是copy,而是Mark-Compact 标记整理算法,也是stw:暂停所有线程进行垃圾回收;
所以 Serial 和Serial Old 组合使用,可用内存一般不大(几十M至一两百M)的服务器环境中,不适合当前的大内存了 。
3.12.3 ParNew收集器
ParNew垃圾收集器是Serial收集器的改进多线程版本(因为内存的不断增大),除了多线程外,其余的行为、特点和Serial收集器一样,实现算法跟Serial完全一样(copy算法),也是stw下执行;
但是如果CPU数量为1个或者少于4个时,该种收集器的性能并不会比Serial要好。因为除去上下文切换,以及占用用户线程CPU时间片,导致用户线程被拖慢
在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;
1 ) CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;
2) 因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码;
设置参数:
“-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代收集器;
“-XX:+UseParNewGC”:强制指定使用ParNew;
“-XX:ParallelGCThreads”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量。
3.12.4 Parallel Old
JDK1.8默认用的是Parallel Scavenge(新生代)+ Parallel Old(老年代) 。
这个是Serial Old的多线程版本,应用在老年代的收集器,也是标记-整理算法,并且是stw的执行收集。但是如果CPU数量少的话性能一样不好。但是现在无论是PC还是server CPU数量都不再是性能瓶颈限制了,所以目前它跟Parallel Scavenge的配合是吞吐量优先场景的优先收集器选择。
3.12.5 Parallel Scavenge
一种新生代垃圾收集器,与 ParNew相比不可以与cms一起组合使用,PS也是复制算法,它与前两种收集器最大的区别是,它关注的是吞吐量而不是延迟。也被称为是吞吐量优先的收集器。其中,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
主要使用场景:主要适合在后台运算而不是太多交互的任务,高吞吐量则可以最高效率的利用CPU时间,尽快的完成程序的运算任务。当然,如果想要降低停顿时间,相应的也会影响吞吐量。
3.12.6 CMS
CMS,Concurrent Mark Sweep,这是第一款真正的并发收集器,就是在线程执行过程中也可以进行垃圾收集的收集器,在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景。 分为四个过程 :
1、初始标记 ,标记跟对象
2、并发标记,最耗时间的阶段,寻找与标记要回收的对象。并发标记一般会产生一些问题,如:1> 漏标 ,某些未标记的对象在后续运行过程中变成了垃圾对象,这种会产生 浮动垃圾 ;2> 错标 ,某些标记的对象,后来又有引用了。此处采用的是三色标记算法。
3、重新标记,用于处理错标的遗憾。
4、并发清理。
CMS执行流程图:
CMS会产生碎片化的内存空间。CMS采用了多种方式尽可能降低GC的暂停时间,减少用户程序停顿。停顿时间降低的同时牺牲了CPU吞吐量 。因为并发情况占用大量cpu资源,这是在停顿时间和性能间做出的取舍,可以简单理解为”空间(性能)”换时间,CMS 是一个承前启后的收集器,下面是不分代收集器。
3.12.7 G1(Garbage First,物理上不分代,逻辑上分代)
在JDK7就已加入JVM的收集器大家庭中,成为HotSpot重点发展的垃圾回收技术。同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。G1最大的特点是引入分区的思路,弱化了分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。
G1收集器,是比前面的更优秀,真正有突破的一款垃圾收集器。其实在G1中还是保留了分代的概念,但是实际上已经在新生代和老年代中没有物理隔离了。在G1中,内存空间被分割成一个个的Region区,所谓新生代和老年代,都是由一个个region组成的。同时G1也不需要跟别的收集器一起配合使用,自己就可以搞定所有内存区域。整体上来讲不是一个分代收集器,是一个通吃收集器。这也是JVM内存管理和垃圾收集的一个发展趋势。从后面zgc中我们可以更清晰的看到这个变化。
G1采用了标记-整理算法,避免了CMS中的内存碎片问题,另外它能达到可控的垃圾时间。是一款优秀的收集器。即便如此,从2004年第一篇论文发表到真正商用推出,也是到了jdk1.7。实现上并不是那么容易的。
G1的工作过程:
1、初始标记 :这个过程跟CMS第一个过程差不多,只是标记一下GC Root关联的对象。
2、并发标记 :这个过程时间比较久,分析GC Root到所有对象的可达性分析。如果从GC Root节点开始遍历所有对象会比较耗时,实际上JVM也不是这么做的。JVM是使用Remembered Set保存了对象引用的调用信息,在可达性分析的时候只需要同时遍历remembered set就好了,不需要从根节点开始挨个遍历。
3、最终标记 :由于并发标记阶段,用户线程仍然在工作,会对标记产生一些偏差,这时候需要通过remembered set log来记录这些改变,在这个阶段将改变合并到remembered set中。完成最终标记。
4、筛选清除 :通过标记整理的算法,根据用户配置的回收时间,和维护的优先级列表,优先收集价值最大的region。收集阶段是基于标记-整理和复制算法实现 。
3.12.8 ZGC
zgc是jdk11中要发布的最新垃圾收集器。完全没有分代的概念,先说下它的优点吧,官方给出的是无碎片,时间可控,超大堆。 Z Garbage Collector,即ZGC,是一个可伸缩的、低延迟的垃圾收集器,主要为了满足如下目标进行设计:
- 停顿时间不会超过10ms
- 停顿时间不会随着堆的增大而增大(不管多大的堆都能保持在10ms以下)
- 可支持几百M,甚至几T的堆大小(最大支持4T)
ZGC为什么可以这么优秀,主要是因为以下几个特性:
1、Concurrent
ZGC只有短暂的STW,大部分的过程都是和应用线程并发执行,比如最耗时的并发标记和并发移动过程。
2、Region-based
ZGC中没有新生代和老年代的概念,只有一块一块的内存区域page,以page单位进行对象的分配和回收。
3、Compacting
每次进行GC时,都会对page进行压缩操作,所以完全避免了CMS算法中的碎片化问题。
4、NUMA-aware
现在多CPU插槽的服务器都是Numa架构,比如两颗CPU插槽(24核),64G内存的服务器,那其中一颗CPU上的12个核,访问从属于它的32G本地内存,要比访问另外32G远端内存要快得多。
ZGC默认支持NUMA架构,在创建对象时,根据当前线程在哪个CPU执行,优先在靠近这个CPU的内存进行分配,这样可以显著的提高性能,在SPEC JBB 2005 基准测试里获得40%的提升。
5、Using colored pointers
和以往的标记算法比较不同,CMS和G1会在对象的对象头进行标记,而ZGC是标记对象的指针。
其中低42位对象的地址,42-45位用来做指标标记。
6、Using load barriers
因为在标记和移动过程中,GC线程和应用线程是并发执行的,所以存在这种情况:对象A内部的引用所指的对象B在标记或者移动状态,为了保证应用线程拿到的B对象是对的,那么在读取B的指针时会经过一个 “load barriers” 读屏障,这个屏障可以保证在执行GC时,数据读取的正确性。
3.12.9 Shenandoah
Shenandoah是一款concurrent及parallel的垃圾收集器;跟ZGC一样也是面向low-pause-time的垃圾收集器,不过ZGC是基于colored pointers来实现,而Shenandoah GC是基于brooks pointers来实现。
其实低停顿的GC,业界早就出现,只不过Java比较晚。
Azul的Zing中C4 GC 土豪选择,oracle中的HotSpot ZGC JDK11的选择。
Epsilon
3.12.10 java 11 新的Epsilon垃圾收集器
Epsilon(A No-Op Garbage Collector)垃圾回收器控制内存分配,但是不执行任何垃圾回收工作。一旦java的堆被耗尽,jvm就直接关闭。设计的目的是提供一个完全消极的GC实现,分配有限的内存分配,最大限度降低消费内存占用量和内存吞吐时的延迟时间。一个好的实现是隔离代码变化,不影响其他GC,最小限度的改变其他的JVM代码。
各个收集器的组合流程图:
常用的垃圾回收器组合有三种:Serial和Serial Old、CMS和ParNew、Parallel Scavenge和Parallel Old。
Serial使用较少,因为随着内存越来越大,执行效率太慢。
3.13 简述分代垃圾回收器是怎么工作的?
分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
1>把 Eden + From Survivor 存活的对象放入 To Survivor 区;
1>清空 Eden 和 From Survivor 分区;
1>From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。
3.14 堆内存逻辑分区(分代)模型及对象分配逻辑
堆内存主要分为两大块: 新生代和老年代:
- 1、新生代/年轻代 new/young
- 新生代又分为三块:1.Eden;2.Survivor-S1;3.Survivor-S2。
- S1和S2一般一起聊,也有人把他俩叫做S0和S1,或者from和to,都一个意思,就是两块survivor区。
- 2、老年代 Old
参考一下分代的简单图示,现在JDK1.8的NewRatio=2,即老年代/新生代=2:
3.14.1 GC相关的概念
- MinorGC/YGC,新生代空间耗尽时触发
- MajorGC/FullGC/FGC,老年代无法继续分配空间时触发,新生代和老年代同时进行回收,比较慢,重量级。
- 一般垃圾回收的过程:
- 1、YGC回收之后,Eden中大多数的对象会被回收,活着的进入s0
- 2、再次YGC,活着的对象eden + s0 -> s1
- 3、再次YGC,eden + s1 -> s0
- 4、年龄足够 -> 老年代 (15 CMS 6)
- 5、s区装不下 -> 老年代
- 6、老年代满了FGC Full GC
- JVM调优指什么?GC Tuning (Generation)
- 尽量减少FGC
- 减少STW时间
- MinorGC = YGC
- MajorGC = FGC
3.14.2 一个对象的生命之旅(对象分配过程)
一个对象产生时,首先尝试在栈上分配,如果符合条件 分配在栈了,当方法结束时栈弹出,对象就终结了;
如果没在栈上分配,就判断对象,如果特别大直接进入Old区,否则的话就分配至Eden区(TLAB也属于Eden区);
如果进入Eden区:
经过一次GC后.Eden区中的存活对象进入S1;
每次GC,会把S1的存活对象扔进S2,S2的存活对象扔进S1,每换个区对象的年龄+1;
多次垃圾回收后,对象的年龄到了,就进入Old区.
3.14.3 什么情况下,对象会分配在栈上
直接分配在Eden区的话,会存在多线程的竞争,效率较低。为了提高效率,减少多线程的竞争,会优先考虑分配在栈上和TLAB上。
- 1、栈上分配
- 线程私有小对象
- 没有逃逸,只在某段代码(比如在某个方法内部)中使用
- 支持标量替换(简单地说,就是用标量替换聚合量。这样做的好处是如果创建的对象并未用到其中的全部变量,则可以节省一定的内存。标量是指不可分割的量,如java中基本数据类型和reference类型,相对的一个数据可以继续分解,称为聚合量;如果把一个对象拆散,将其成员变量恢复到基本类型来访问就叫做标量替换),这个对象可以用几个简单的变量替换
无需调整
多线程没有竞争
方法结束,栈弹出,对象直接拜拜了,不用GC回收
- 2、线程本地分配TLAB(Thread Local Allocation Buffer)
- 如果栈空间不够了,会优先分配在TLAB
- 占用Eden,默认是Eden的1%
- 小对象
无需调整
多线程没有竞争,或者竞争很少
3.14.4 对象什么时候进入老年代
其实都跟年龄有关:
- 1、age超过-XX:MaxTenuringThreshold指定次数(TGC)
- 对象头,markword里面,GC age标识位占用4位,所以对象的年龄最大为15
- Parallel Scavenge 阈值 15
- CMS 6
- G1 15
- 2、动态年龄(不重要)
- 假设有次的YGC是Eden&S1->S2,如果S2中的存活对象超过了S2空间的一半,就把S2中年龄最大的对象放入老年代
- 3、分配担保(不重要)
- YGC期间 survivor区空间不够了 空间担保直接进入老年代
3.14.5 对象的分配总结图
3.14.6 JVM参数查看
JVM的参数分为三种:
- 标准参数,-开头的参数, 所有版本JDK都支持,直接输入java 查看
- 非标准参数, -X开头的参数,输入java -X 查看
- 不稳定参数 -XX:(+/-), 每个版本可能不同, java -XX:+PrintFlagsFinal 查看,特别多,几百个
四、虚拟机类加载机制
4.1 描述一下JVM加载Class文件的原理机制
Java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
类装载方式,有两种 :
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
2.显式装载, 通过class.forname()等方法,显式加载需要的类
Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销。
4.2 什么是类加载器,类加载器有哪些?
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
主要有一下四种类加载器:
启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
4.3 说一下类装载的执行过程?
类装载分为以下 5 个步骤:
加载:根据查找路径找到相应的 class 文件然后导入;
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址;
初始化:对静态变量和静态代码块执行初始化工作。
4.4 什么是双亲委派模型?
在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
五、JVM调优
5.1 说一下 JVM 调优的工具?
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。
jconsole:用于对 JVM 中的内存、线程和类等进行监控;
jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。
5.2 Java虚拟机参数类型
有三种,如图:
5.3 常用的 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 详细信息。