JVM手册(基础)
- JVM手册(基础)
- 描述一下JVM内存模型以及分区,需要详细介绍每隔内存区域存放什么?
- Java内存模型(JMM)
- 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
- 运行时常量池
- 直接内存
- java对象的创建过程
- 对象的创建方法,对象的内存分配,对象的访问定位
- 对象访问定位的两种方式
- 对象在内存中的布局
- 类的初始化时机
- 什么是类加载器,常见的类加载器有哪些?
- 双亲委派模型,问什么需要双亲委派模型,有什么优点?
- 如何打破双亲委派模型?
- 对象如何分配内存
- Full GC的触发条件
- GC的两种判定方法,以及各有什么特点
- 强引用、软引用、弱引用、虚引用以及他们之间和gc的关系
- 不可达的对象并非“非死不可”
- 能够找到 Reference Chain 的对象,就一定会存活么?
- 请问如何查看 JVM 系统默认值
- 谈谈对 OOM 的认识?如何排查 OOM 的问题?
- 那些内存区域会发生OOM以及会进行GC?
- 谈谈 JVM 中的常量池?
- 栈帧都有哪些数据?
- 什么情况会造成元空间溢出?
- 什么时候会造成堆外内存溢出?
- HashMap 中的 key,可以是普通对象么?有什么需要注意的地方?
- 怎么看死锁的线程?
- 什么是方法内联?
- 对象是怎么从年轻代进入老年代的?
- 垃圾回收算法(重点)
- 你知道都有哪些垃圾回收器,各有什么特点
- 对象什么时候会被GC
- 说说Java中栈内存和堆内存的区别
- java对象创建(5种创建对象的方法)
- 如果对象的引用被置为 null,垃圾收集器是否会立即回收对象?
- 你知道哪些JVM调优参数
- 为什么字符串常量池在不同版本的jdk中位置会发生变化
- 概述一下类结构文件
- 谈谈你对jvm的理解
- JVM 配置常用参数有哪些?
- 虚拟机栈和本地方法栈为什么是线程私有的?
- 说一下 JVM 调优的命令?
- 说说类加载的五个过程
- 那些GCroots可以回收
- 简述 Java 垃圾回收机制。
- 垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
- GC收集器有哪些,CMS收集器和G1收集器的特点
- Minor GC,Major GC 和Full GC分别发生在什么时候,各有什么特点?
- 说说常用的内存调试工具?
- 简单介绍一下什么是类加载机制
- 描述一下 JVM 加载 Class 文件的原理机制?
- 双亲委派模型,问什么需要双亲委派模型,有什么优点?
- Java内存分配
- 分派:静态分派和动态分派
- 那些内存区域会发生OOM以及会进行GC?
- 如何打破双亲委派模型?
- 新生代中区分Eden和Survivor的作用是什么
- 简述分代垃圾回收器工作流程
- GC是什么? 为什么要有 GC?
- 简述CMS收集器
- 简述G1收集器
- 简述G1和CMS的对比
- 垃圾回收算法(重点)
- 什么是分代回收算法,为什么要进行分代回收
- 你知道都有哪些垃圾回收器,各有什么特点
- 对象什么时候会被GC
- 对象什么时候会放入老年代
- 引起类加载操作有哪些?
- 说说Java中栈内存和堆内存的区别
- 强引用、软引用、弱引用、虚引用以及他们之间和gc的关系
- java对象创建(5种创建对象的方法)
- jvm调优工具又哪些?各自的作用又是什么(重点)
- 你知道哪些JVM调优参数
- 为什么字符串常量池在不同版本的jdk中位置会发生变化
- 概述一下类结构文件
- jvm工具
- JVM 配置常用参数有哪些?
- 虚拟机栈和本地方法栈为什么是线程私有的?
- java对象的创建过程
- 对象访问定位的两种方式
- finalize() 方法什么时候被调用?析构函数 (finalization) 的目的是什么?
- 简述 Java 内存分配与回收策率以及 Minor GC 和 Major GC
- 思维导图
JVM手册(基础)
描述一下JVM内存模型以及分区,需要详细介绍每隔内存区域存放什么?
运行时数据区在jdk7和jdk8中有些不同
jdk8之前
jdk8
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 直接内存 (非运行时数据区的一部分)
java内存模型整体上可以划分为三部分:类加载子系统,运行时数据区和执行引擎子系统。
程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
从上面的介绍中我们知道程序计数器主要有两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。程序计数器也不会出现GC
java虚拟机栈
与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。
Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分。 (实际上,Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。)
局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
- StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 异常。
- OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出 OutOfMemoryError 异常。
Java 虚拟机栈也是线程私有的,每个线程都有各自的 Java 虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
扩展:那么方法/函数如何调用?
Java 栈可用类比数据结构中栈,Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。
Java 方法有两种返回方式:
- return 语句。
- 抛出异常。
不管哪种返回方式都会导致栈帧被弹出。
本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
堆
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
上图所示的 eden 区、s0 区、s1 区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold
来设置。
方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。
常用参数
JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小
java -XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。
JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
下面是一些常用参数:
java -XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到 java.lang.OutOfMemoryError。你可以使用 -XX:MaxMetaspaceSize
标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize
调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 异常出现。
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
执行引擎:分为解释器和jit即使编译器,解释器主要作用是对字节码的解释执行,而jit及时编译器主要是对一些热点代码进行二次编译,直接编译我机器可以理解的机器语言,这样下次执行是效率更高。
Java内存模型(JMM)
JVM 试图定义一种统一的内存模型,能将各种底层硬件以及操作系统的内存访问差异进行封装,使 Java 程序在不同硬件以及操作系统上都能达到相同的并发效果。它分为工作内存和主内存,线程无法对主存储器直接进行操作,如果一个线程要和另外一个线程通信,那么只能通过主存进行交换,如下图所示。
工作内存:寄存器,高速缓存
主内存:硬件的内存
内存间操作:
Java 内存模型描述的是多线程对共享内存修改后彼此之间的可见性。
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到 java.lang.OutOfMemoryError。你可以使用 -XX:MaxMetaspaceSize
标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize
调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
方法区的一部分,存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到运行时常量池中;字符串常量池1.7以后放在堆中,运行时常量池放在元空间中
直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 异常出现。
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
执行引擎:分为解释器和jit即使编译器,解释器主要作用是对字节码的解释执行,而jit及时编译器主要是对一些热点代码进行二次编译,直接编译我机器可以理解的机器语言,这样下次执行是效率更高。
java对象的创建过程
下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。
Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
Step2:分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式:(补充内容,需要掌握)
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
内存分配并发问题(补充内容,需要掌握)
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
- CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
Step5:执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
对象的创建方法,对象的内存分配,对象的访问定位
对象创建过程图
对象的创建详细过程
Java对象的创建大致上有以下几个步骤:
- 类加载检查:检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类的加载过程
- 为对象分配内存:对象所需内存的大小在类加载完成后便完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。由于堆被线程共享,因此此过程需要进行同步处理(分配在TLAB上不需要同步)
- 内存空间初始化:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
- 对象设置:JVM对对象头进行必要的设置,保存一些对象的信息(指明是哪个类的实例,哈希码,GC年龄等)
- init:执行完上面的4个步骤后,对JVM来说对象已经创建完毕了,但对于Java程序来说,我们还需要对对象进行一些必要的初始化。
在这里说明一下TLAB是什么意思:
因为堆是线程之间共享的,如果在并发场景中,两个线程先后把对象的引用指向了同一个内存区域,怎么办?
为了解决这个并发问题,对象的内存分配过程就必须进行同步控制,但是,无论使用哪种方案(有可能是CAS),都会影响内存的分配效率。然而对于 Java 来说对象的分配是高频操作。
由此 HotSpot 虚拟机采用了这个方案:每个线程在 Java 堆中预先分配一小块内存,然后在给对象分配内存的时候,直接在自己的这块”私有“内存中进行分配,当这部分用完之后,再分配新的”私有“内存。
这种方案被称之为 TLAB 分配。这部分 buffer 是从堆中划分出来的,但是本地线程独享的。
TLAB 是虚拟机在内存的 eden 区划分出来的一块专用空间,是线程专属的。在启用 TLAB 的情况下,当线程被创建时,虚拟机会为每个线程分配一块 TLAB 空间,只给当前线程使用,这样每个线程都单独拥有一个空间,如果需要分配内存,就在自己的空间上分配,这样就不存在竞争的情况,可以大大提高分配效率。
所以说,因为有了 TLAB 技术,堆内存并不是完完全全的线程共享,其中 eden 区中还是有一部分空间是分配给线程独享的。
注意:这里 TLAB 的线程独享是针对于分配动作,至于读取、垃圾回收等工作是线程共享的,而且在使用上也没什么区别。
也就是说,虽然每个线程在初始化时都会去堆内存中申请一块 TLAB,并不是说这个 TLAB 区域的内存其他线程就完全无法访问了,其他线程的读取还是可以的,只不过无法在这个区域中分配内存而已。
并且,在 TLAB 分配之后,并不影响对象的移动和回收,也就是说,虽然对象刚开始可能通过 TLAB 分配内存,存放在 Eden 区,但是还是会被垃圾回收或者被移到 S 区和老年代等。
还有一点需要注意的是,我们说 TLAB 是在 eden 区分配的,因为 eden 区域本身就不太大,而且 TLAB 空间的内存也非常小,默认情况下仅占有整个 eden 空间的 1%。所以,必然存在一些大对象是无法在 TLAB 直接分配。遇到 TLAB 中无法分配的大对象,对象还是可能在 eden 区或者老年代等进行分配的,但是这种分配就需要进行同步控制,这也是为什么我们经常说:小的对象比大的对象分配起来更加高效。
对象的内存分配
Java对象的内存分配有两种情况,由Java堆是否规整来决定(Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定):
- 指针碰撞(Bump the pointer):如果Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离
- 空闲列表(Free List):如果Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
对象的访问定位
对象的访问形式取决于虚拟机的实现,目前主流的访问方式有使用句柄和直接指针两种:
- 使用句柄:
如果使用句柄访问,Java堆中将会划分出一块内存来作为句柄池,引用中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息:
优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。
- 直接指针:
如果使用直接指针访问对象,那么对象的实例数据中就包含一个指向对象类型数据的指针,引用中存的直接就是对象的地址:
优势:速度更快,节省了一次指针定位的时间开销,积少成多的效应非常可观。
缺点:如果发生GC行为对象的内存地址发生变化,那么需要更新引用的地址。
对象访问定位的两种方式
java对象在访问的时候,我们需要通过java虚拟机栈的reference类型的数据去操作具体的对象。由于reference类型在java虚拟机规范中只规定了一个对象的引用,并没有定义这个这个引用应该通过那种方式去定位、访问java堆中的具体对象实例,所以一般的访问方式也是取决与java虚拟机的类型。目前主流的访问方式有通过句柄和直接指针两种方式。
- 句柄访问
使用句柄访问方式,java堆将会划分出来一部分内存去来作为句柄池,reference中存储的就是对象的句柄地址。而句柄中则包含对象实例数据的地址和对象类型数据(如对象的类型,实现的接口、方法、父类、field等)的具体地址信息。下边我以一个例子来简单的说明一下:
Object obj = new Object();
Object obj表示一个本地引用,存储在java栈的本地便变量表中,表示一个reference类型的数据。
new Object()作为实例对象存放在java堆中,同时java堆中还存储了Object类的信息(对象类型、实现接口、方法等)的具体地址信息,这些地址信息所执行的数据类型存储在方法区中。
- 直接指针访问
如果使用指针访问,那么java堆对象的布局中就必须考虑如何放置访问类型的相关信息(如对象的类型,实现的接口、方法、父类、field等),而reference中存储的就是对象的地址。
这两种访问方式各有利弊,使用句柄访最大的好处是reference中存储着稳定的句柄地址,当对象移动之后(垃圾收集时移动对象是非常普遍的行为),只需要改变句柄中的对象实例地址即可,reference不用修改。
使用指针访问的好处是访问速度快,它减少了一次指针定位的时间开销,由于java是面向对象的语言,在开发中java对象的访问非常的频繁,因此这类开销积少成多也是非常可观的,反之则提升访问速度。
java内存的两种分配方式
- 指针碰撞
- 空闲列表
对象在内存中的布局
对象头主要包括对象自身的运行时数据,比如哈希码,对象分代年龄等,还有类型指针,确定对象是哪个类的实例。
(对象在堆中的存储布局):
- 对象头(如果对象是数组还需要存储数组长度):
- 实例数据:定义的各种类型的字段内容
- 对齐填充:保证对象的大小是8字节的整数倍
类的初始化时机
主动引用
- 当程序创建一个类的实例对象;
- 程序访问或设置类的静态变量(不是静态常量,会在编译时被加载到运行时常量池);程序调用类的静态方法
- 对类进行反射调用的时候
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
被动引用(不会触发)
通过子类引用父类的静态字段,不会导致子类初始化。
通过数组定义来引用类,不会触发初始化(数组由虚拟机直接创建)。
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
什么是类加载器,常见的类加载器有哪些?
类加载器是指:通过一个类的全限定性类名获取该类的二进制字节流叫做类加载器;类加载器分为以下四种:
启动类加载器(BootStrapClassLoader):用来加载java核心类库,无法被java程序直接引用;
扩展类加载器(Extension ClassLoader):用来加载java的扩展库,java的虚拟机实现会提供一个扩展库目录,该类加载器在扩展库目录里面查找并加载java类;
系统类加载器(AppClassLoader):它根据java的类路径来加载类,一般来说,java应用的类都是通过它来加载的;
自定义类加载器:由java语言实现,继承自ClassLoader;
双亲委派模型,问什么需要双亲委派模型,有什么优点?
当一个类收到了类加载请求时,自己不会先去加载这个类,而是将其委派给父类去加载,如果父类不能加载,反馈给子类,由子类去完成类的加载;
启动类加载器(引导类加载器 Bootstrap ClassLoader):此加载器用来加载java的核心库(JAVA_HOME/jre/lib/rt.jar)下面的内容,用于提供jvm自身需要的类。
扩展类加载器:主要加载%JAVA_HOME%\lib\ext目录下的类库文件或者java.ext.dirs系统变量所指定的类库文件(加载扩展库)
程序应用类加载器:主要加载用户类路径(classpath)所指定的类库。
用户自定义类加载器:加载用户自定义的类库。
为什么使用双亲委派机制对类进行加载?
- 避免类的重复加载,这样可以保证一个类只有一个类加载器进行加载。
- 保护程序的安全,防止核心的api被篡改。
为了防止内存中出现多个相同的字节码;因为如果没有双亲委派的话,用户就可以自己定义一个java.lang.String类,那么就无法保证类的唯一性。
补充:那怎么打破双亲委派模型?
自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法。
如何打破双亲委派模型?
双亲委派模型都依靠loadClass()
,重写loaderClass()
即可;
打破类加载器的案例
Tomcat 可以加载自己目录下的 class 文件,并不会传递给父类的加载器;
Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了才委派给父加载器。
tomcat之所以造了一堆自己的classloader,大致是出于下面三类目的:
- 对于各个 webapp中的 class和 lib,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的lib以便不浪费资源。
- 与 jvm一样的安全性问题。使用单独的 classloader去装载 tomcat自身的类库,以免其他恶意或无意的破坏;
- 热部署。
对象如何分配内存
对象优先在eden区分配
对象在新生代中eden区分配。当eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。先把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果ServicorTo不够位置了就放到老年区),同时把这些对象的年龄+1;然后,清空Eden和ServicorFrom中的对象;最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom区 ,也就是先从eden中分配内存。
大对象直接进入老年代(字符串,数组)
如果进行一次minor gc之后,还是没有足够的内存,那么就讲大对象直接分配到老年代。
长期存活的对象进入老年代
Eden区域对象经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。
动态对象年龄判定
如果在Survivor中相同年龄所有对象大小的总和大于Survivor空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代。
空间分配担保
在发生Minor GC之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果不成立的话虚拟机会查看是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC;如果小于,或者不允许冒险,那么就要进行一次Full GC。
Full GC的触发条件
对于Minor GC,其触发条件非常简单,当Eden空间满时,就将触发一次Minor GC。而Full GC则相对复杂,full gc是对整个堆内存空间进行回收,在进行full gc之前还会进行major gc操作,有以下条件:
1.调用System.gc()
只是建议虚拟机执行Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
2.老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过-Xmn虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过- XX:MaxTenuringThreshold调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
3.空间分配担保失败
使用复制算法的Minor GC需要老年代的内存空间作担保,如果担保失败会执行一次Full GC。
4.Concurrent Mode Failure
执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是GC过程中浮动垃圾过多导致暂时性的空间不足),便会报Concurrent Mode Failure错误,并触发Full GC。
5.JDK 1.7及以前的永久代空间不足
GC的两种判定方法,以及各有什么特点
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
缺点:无法解决循环引用的问题,当A引用B,B也引用A的时候,此时AB对象的引用都不为0,此时也就无法垃圾回收,所以一般主流虚拟机都不采用这个方法;
可达性分析算法
可达性分析法 从一个被称为GC Roots的对象向下搜索,如果一个对象到GC Roots没有任何引用链相连接时,说明此对象不可用,在java中可以作为GC Roots的对象有以下几种:
扩展:
可作为GC Roots的对象:(当前存活的对象)
虚拟机栈(栈帧中的本地变量表)中的引用的对象;
方法区中常量引用的对象(字符串常量池的引用);
Native方法引用的对象
但一个对象满足上述条件的时候,不会马上被回收,还需要进行两次标记;第一次标记:判断当前对象是否有finalize()方法并且该方法没有被执行过,若不存在则标记为垃圾对象,等待回收;若有的话,则进行第二次标记;第二次标记将当前对象放入F-Queue队列,并生成一个finalize线程去执行该方法,虚拟机不保证该方法一定会被执行,这是因为如果线程执行缓慢或进入了死锁,会导致回收系统的崩溃;如果执行了finalize方法之后仍然没有与GC Roots有直接或者间接的引用,则该对象会被回收;
强引用、软引用、弱引用、虚引用以及他们之间和gc的关系
- 强引用:new出的对象之类的引用, 只要强引用还在,gc时永远不会被回收
- 软引用:有用但非必须的对象,内存溢出异常之前,将会把软引用对象列入第二次回收。
- 弱引用:有用但非必须的对象,对象能生存到下一次垃圾收集发生之前。
- 虚引用:对生存时间无影响,在垃圾回收时得到通知。
- 终结器引用:
- 它用于实现对象的finalize() 方法,也可以称为终结器引用
- 无需手动编码,其内部配合引用队列使用
- 在GC 时,终结器引用入队。由Finalizer 线程通过终结器引用找到被引用对象调用它的finalize() 方法,第二次GC 时才回收被引用的对象
强引用是造成java内存泄漏的主要原因
不可达的对象并非“非死不可”
可达性分析法中不可达的对象先判断是否有必要执行finalize方法。当对象没有覆盖finalize方法,或finalize方法已经被虚拟机调用过时,虚拟机将直接回收。
被判定为需要执行的对象将会被放在一个队列中,除非在finalize方法中重新与引用链建立联系,否则直接回收。
能够找到 Reference Chain 的对象,就一定会存活么?
不一定,还要看 Reference 类型,弱引用在 GC 时会被回收,软引用在内存不足的时候会被回收,但如果没有 Reference Chain 对象时,就一定会被回收。
请问如何查看 JVM 系统默认值
使用 -XX:+PrintFlagsFinal 参数可以看到参数的默认值,这个默认值还和垃圾回收器有关,比如 UseAdaptiveSizePolicy。
谈谈对 OOM 的认识?如何排查 OOM 的问题?
除了程序计数器,其他内存区域都有 OOM 的风险。
- 栈一般经常会发生 StackOverflowError,比如 32 位的 windows 系统单进程限制 2G 内存,无限创建线程就会发生栈的 OOM
- Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最大元空间大小参数无效;
- 堆内存溢出,报错同上,这种比较好理解,GC 之后无法在堆中申请内存创建对象就会报错;
- 方法区 OOM,经常会遇到的是动态生成大量的类、jsp 等;
- 直接内存 OOM,涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请。
排查 OOM 的方法:
- 增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;
- 同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
- 使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 。
那些内存区域会发生OOM以及会进行GC?
会发生OOM的内存区域:本地方法栈,虚拟机栈,堆区域,方法区
会发生GC的区域:堆区,方法区
既没有GC又没有OOM的区域:PC寄存器
谈谈 JVM 中的常量池?
JVM常量池主要分为Class文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类对象常量池。
- Class文件常量池。class文件是一组以字节为单位的二进制数据流,在java代码的编译期间,我们编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池。
- 运行时常量池:运行时常量池相对于class常量池一大特征就是具有动态性,java规范并不要求常量只能在运行时才产生,也就是说运行时常量池的内容并不全部来自class常量池,在运行时可以通过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是String.intern()。
- 全局字符串常量池:字符串常量池是JVM所维护的一个字符串实例的引用表,在HotSpot VM中,它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned string”或通常所说的”进入了字符串常量池的字符串”。
- 基本类型包装类对象常量池:java中基本类型的包装类的大部分都实现了常量池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。
栈帧都有哪些数据?
栈帧包含:局部变量表、操作数栈、动态连接、返回地址等。
什么情况会造成元空间溢出?
元空间默认是没有上限的,不加限制比较危险。当应用中的 Java 类过多时,比如 Spring 等一些使用动态代理的框架生成了很多类,如果占用空间超出了我们的设定值,就会发生元空间溢出。
什么时候会造成堆外内存溢出?
使用了 Unsafe 类申请内存,或者使用了 JNI 对内存进行操作,这部分内存是不受 JVM 控制的,不加限制使用的话,会很容易发生内存溢出。
HashMap 中的 key,可以是普通对象么?有什么需要注意的地方?
Map 的 key 和 value 可以是任何类型,但要注意的是,一定要重写它的 equals 和 hashCode 方法,否则容易发生内存泄漏。
HashMap中的key可以是null,基本数据类型或引用数据类型。为了HashMap的正常使用,key一般是不可变对象,至少该对象中用于计算hash值的属性要不可变,方可保证HashMap的正常使用。
怎么看死锁的线程?
通过 jstack 命令,可以获得线程的栈信息,死锁信息会在非常明显的位置(一般是最后)进行提示。
什么是方法内联?
为了减少方法调用的开销,可以把一些短小的方法,比如 getter/setter,纳入到目标方法的调用范围之内,这样就少了一次方法调用,速度就能得到提升,这就是方法内联的概念。
对象是怎么从年轻代进入老年代的?
在下面 4 种情况下,对象会从年轻代进入到老年代。
如果对象够老,则会通过提升(Promotion)的方式进入老年代,一般根据对象的年龄进行判断。
动态对象年龄判定,有的垃圾回收算法,比如 G1,并不要求 age 必须达到 15 才能晋升到老年代,它会使用一些动态的计算方法。
分配担保,当 Survivor 空间不够的时候,则需要依赖其他内存(指老年代)进行分配担保,这个时候,对象也会直接在老年代上分配。
超出某个大小的对象将直接在老年代上分配,不过这个值默认为 0,意思是全部首选 Eden 区进行分配。
垃圾回收算法(重点)
标记-清除法:标记出没有用的对象,之后一个一个回收掉
算法过程
标记: Collector 从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header 中记录为可达对象。注意:标记的是被引用的对象,也就是可达对象,并非标记的是即将被清除的垃圾对象,不可达的对象无法标记。
清除: Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header 中没有标记为可达对象,则将其回收,此时对整个堆内存执行遍历操作,就可以发现那些不可达的垃圾对象然后清除操作。
缺点
- 标记清除算法的效率不算高(应为需要对整个堆空间进行遍历,还有遍历可达的对象)。
- 在进行GC 的时候,需要停止整个应用程序,用户体验较差
- 这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表。
优点
- 实现起来比较简单
复制算法
算法过程
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
大部分新生代使用的垃圾回收算法就是复制算法。
优点
- 没有标记和清除过程,实现简单,运行高效,最明显的特征。
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。所以这种垃圾清楚后,对象内存的分配可以用指针碰撞的方式进行分配,但是标记-清楚算法回收的内存,只能采用空闲列表的方式分配对象的内存。
缺点
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于G1 这种分拆成为大量region 的GC ,复制而不是移动,意味着GC 需要维护region 之间对象引用关系,不管是内存占用或者时间开销也不小(也就是栈中存储对象的引用(对象的地址)也需要发生变化)。
标记压缩算法
执行过程
- 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
- 第二阶段将所有的存活对象压缩到内存的一边,按照顺序排放,之后清理边界之外的所有内存空间。
标记-压缩算法与标记-清除算法的比较
- 标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩( Mark-Sweep-Compact )算法。
- 二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
- 可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时, JVM 只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
优点
消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
消除了复制算法当中,内存减半的高额代价。
缺点
- 从效率上来说,标记-整理算法要低于复制算法。因为清除后还涉及对象内存的整理。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址(因为HotSpot 虚拟机采用的不是句柄池的方式,而是直接指针)
- 移动过程中,需要全程暂停用户应用程序。即: STW
垃圾回收算法小结
- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。以空间换取时间效率。
- 而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
你知道都有哪些垃圾回收器,各有什么特点
图片来自于知乎:知识追寻者。
对象什么时候会被GC
引用计数算法
- 引用计数算法( Reference Counting )比较简单,对每个对象保存一个整数的引用计数器属性,用于记录对象被引用的情况。
- 对于一个对象A ,只要有任何一个对象引用了A ,则A 的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A 的引用计数器的值为0,即表示对象A 不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java 的垃圾回
收器中没有使用这类算法。
可达性分析算法
- 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中存在的循环引用问题,防止内存泄漏的发生。
- 相较于引用计数算法,这里的可达性分析就是Java 、C# 选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集( Tracing Garbage Collection )
算法实现思路
可达性分析算法是以根对象集合( GCRoots )为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链( Reference Chain )
如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GC root可以是那些元素?
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中常量池中引用的对象
- 所有被同步锁synchronized持有的对象
- java虚拟机内部引用的对象
- 方法区中类静态属性引用的对象
小结
- 总结一句话就是,除了堆空间的周边,比如:虚拟机栈、本地方法栈、方法区地方对堆空间进行引用的,都可以作为GC Roots 进行可达性分析
- 除了这些固定的GC Roots 集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots 集合。比如:分代收集和局部回收( PartialGC )
- 可达性分析算法必须在一个能保证一致性快照中进行。
说说Java中栈内存和堆内存的区别
- 从存储数据的角度说:栈内存用来存储基本类型的变量和对象的引用变量,堆内存用来存储Java中的对象;java中基本上所有的对象都存储在堆内存区域。
- 从是否共享角度说:栈内存线程私有,堆内存线程共享
- 是否会发生内存的溢出:栈内存不足时,JVM会抛出java.lang.StackOverFlowError(一般发生在递归的时候);堆内存不足时,JVM会抛出java.lang.OutOfMemoryError
- 栈的内存远小于堆内存,-Xss选项设置栈的大小。-Xms选项可以设置堆的开始大小;堆一般把最大堆内存和最小堆内存设置为一样。
java对象创建(5种创建对象的方法)
- 使用new关键字创建对象; 会调用构造方法;
- 使用Class类的newInstance方法(反射机制);会调用构造方法;
- 使用Constructor类的newInstance方法(反射机制);会调用构造方法;
- 使用Clone方法创建对象;不会调用构造方法;
- 使用(反)序列化机制创建对象;不会调用构造方法;
如果对象的引用被置为 null,垃圾收集器是否会立即回收对象?
不会,在下一个垃圾回收周期中回收对象。
你知道哪些JVM调优参数
- -Xms128m JVM初始分配的堆内存
- -Xmx512m JVM最大允许分配的堆内存,按需分配;
- -XX:MetaspaceSize:分配给类元数据空间(以字节计)的初始大小;
- -XX:MaxMetaspaceSize:分配给类元数据空间的最大值,超过此值就会触发Full GC
- -XX:NewRatio:新生代和老年代的占比;
- -XX:NewSize:新生代空间;
- -XX:SurvivorRatio:伊甸园空间和幸存者空间的占比;
- -XX:MaxTenuringThreshold:对象进入老年代的年龄阈值;
- XX:+PrintGC:打印 gc 信息;
- -XX:+PrintGCDetails:打印 gc 详细信息
为什么字符串常量池在不同版本的jdk中位置会发生变化
- 永久代的默认空间大小比较小,但是字符串的使用又比较的频繁,所以进行调整,放入堆内存中,空间比较大。最初是放在永久代中,但是永久代中垃圾回收不频繁。
- 永久代垃圾回收频率低,大量的字符串无法及时回收,容易进行Full GC 产生STW 或者容易产生OOM:
PermGen Space - 堆中空间足够大,字符串可被及时回收。
- 在jdk6中是放在永久代中,但是在jdk7/jdk8中,把静态变量和字符串常量池移动到堆内存中,可以频繁的进行垃圾回收操作。
概述一下类结构文件
在 Java 中,JVM 可以理解的代码就叫做字节码
(即扩展名为 .class
的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。比如scala也可以被编译为.class文件在虚拟机上面运行。
可以说.class
文件是不同的语言在 Java 虚拟机之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因。
谈谈你对jvm的理解
Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
什么是字节码文件,采用字节码文件的好处?
在 Java 中,JVM可以理解的代码就叫做
字节码
(即扩展名为.class
的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。
java程序从源文件到运行可以经过下面三个步骤:
我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了JIT 编译器,而JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。
HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了JIT预热等各方面的开销。JDK支持分层编译和AOT协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。
总结:
Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
JVM 配置常用参数有哪些?
垃圾回收参数
-Xnoclassgc 是否对类进行回收
-verbose:class -XX:+TraceClassUnloading 查看类加载和卸载信息
-XX:SurvivorRatio Eden和其中一个survivor的比值
-XX:PretenureSizeThreshold 大对象进入老年代的阈值,Serial和ParNew生效
-XX:MaxTenuringThreshold 晋升老年代的对象年龄,默认15, CMS默认是4
-XX:HandlePromotionFailure 老年代担保
-XX:+UseAdaptiveSizePolicy动态调整Java堆中各个区域大小和进入老年代年龄
-XX:ParallelGCThreads 并行回收的线程数
-XX:MaxGCPauseMillis Parallel Scavenge参数,设置GC的最大停顿时间
-XX:GCTimeRatio Parallel Scavenge参数,GC时间占总时间的比率,默认99%,即1%的GC时间
-XX:CMSInitiatingOccupancyFraction,old区触发cms阈值,默认68%
-XX:+UseCMSCompactAtFullCollection(CMS完成后是否进行一次碎片整理,停顿时间加长)
-XX:CMSFullGCsBeforeCompaction(执行多少次不进行碎片整理的FullGC后进行一次带压缩的)
-XX:+ScavengeBeforeFullGC,在fullgc前触发一次minorGC
垃圾回收统计信息
-XX:+PrintGC 输出GC日志
-verbose:gc等同于上面那个
-XX:+PrintGCDetails 输出GC的详细日志
堆大小设置
-Xmx:最大堆大小
-Xms:初始堆大小(最小内存值)
-Xmn:年轻代大小
-XX:NewSize和-XX:MaxNewSize 新生代大小
-XX:SurvivorRatio:3 意思是年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-Xss栈容量 默认256k
-XX:PermSize永久代初始值
-XX:MaxPermSize 永久代最大值
进程是资源分配的基本单位,线程是调度的基本单位
虚拟机栈和本地方法栈为什么是线程私有的?
- 虚拟机栈:每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
说一下 JVM 调优的命令?
jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
jstat:jstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jmap:jmap(JVM Memory Map)命令用于生成heap dump文件,如果不使用这个命令,还阔以使用-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候·自动生成dump文件。 jmap不仅能生成dump文件,还阔以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。
jhat:jhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。
jstack:jstack用于生成java虚拟机当前时刻的线程快照。jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。
说说类加载的五个过程
类的加载主要是三个阶段,加载,链接(验证,准备,解析),初始化,其中在链接阶段又分为三个阶段。其中类的加载过程如下:
每一个阶段的工作如下:
加载阶段:
- 通过一个类的权限定名,获取定义此类的二进制字节流,注意这个字节流可以是经过编译器编译后产生的字节流,也可以是网络上的字节流文件。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(也就是运行时数据区)。
- 在堆内存中生成一个代表这个类的java. lang.class对象,作为方法区这个类的各种数据结构的访问入口。
根据类的全限定名获取类的二进制字节流,并转化成方法区的运行时数据结构,然后生成一个对应的Class对象,作为方法区中该类各种数据的访问入口。(数组由虚拟机创建而不是类加载器)
链接阶段:
验证:验证经过第一个阶段后加载进来的字节码文件是否是正确的,防止危害虚拟机的安全。
准备:对类变量以及类分配内存并且初始化,常量在编译的时候已经进行内存分配和初始化操作。
解析:将常量池的符号引用转换为直接引用的过程。
初始化阶段:
初始化阶段就是执行类的构造器方法clinit()的过程。这个方法是jvm把类中的类变量和静态代码块组合起来形成的一个方法
**使用阶段:**使用类
**卸载阶段:**对类型进行卸载。
那些GCroots可以回收
- 虚拟机栈 本地方法栈引用的对象
- 方法区中静态属性、常量引用的对象
简述 Java 垃圾回收机制。
在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
对于 GC 来说,当程序员创建对象时,GC 就开始监控这个对象的地址、大小以及使用情况。通常,GC 采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当 GC 确定一些对象为“不可达”时,GC 就有责任回收这些内存空间。程序员可以手动执行 System.gc(),通知 GC 运行,但是 Java 语言规范并不保证 GC 一定会执行。
GC收集器有哪些,CMS收集器和G1收集器的特点
一共有7款收集器
serial收集器
单线程,串行执行,工作时必须暂停其他工作线程。多用于client机器上,使用复制算法。
一个单线程的收集器,在进行垃圾收集时候,必须暂停其他所有的工作线程直到它收集结束,年轻代垃圾收集器。
- 特点:CPU利用率最高,停顿时间即用户等待时间比较长。
- 适用场景:小型应用
- 通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
ParNew收集器(又叫Parallel收集器)
serial收集器的多线程版本,server模式下虚拟机首选的新生代收集器,年轻代垃圾收集器。复制算法
采用多线程来通过扫描并压缩堆
- 特点:停顿时间短,回收效率高,对吞吐量要求高。
- 适用场景:大型应用,科学计算,大规模数据采集等。
- 通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
Parallel Scavenge(并行清除)收集器
复制算法,可控制吞吐量的收集器。吞吐量即有效运行时间,吞吐量优先,年轻代垃圾收集器。并行垃圾收集器
Serial Old收集器
serial的老年代版本,使用整理算法。串行垃圾收集器。
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,多线程,采用标记压缩算法,基于并行回收。
CMS收集器
目标是最短回收停顿时间。标记清除算法实现,使用多线程的算法去扫描堆,对发现未使用的对象进行回收。分四个阶段:
- 初始标记:GC Roots直连的对象做标记
- 并发标记:多线程方式GC Roots Tracing,这个时候还有可能产生垃圾。
- 重新标记:修正第二阶段标记的记录
- 并发清除。
尽管CMS
收集器采用的是并发回收(非独占式),但是在其初始化标记和重新标记这两个阶段中仍然需要执行“Stop-the-World”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“Stop-the-World
”,只是尽可能地缩短暂停时间。
由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
- 特点:响应时间优先,减少垃圾收集停顿时间,缺点是会产生垃圾碎片。
- 适应场景:服务器、电信领域等。
- 通过JVM参数 -XX:+UseConcMarkSweepGC设置
G1收集器
基本思想是化整为零,将堆分为多个Region,优先收集回收价值最大的Region。
- 并行并发
- 分代收集
- 空间整合(标记整理算法)
- 可预测的停顿
在G1中,堆被划分成 许多个连续的区域(region)。采用G1算法进行回收,吸收了CMS收集器特点。
特点:
- 支持很大的堆,高吞吐量
- 支持多CPU和垃圾回收线程
- 在主线程暂停的情况下,使用并行收集
- 在主线程运行的情况下,使用并发收集
- 实时目标:可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收
- 通过JVM参数 –XX:+UseG1GC 使用G1垃圾回收器
Minor GC,Major GC 和Full GC分别发生在什么时候,各有什么特点?
新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。
老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。
什么时候触发FULL GC
说说常用的内存调试工具?
- jps:查看虚拟机进程的状况,如进程ID
- jmap:用于生成堆转储快照文件(某一时刻的)
- jstack:用来生成线程快照(某一时刻的)。生成线程快照的主要目的是定位线程长时停顿的原因(如死锁,死循环,等待I/O 等),通过查看各个线程的调用堆栈,就可以知道没有响应的线程在后台做了什么或者等待什么资源。
- jconsole:主要是内存监控和线程监控。内存监控:可以显示内存的使用情况。线程监控:遇到线程停顿时,可以使用这个功能。
- jstat:虚拟机统计信息监视工具。如显示垃圾收集的情况,内存使用的情况。
简单介绍一下什么是类加载机制
描述一下 JVM 加载 Class 文件的原理机制?
Java 语言是一种具有动态性的解释型语言,类(Class)只有被加载到 JVM 后才能运行。当运行指定程序时,JVM 会将编译生成的 .class 文件按照需求和一定的规则加载到内存中,并组织成为一个完整的 Java 应用程序。这个加载过程是由类加载器完成,具体来说,就是由 ClassLoader 和它的子类来实现的。类加载器本身也是一个类,其实质是把类文件从硬盘读取到内存中。
类的加载方式分为隐式加载和显示加载。隐式加载指的是程序在使用 new 等方式创建对象时,会隐式地调用类的加载器把对应的类加载到 JVM 中。显示加载指的是通过直接调用 class.forName() 方法来把所需的类加载到 JVM 中。
任何一个工程项目都是由许多类组成的,当程序启动时,只把需要的类加载到 JVM 中,其他类只有被使用到的时候才会被加载,采用这种方法一方面可以加快加载速度,另一方面可以节约程序运行时对内存的开销。此外,在 Java 语言中,每个类或接口都对应一个 .class 文件,这些文件可以被看成是一个个可以被动态加载的单元,因此当只有部分类被修改时,只需要重新编译变化的类即可,而不需要重新编译所有文件,因此加快了编译速度。
在 Java 语言中,类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(例如基类)完全加载到 JVM 中,至于其他类,则在需要的时候才加载。
类加载的主要步骤:
• 装载。根据查找路径找到相应的 class 文件,然后导入。
• 链接。链接又可分为 3 个小步:
• 检查,检查待加载的 class 文件的正确性。
• 准备,给类中的静态变量分配存储空间。
• 解析,将符号引用转换为直接引用(这一步可选)
• 初始化。对静态变量和静态代码块执行初始化工作。
双亲委派模型,问什么需要双亲委派模型,有什么优点?
启动类加载器(引导类加载器 Bootstrap ClassLoader):此加载器用来加载java的核心库(JAVA_HOME/jre/lib/rt.jar)下面的内容,用于提供jvm自身需要的类。
扩展类加载器:主要加载%JAVA_HOME%\lib\ext目录下的类库文件或者java.ext.dirs系统变量所指定的类库文件(加载扩展库)
程序应用类加载器:主要加载用户类路径(classpath)所指定的类库。
用户自定义类加载器:加载用户自定义的类库。
为什么使用双亲委派机制对类进行加载?
- 避免类的重复加载,这样可以保证一个类只有一个类加载器进行加载。
- 保护程序的安全,防止核心的api被篡改。
Java内存分配
- 寄存器:我们无法控制。
- 静态域:static定义的静态成员。
- 常量池:编译时被确定并保存在 .class 文件中的(final)常量值和一些文本修饰的符号引用(类和接口的全限定名,字段的名称和描述符,方法和名称和描述符)。
- 非 RAM 存储:硬盘等永久存储空间。
- 堆内存:new 创建的对象和数组,由 Java 虚拟机自动垃圾回收器管理,存取速度慢。
- 栈内存:基本类型的变量和对象的引用变量(堆内存空间的访问地址),速度快,可以共享,但是大小与生存期必须确定,缺乏灵活性。
Java 堆的结构是什么样子的?什么是堆中的永久代(Perm Gen space)?
- JVM 的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在 JVM 启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。
- 堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些 对象回收掉之前,他们会一直占据堆内存空间。
分派:静态分派和动态分派
解析
类加载时进行,将部分方法的符号引用转化为直接引用。这种解析能够成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法的调用被称为解析(Resolution)。这里边有两个重要的点:编译期可知,运行期不可变只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为“非虚方法”(Non-VirtualMethod),与之相反,其他方法就被称为“虚方法”(Virtual Method)。
分派
这里所谓的分派指的是在Java中对方法的调用。Java中有三大特性:封装、继承和多态。分派是多态性的体现,Java虚拟机底层提供了我们开发中“重写”和“重载”的底层实现。其中重载属于静态分派,而重写则是动态分派的过程。除了使用分派的方式对方法进行调用之外,还可以使用解析调用,解析调用是在编译期间就已经确定了,在类装载的解析阶段就会把符号引用转化为直接引用,不会延迟到运行期间再去完成。而分派调用则既可以是静态的也可以是动态(就是这里的静态分派和动态分派)的。
方法解析
对于方法的调用,虚拟机提供了四条方法调用的字节码指令,分别是:
invokestatic
: 调用静态方法invokespecial
: 调用构造方法,私有方法,父类方法invokevirtual
: 调用虚方法invokeinterface
: 调用接口方法
其中,1和2都可以在类加载阶段确定方法的唯一版本,因此,在类加载阶段就可以把符号引用解析为直接引用,在调用时刻直接找到方法代码块的内存地址进行执行(编译时已经找到了,并且存在方法调用的入口);3和4则是在运行期间动态绑定方法的直接引用。
invokestatic
指令和invokespecial
指令调用的方法称为非虚方法,注意,final
修饰的方法也属于虚方法。
静态分派
静态分派只会涉及重载,而重载是在编译期间确定的,那么静态分派自然是一个静态的过程(因为还没有涉及到Java虚拟机)。静态分派的最直接的解释是在重载的时候是通过参数的静态类型而不是实际类型作为判断依据的。比如创建一个类O
,在O
中创建了静态类内部类A
,O
中又有两个静态类内部类B
、C
继承了这个静态内部类A
,那么实际上当编写如下的代码:
那些内存区域会发生OOM以及会进行GC?
会发生OOM的内存区域:本地方法栈,虚拟机栈,堆区域,方法区
会发生GC的区域:堆区,方法区
既没有GC又没有OOM的区域:PC寄存器
如何打破双亲委派模型?
双亲委派模型都依靠loadClass()
,重写loaderClass()
即可;
新生代中区分Eden和Survivor的作用是什么
新生代分为 3 个分区:Eden(伊甸园)、Survivor0、Survivor1;其中Survivor0、 Survivor1 合起来成为Survivor(幸存区); 如果没有Survivor,Eden区每进行一次Minor GC
,存活的对象都会被送到老年代。老年代将很快被填满,老年代每发生一次Full GC
的速度比 Minor GC
慢10倍;所以产生了Survivor区,每产生一次minor GC操作,都会把当前存活下来的对象放入Survivor区域中,等到对象存活到一定的年龄,然后在放到老年代,等老年代块满的时候,在进行一次major gc释放内存空间。对象年龄默认是16岁。
Survivor 的作用就是减少老年代
Full GC
的次数,相当于缓冲带;
Eden和Survivor的比例分配8:1:1默认情况下新生代和老年代的比例是1:2的比例。
简述分代垃圾回收器工作流程
- new 的对象先放eden 区。此区有大小限制。
- 当伊甸园的空间填满时,程序又需要创建对象, JVM 的垃圾回收器将对伊甸园区进行垃圾回收( Minor GC ),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区,没有被销毁的对象放入survivor0 区域。
- 如果eden区域满,再次触发垃圾回收,此时上次幸存下来的放到幸存者survivor0 区的,如果没有回收,就会放到幸存者survivor1 区,然后把这一次eden中存活的对象也放到survivor1 区。
- ……..
- 如果再次经历垃圾回收,此时会重新放回幸存者survivor0 区,接着再去幸存者survivor1 区。
- 啥时候能去养老区呢?可以设置次数。默认是15次。·可以设置参数: -XX:MaxTenuringThreshold =进行设置,也就是设置对象的生存年龄。
- 在养老区,对象相对悠闲。当老年区内存不足时,再次触发GC:Major GC ,进行养老区的内存清理。
- 若养老区执行了Major GC 之后发现依然无法进行对象的保存,就会触发FULL GC操作,如果内存空间还是不够,就会产生OOM 异常。
关于垃圾回收,频繁发生在新生代,很少发生在老年代,几乎不会再永久代或者元空间发生,
对象内存空间分配的特殊情况
- 如果来了一个新对象,先看看 Eden 是否放的下?
- 如果Eden 放得下,则直接放到 Eden 区
- 如果 Eden 放不下,则触发YGC ,执行垃圾回收,看看还能不能放下?
- 将对象放到老年区又有两种情况:
- 如果 Eden 执行了 YGC 还是无法放不下该对象,那没得办法,只能说明是超大对象,只能直接放到老年代
- 那万一老年代都放不下,则先触发FullGC ,再看看能不能放下,放得下最好,但如果还是放不下,那只能报OOM
- 如果 Eden 区满了,将对象往幸存区拷贝时,发现幸存区放不下啦,那只能便宜了某些新对象,让他们直接晋升至老年区
图示过程:
Minor GC和Full GC触发条件,答默认情况下发生15次Minor GC之后就会触发一次Full GC
触发Major GC是eden区域的行为,不是幸存者区域的行为,eden是主动的,幸存者区是被动行为。
GC是什么? 为什么要有 GC?
GC 是垃圾收集(GabageCollection);Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,而不需要人为手动释放内存;主要调用的是 System.gc() 和 Runtime.getRuntime().gc();
简述CMS收集器
CMS(Concurrent Mark Sweep)收集器基于标记—清除算法
实现的收集器,是一种以获取最短回收停顿时间为目标的收集器。主要优点是并发收集,低停顿,在cpu多核的情况下性能较好。在启动 JVM 的参数加上
“-XX:+UseConcMarkSweepGC”来指定使用 CMS 垃圾回收器;其使用在老年代 可以配合新生代的Serial和ParNew收集器一起使用;由于 CMS 使用 标记—清除算法 GC时会产生大量碎片,有可能提前触发Full GC;如果在老年代充满之前无法回收不可达对象,或者没有足够的空间满足分配就会导致Concurrent Mode Failure(并发模式故障);
简述G1收集器
G1(Garbage-First)从整体来看是基于标记—整理
算法实现的收集器,能够实现并发并行,对cpu利用率较高,减少停顿时间。目标是取代jdk1.5发布的CMS收集器。G1收集器收集范围是老年代和新生代,不需要结合其他收集器使用,G1收集器可预测垃圾回收的停顿时间,对空间进行整合;由于G1是基于复制算法实现,当没有足够的空间(region)分配存活的对象就会导致Allocation (Evacuation) Failure(分配失败);
简述G1和CMS的对比
垃圾回收算法(重点)
标记-清除法:标记出没有用的对象,之后一个一个回收掉
算法过程
- 标记: Collector 从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header 中记录为可达对象。注意:标记的是被引用的对象,也就是可达对象,并非标记的是即将被清除的垃圾对象,不可达的对象无法标记。
- 清除: Collector 对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header 中没有标记为可达对象,则将其回收,此时对整个堆内存执行遍历操作,就可以发现那些不可达的垃圾对象然后清除操作。
缺点
- 标记清除算法的效率不算高(应为需要对整个堆空间进行遍历,还有遍历可达的对象)。
- 在进行GC 的时候,需要停止整个应用程序,用户体验较差
- 这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表。
优点
- 实现起来比较简单
复制算法
算法过程
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收,但是这种方式,内存的代价太高,每次基本上都要浪费一般的内存。
于是将该算法进行了改进,内存区域不再是按照 1:1 去划分,而是将内存划分为 8:1:1 三部分,较大那份内存交 Eden 区,其余是两块较小的内存区叫 Survior 区。每次都会优先使用 Eden 区,若 Eden 区满,就将对象复制到第二块内存区上,然后清除 Eden 区,如果此时存活的对象太多,以至于 Survivor 不够时,会将这些对象通过分配担保机制复制到老年代中。(java 堆又分为新生代和老年代)
大部分新生代使用的垃圾回收算法就是复制算法。
优点
- 没有标记和清除过程,实现简单,运行高效,最明显的特征。
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。所以这种垃圾清楚后,对象内存的分配可以用指针碰撞的方式进行分配,但是标记-清楚算法回收的内存,只能采用空闲列表的方式分配对象的内存。
缺点
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于G1 这种分拆成为大量region 的GC ,复制而不是移动,意味着GC 需要维护region 之间对象引用关系,不管是内存占用或者时间开销也不小(也就是栈中存储对象的引用(对象的地址)也需要发生变化)。
标记压缩算法
执行过程
- 第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
- 第二阶段将所有的存活对象压缩到内存的一边,按照顺序排放,之后清理边界之外的所有内存空间。
标记-压缩算法与标记-清除算法的比较
- 标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩( Mark-Sweep-Compact )算法。
- 二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。
- 可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给新对象分配内存时, JVM 只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。
优点
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
- 消除了复制算法当中,内存减半的高额代价。
缺点
- 从效率上来说,标记-整理算法要低于复制算法。因为清除后还涉及对象内存的整理。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址(因为HotSpot 虚拟机采用的不是句柄池的方式,而是直接指针)
- 移动过程中,需要全程暂停用户应用程序。即: STW
分代收集算法
现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期
短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保。
垃圾回收算法小结
- 效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。以空间换取时间效率。
- 而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
什么是分代回收算法,为什么要进行分代回收
分代垃圾回收是基于这样一个事实:不同的对象的生存周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
分代收集算法
当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。
年轻代(Young Generation)的回收算法 (主要以 复制算法为主)
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
分代收集下的年轻代和老年代应该采用什么样的垃圾回收算法?
- 年轻代(Young Generation)的回收算法 (主要以 复制算法为主)
- 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
- 新生代内存按照 8:1:1 的比例分为一个 eden 区和两个 survivor(survivor0、 survivor1)区。大部分对象在 Eden 区中生成。回收时先将 Eden 区存活对象复制到一个 survivor0 区,然后清空 eden 区,当这个 survivor0 区也存放满了时,则将 eden 区和 survivor0 区存活对象复制到另一个 survivor1 区,然后清空 eden 区 和这个 survivor0 区,此时 survivor0 区是空的,然后将survivor0 区和 survivor1 区交换,即保持 survivor1 区为空, 如此往复。
- 当 survivor1 区不足以存放 Eden 区 和 survivor0区 的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。
- 新生代发生的 GC 也叫做 Minor GC,MinorGC 发生频率比较高(不一定等 Eden 区满了才触发)。新生代触发GC一定是eden区域的行为,幸存者区域一般是被动的行为。
- 年老代(Old Generation)的回收算法(主要以 标记压缩 为主)
- 在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
- 内存比新生代也大很多(大概比例是1 : 2),当老年代内存满时触发 Major GC 即 Full GC,Full GC 发生频率比较低,老年代对象存活时间比较长,存活率标记高。
- 如果有大对象,eden区域放不下,也会考虑直接存放在老年代中。
你知道都有哪些垃圾回收器,各有什么特点
对象什么时候会被GC
引用计数算法
- 引用计数算法( Reference Counting )比较简单,对每个对象保存一个整数的引用计数器属性,用于记录对象被引用的情况。
- 对于一个对象A ,只要有任何一个对象引用了A ,则A 的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A 的引用计数器的值为0,即表示对象A 不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java 的垃圾回
收器中没有使用这类算法。
可达性分析算法
- 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中存在的循环引用问题,防止内存泄漏的发生。
- 相较于引用计数算法,这里的可达性分析就是Java 、C# 选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集( Tracing Garbage Collection )
算法实现思路
- 可达性分析算法是以根对象集合( GCRoots )为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
- 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链( Reference Chain )
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
- 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
GC root可以是那些元素?
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中常量池中引用的对象
- 所有被同步锁synchronized持有的对象
- java虚拟机内部引用的对象
- 方法区中类静态属性引用的对象
小结
- 总结一句话就是,除了堆空间的周边,比如:虚拟机栈、本地方法栈、方法区地方对堆空间进行引用的,都可以作为GC Roots 进行可达性分析
- 除了这些固定的GC Roots 集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots 集合。比如:分代收集和局部回收( PartialGC )
- 可达性分析算法必须在一个能保证一致性快照中进行。
对象什么时候会放入老年代
引起类加载操作有哪些?
说说Java中栈内存和堆内存的区别
- 从存储数据的角度说:栈内存用来存储基本类型的变量和对象的引用变量,堆内存用来存储Java中的对象;java中基本上所有的对象都存储在堆内存区域。
- 从是否共享角度说:栈内存线程私有,堆内存线程共享
- 是否会发生内存的溢出:栈内存不足时,JVM会抛出java.lang.StackOverFlowError(一般发生在递归的时候);堆内存不足时,JVM会抛出java.lang.OutOfMemoryError
- 栈的内存远小于堆内存,-Xss选项设置栈的大小。-Xms选项可以设置堆的开始大小;堆一般把最大堆内存和最小堆内存设置为一样。
强引用、软引用、弱引用、虚引用以及他们之间和gc的关系
- 强引用:new出的对象之类的引用, 只要强引用还在,gc时永远不会被回收
- 软引用:有用但非必须的对象,内存溢出异常之前,将会把软引用对象列入第二次回收。
- 弱引用:有用但非必须的对象,对象能生存到下一次垃圾收集发生之前。
- 虚引用:对生存时间无影响,在垃圾回收时得到通知。
- 终结器引用:
- 它用于实现对象的finalize() 方法,也可以称为终结器引用
- 无需手动编码,其内部配合引用队列使用
- 在GC 时,终结器引用入队。由Finalizer 线程通过终结器引用找到被引用对象调用它的finalize() 方法,第二次GC 时才回收被引用的对象
强引用是造成java内存泄漏的主要原因
java对象创建(5种创建对象的方法)
- 使用new关键字创建对象; 会调用构造方法;
- 使用Class类的newInstance方法(反射机制);会调用构造方法;
- 使用Constructor类的newInstance方法(反射机制);会调用构造方法;
- 使用Clone方法创建对象;不会调用构造方法;
- 使用(反)序列化机制创建对象;不会调用构造方法;
jvm调优工具又哪些?各自的作用又是什么(重点)
- jps: 查看进程的参数信息;
- jstat: 查看某个Java进程内的线程堆栈信息;
- jinfo: 查看虚拟机参数;
- jmap:查看堆内存使用状况,生成快照存储(dump文件);
- jhat: 分析jmap dump生成的快照文件;
- jconsole: 基于JMX的可视化工具,监控 cpu, 内存,线程等使用情况;
- jvisualvm: JDK 自带分析工具,功能齐全,如查看进行信息,快照转存,监控cpu,线程,方法区,堆等;
你知道哪些JVM调优参数
- -Xms128m JVM初始分配的堆内存
- -Xmx512m JVM最大允许分配的堆内存,按需分配;
- -XX:MetaspaceSize:分配给类元数据空间(以字节计)的初始大小;
- -XX:MaxMetaspaceSize:分配给类元数据空间的最大值,超过此值就会触发Full GC
- -XX:NewRatio:新生代和老年代的占比;
- -XX:NewSize:新生代空间;
- -XX:SurvivorRatio:伊甸园空间和幸存者空间的占比;
- -XX:MaxTenuringThreshold:对象进入老年代的年龄阈值;
- XX:+PrintGC:打印 gc 信息;
- -XX:+PrintGCDetails:打印 gc 详细信息
为什么字符串常量池在不同版本的jdk中位置会发生变化
- 永久代的默认空间大小比较小,但是字符串的使用又比较的频繁,所以进行调整,放入堆内存中,空间比较大。最初是放在永久代中,但是永久代中垃圾回收不频繁。
- 永久代垃圾回收频率低,大量的字符串无法及时回收,容易进行Full GC 产生STW 或者容易产生OOM:
PermGen Space - 堆中空间足够大,字符串可被及时回收。
- 在jdk6中是放在永久代中,但是在jdk7/jdk8中,把静态变量和字符串常量池移动到堆内存中,可以频繁的进行垃圾回收操作。
概述一下类结构文件
在 Java 中,JVM 可以理解的代码就叫做字节码
(即扩展名为 .class
的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。比如scala也可以被编译为.class文件在虚拟机上面运行。
可以说.class
文件是不同的语言在 Java 虚拟机之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因。
jvm工具
- java – 运行工具,运行.class字节码的工具、
- javac -编译器,将后缀名为.java的文件编译为后缀名为.class的文件。
- javap - 反编译程序
- javadoc - 文档生成器,从源码的注释中提取文档。
- jar - 打包工具,将相关的类文件打成一个jar包。
- jdb -debugger 调试工具
- jps - 显示当前java程序的运行状态。
- extcheck -一个检测jar包冲突的工具
JVM 配置常用参数有哪些?
垃圾回收参数
-Xnoclassgc 是否对类进行回收
-verbose:class -XX:+TraceClassUnloading 查看类加载和卸载信息
-XX:SurvivorRatio Eden和其中一个survivor的比值
-XX:PretenureSizeThreshold 大对象进入老年代的阈值,Serial和ParNew生效
-XX:MaxTenuringThreshold 晋升老年代的对象年龄,默认15, CMS默认是4
-XX:HandlePromotionFailure 老年代担保
-XX:+UseAdaptiveSizePolicy动态调整Java堆中各个区域大小和进入老年代年龄
-XX:ParallelGCThreads 并行回收的线程数
-XX:MaxGCPauseMillis Parallel Scavenge参数,设置GC的最大停顿时间
-XX:GCTimeRatio Parallel Scavenge参数,GC时间占总时间的比率,默认99%,即1%的GC时间
-XX:CMSInitiatingOccupancyFraction,old区触发cms阈值,默认68%
-XX:+UseCMSCompactAtFullCollection(CMS完成后是否进行一次碎片整理,停顿时间加长)
-XX:CMSFullGCsBeforeCompaction(执行多少次不进行碎片整理的FullGC后进行一次带压缩的)
-XX:+ScavengeBeforeFullGC,在fullgc前触发一次minorGC
垃圾回收统计信息
-XX:+PrintGC 输出GC日志
-verbose:gc等同于上面那个
-XX:+PrintGCDetails 输出GC的详细日志
堆大小设置
-Xmx:最大堆大小
-Xms:初始堆大小(最小内存值)
-Xmn:年轻代大小
-XX:NewSize和-XX:MaxNewSize 新生代大小
-XX:SurvivorRatio:3 意思是年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-Xss栈容量 默认256k
-XX:PermSize永久代初始值
-XX:MaxPermSize 永久代最大值
进程是资源分配的基本单位,线程是调度的基本单位
虚拟机栈和本地方法栈为什么是线程私有的?
- 虚拟机栈:每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
java对象的创建过程
下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。
Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
Step2:分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式:(补充内容,需要掌握)
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
内存分配并发问题(补充内容,需要掌握)
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
- CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
- TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配
Step3:初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
Step4:设置对象头
初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
Step5:执行 init 方法
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
对象访问定位的两种方式
java对象在访问的时候,我们需要通过java虚拟机栈的reference类型的数据去操作具体的对象。由于reference类型在java虚拟机规范中只规定了一个对象的引用,并没有定义这个这个引用应该通过那种方式去定位、访问java堆中的具体对象实例,所以一般的访问方式也是取决与java虚拟机的类型。目前主流的访问方式有通过句柄和直接指针两种方式。
- 句柄访问
使用句柄访问方式,java堆将会划分出来一部分内存去来作为句柄池,reference中存储的就是对象的句柄地址。而句柄中则包含对象实例数据的地址和对象类型数据(如对象的类型,实现的接口、方法、父类、field等)的具体地址信息。下边我以一个例子来简单的说明一下:
Object obj = new Object();
Object obj表示一个本地引用,存储在java栈的本地便变量表中,表示一个reference类型的数据。
new Object()作为实例对象存放在java堆中,同时java堆中还存储了Object类的信息(对象类型、实现接口、方法等)的具体地址信息,这些地址信息所执行的数据类型存储在方法区中。
- 直接指针访问
如果使用指针访问,那么java堆对象的布局中就必须考虑如何放置访问类型的相关信息(如对象的类型,实现的接口、方法、父类、field等),而reference中存储的就是对象的地址。
这两种访问方式各有利弊,使用句柄访最大的好处是reference中存储着稳定的句柄地址,当对象移动之后(垃圾收集时移动对象是非常普遍的行为),只需要改变句柄中的对象实例地址即可,reference不用修改。
使用指针访问的好处是访问速度快,它减少了一次指针定位的时间开销,由于java是面向对象的语言,在开发中java对象的访问非常的频繁,因此这类开销积少成多也是非常可观的,反之则提升访问速度。
finalize() 方法什么时候被调用?析构函数 (finalization) 的目的是什么?
垃圾回收器(garbage colector)决定回收某对象时,就会运行该对象的 finalize() 方法 但是在 Java 中很不幸,如果内存总是
充足的,那么垃圾回收可能永远不会进行,也就是说 filalize() 可能永远不被执行,显然指望它做收尾工作是靠不住的。 那么
finalize() 究竟是做什么的呢? 它最主要的用途是回收特殊渠道申请的内存。Java 程序有垃圾回收器,所以一般情况下内存问
题不用程序员操心。但有一种 JNI(Java Native Interface)调用non-Java 程序(C 或 C++), finalize() 的工作就是回收这部
分的内存。
简述 Java 内存分配与回收策率以及 Minor GC 和 Major GC
• 对象优先在堆的 Eden 区分配
• 大对象直接进入老年代
• 长期存活的对象将直接进入老年代
当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次 Minor GC。Minor GC 通常发生在新生代的 Eden 区,在这个区的
对象生存期短,往往发生 Gc 的频率较高,回收速度比较快;Full GC/Major GC 发生在老年代,一般情况下,触发老年代 GC
的时候不会触发 Minor GC,但是通过配置,可以在 Full GC 之前进行一次 Minor GC 这样可以加快老年代的回收速度。