ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 1. 字节码 ## 1.1 Java 为什么跨平台? 先将java文件编译成**字节码(.class)文件**, 在所有的平台上生成的字节码文件都是相同的。 再**使用Java虚拟机运行字节码文件**。因为**不同操作系统有对应版本的jvm**, 这使得同一个java代码文件可以在不同的平台上运行。 ## 1.2 JVM 的生命周期? ### 虚拟机的启动 Java 虚拟机的启动是通过引导类加载器(bootstrap class loader) 场景一个初始类(initial class) 来完成的 , 这个类是由虚拟机的具体实现指定的. ### 虚拟机的退出有如下几种情况 * 某线程调用Runtime类或System类的exit方法, 或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作。 * 程序正常执行结束 * 程序在执行过程中遇到了异常或错误而异常终止 * 由于操作系统出现错误而导致Java虚拟机进程终止 ## 1.3 JVM 的组成? (虚拟机的体系结构?) 1. 类加载器 2. 运行时数据区 3. 执行引擎 ![](https://img.kancloud.cn/88/2e/882eb2e4b529aa785962f9e2888253f9_992x849.png) 这个架构可以分成三层看: * 最上层:javac编译器将编译好的字节码class文件,通过java 类装载器执行机制,把对象或class文件存放在 jvm划分内存区域。 * 中间层:称为Runtime Data Area,主要是在Java代码运行时用于存放数据的,从左至右为方法区(永久代、元数据区)、堆(共享,GC回收对象区域)、栈、程序计数器、寄存器、本地方法栈(私有)。 * 最下层:解释器、JIT(just in time)编译器和 GC(Garbage Collection,垃圾回收器) ## 1.4 什么是字节码的指令?(虚拟机指令?) 字节码是一种二进制的类文件,其指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。 ## 1.5 字节码(.class) 文件结构 * 魔数 (用来标识文件类型的) * class文件版本 (jdk的版本信息) * 常量池计数器 和 常量池表数据 * 访问标识(或标志) (public final 等等信息 ) * 类索引、父类索引、接口索引集合 (继承那个类, 实现哪些接口) * 字段表集合 * 方法表集合 * 属性表集合 ## 1.6 基本类型为什么不存到堆中而是存到栈中? 栈空间相对小, 运算速度更快, 基本类型占用空间小更适合放到栈中 # 2. 类的加载 ## 2.1 类加载过程?(生命周期?加载 .class文件的原理机制?) 1. 加载 (Loading) 根据类的路径找到相对应的calss文件,然后导入。 2. 链接(Linking) 1. 验证(Verification) 检查待加载的class文件的正确性 2. 准备(Preparation) 给类中的静态变量分配存储空间 3. 解析(Resolution) 将符号引用转换成直接引用 4. 初始化(Initialization) 对静态变量和静态代码块执行初始化工作 ![](https://img.kancloud.cn/65/57/65574732c4c9c821221aadb53d5f355a_735x241.png) ![](https://img.kancloud.cn/b3/60/b36006f64d5cac0ecb3eb3c12edea5a5_598x476.png) ### 一、加载: * 通过一个类的全限定名获取定义此类的二进制字节流 * 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 * 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口 注意:数组类是如何创建加载的呢? ### 二、链接: * 验证(Verify): * 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性, 不会危害虚拟机自身安全。 * 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。 * 准备(Prepare): * 为类变量分配内存并且设置该类变量的默认初始值,即零值。 * 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化; * 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。 * 解析(Resolve): * 将常量池内的符号引用转换为直接引用的过程。 * 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。 在解析阶段,jvm根据字符串的内容找到内存区域中相应的地址,然后把符号引用替换成直接指向目标的指针、句柄、偏移量等,这些直接指向目标的指针、句柄、偏移量就被成为直接引用。 * 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT\_Class\_info、CONSTANT\_Fieldref\_info、CONSTANT\_Methodref\_info等。 ### 三、初始化: * 初始化阶段就是执行类构造器方法()的过程。 * 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。 * 构造器方法中指令按语句在源文件中出现的顺序执行。 * ()不同于类的构造器。(关联:构造器是虚拟机视角下的()) * 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。 * 虚拟机必须保证一个类的()方法在多线程下被同步加锁。 ## 2.2 什么是类加载器?(简述下类加载器?) 类加载器就是用来加载字节码文件(.class)的类,其实质是把类文件从硬盘读取到内存中! ## 2.3 类加载器有哪些? (类加载器的分类?) * 引导类加载器:Bootstrap ClassLoader,用来加载Java核心库,比如rt.jar、java/javax/sun开头的类, 并且负责加载 自定义类加载器。使用C++编写 * 自定义类加载器(其他类加载器):扩展类加载器、系统类加载器、开发人员自定义的。主要是Java语言编写,顶级父类是 ClassLoader 也有分成 * 启动类加载器:BootstrapClassLoader * 扩展类加载器:ExtentionClassLoader * 应用类加载器:AppClassLoader (也叫做“系统类加载器”)开发人员自定义的也在这里 ![](https://img.kancloud.cn/96/fe/96fe784c7554501e0472fbf43a504878_658x469.png) ## 2.4 父类加载器和子类加载器的关系? 两者不是子父类的那样的继承关系,而是包含关系 ## 2.5 为什么要自定义类加载器 1. 隔离加载类 : 如tomcat 就内部有自定义的类加载器用来隔离同一个服务器上的不同应用程序 2. 修改类加载方式 : 类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载 3. 扩展加载源:比如从数据库、网络 4. 防止源码泄露:比如对字节码加密,在加载时去解密 ## 2.6 双亲委派机制? 双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,一层层往上找,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。如果找不到抛出ClassNotFoundException异常。 ### 工作原理 首先判断被加载的类是否已经加载过,如果是则结束。 1. 如果一个类加载器收到了类加载请求,它并不会自己去加载,而是把这个请求委托给父类的加载器去执行; 2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器; 3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,再一层层子加载器去加载,最终找不到,就会抛出一个异常:ClassNotFoundException。。 **先是子类一层层往上委托,再父类一层层往下开始加载(最上层的父类能加载就加载,不能就让其子类加载)。** ## 2.7 双亲委派机制的作用?(好处?) * **保护程序安全,防止核心API被随意篡改**。借助父亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成的,从而确保了Java应用所使用的都是同一个版本的Java核心类库,他们之间是互相兼容的。 * **避免类的重复加载。**通过委托去向上加载,如果已经有加载过,就不需要再加载了。 ## 2.8 双亲委派机制的缺点 子能使用父, 父不能使用子 父类加载器不能使用子类加载器的应用实例 # 3. 运行时内存 ***** ## 3.1 说一下 JVM 内存分区?(JVM 内存模型?)每个区放什么?(每个区作用?) 1. **方法区** 线程**共享**,它用于存储已被虚拟机加载的**类的信息(类的名称、字段信息、方法信息)、常量池、静态变量、即时编译器编译后的代码缓存**等,1.7 和 1.8 把**常量池、静态变量都放到堆**中了。 2. **堆 **线程**共享**,主要存储引用类型的数据 (对象实例) 3. **虚拟机栈** 线程**私有**,存的是**基本数据类型和堆中对象的引用**。内存结构是一个个栈帧,一个栈帧对应一个方法。栈帧主要包含 * 局部变量表(Local Variables) * 操作数栈(Operand stack) (或表达式栈) * 动态链接(Dynamic Linking) (或指向运行时常量池的方法引用) * 方法返回地址(Return Address) (或方法正常退出或者异常退出的定义) 4. **程序计数器** 线程**私有**,任何时间一个线程都只有一个方法在执行,程序计数器会存储当前线程正在执行的Java方法的JVM字节码指令地址; 5. **本地方法栈** 线程**私有**,用于管理本地方法的调用。 ![](https://img.kancloud.cn/a8/40/a840de7a52dfcb14c2359785f7aabeb5_665x343.png) ![](https://img.kancloud.cn/90/22/9022bd2a37fd0dd86a133c0306908e0b_343x526.png) ## 3.2 堆和栈的区别? 1. 栈空间很小,运行速度快,主要存放基本类型数据和存储在堆中的引用类型数据的调用地址、部分结果、并参与方法的调用和返回 2. 堆空间大, 运行较慢,主要存放引用类型的数据 3. 栈不存在GC,堆存在GC 4. 栈管运行,堆管存储 ## 3.3 栈溢出? * 递归的时候,压栈远远超过出栈就可能会内存溢出 * 栈中局部变量表数据大导致栈帧过多就可能也出现内存溢出 ## 3.4 堆的内存结构? * 新生代 * 伊甸园(Eden) * Survivor 0 (From区) /sərˈvaɪvər/ * Survivor 1 (To区) * 老年代 ## 3.5 为什么有新生代和老年代?(为什么分代?)新生代、老年代的比例如何? 不同对象的生命周期不同,大量对象是生命周期短的临时对象,把生命周期短的放到新生代,生命周期长的放到老年代,以优化GC性能。 默认新生代和老年代的比例:1:2 \-XX:NewRatio=2 表示新生代占1,老年代占2,新生代占整个堆的1/3 ## 3.6 为什么新生代分为 Eden 和 Survivor?Eden 和 Survivor 的比例分配? 伊甸园 (Eden)主要是用来创建对象 幸存者(Survivor)是因为老年代的对象一般都是生命周期长,老年代内存也大,GC回收的效率低,在伊甸园和老年代增加筛选过滤,防止不必要的对象送往老年代。 默认新生代的Eden、S0、S1的比例是 8:1:1 \-XX:SurvivorRatio=8 JDK9及以上版本中默认使用的G1 垃圾收集器,会自动调整大小,这种设置就无效了 ## 3.7 为什么有俩个 survivor 区? 解决内存空间碎片化,新生代GC时是使用的复制算法,使用两个 survivor 实现复制,以使伊甸园创建的对象和其中一个survivor的对象复制到空的survivor 区。 复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续 的内存空间,避免了碎片化的发生。 当新生代的 Survivor 分区为 2 个的时候,不论是空间利用率还是程序运行的效率都是最优的,所以这也是为什么 Survivor 分区是 2 个的原因了。 ## 3.8 什么时候对象会进入老年代? 新生代有GC最大次数, 超过这个次数,就会放到老年代了,默认 15 -XX: MaxTenuringThreshold=15 大对象直接进入老年代:设置了-XX:PretenureSizeThreshold这个参数,那么如果你要创建的对象大于这个参数的值,比如分配一个超大的字节数组,此时就直接把这个大对象放入到老年代,不会经过新生代。 长期存活的对象直接进入老年代 ## 3.9 为什么幸存者区15次进入老年代,原理是啥?对象如何晋升到老年代? 对象头里的Mark Word(标记字),对象的分代年龄占4位,也就是0000,最大值为1111也就是最大为15。 ![](https://img.kancloud.cn/05/92/05929707ea08417803772b6292decf86_629x83.png) ## 3.10 初始堆大小和最大堆大小一样,问这样有什么好处? 为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能 ## 3.11 JVM中最大堆大小有没有限制? 系统的可用物理内存限制,默认最大值为物理内存的1/4 ## 3.12 什么是空间分配担保策略? HandlePromotionFailure 已经没用了 JDK 6 Update 24之后的规则变为老年代的连续空间大于 新生代对象总大小或者历次晋升的平均大小就会进行Minor GC, 否则将进行FullGC. /fʊl/ adj. 满的,满是……的; ## 3.13 新生代和老年代的内存回收策略? * 对象优先在堆的 Eden 区分配 * 大对象直接进入老年代 * 长期存活的对象将直接进入老年代 ## 3.14 对象内存分配过程 以对象都先分配到 伊甸园 区举例 Minor GC 或者 YGC 是新生代的GC /ˈmaɪnər/ n. (Minor)(美)迈纳(人名) 1. 对象先在伊甸园区创建完成,当伊甸园内存不足时,触发GC,将可回收的回收,剩下的放到空的幸存者区(Survivor)并记录年龄为1 表示已经经过一次GC。Survivor 共有俩个区 2. 再继续创建对象时,伊甸园内存不足再次触发GC,将可回收的回收,剩下的放到空的幸存者区(Survivor)并记录年龄为1,同时上一步中的Survivor 也 进行GC,存活的对象年龄增1放到这一个Survivor里 每次GC 伊甸园区和其中一个幸存者区都会清空 3. 上面两步这样反复循环,当有对象在GC时,年龄超过了设置得上限就会放入到老年代 ## 3.15 内存分配原则: 针对不同年龄段的对象分配原则如下所示: * 优先分配到Eder * 大对象直接分配到老年代,尽量避免程序中出现过多的大对象 * 长期存活的对象分配到老年代 * 动态对象年龄判断,如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到设置得上限( MaxTenuringThreshold) 要求的年龄。 * 空间分配担保 -XX: HandlePromotionFailure ## 新生代的垃圾回收(Minor GC)什么时候触发? * 新生代内存不足会自动触发 * 老年代GC 会先执行一次新生代的GC * 全堆GC(Full GC )也会触发新生代的GC 1. 对象先在伊甸园区创建完成,当伊甸园内存不足时,触发GC,将可回收的回收,剩下的放到空的幸存者区(Survivor)并记录年龄为1 表示已经经过一次GC。Survivor 共有俩个区 2. 再继续创建对象时,伊甸园内存不足再次触发GC,将可回收的回收,剩下的放到空的幸存者区(Survivor)并记录年龄为1,同时上一步中的Survivor 也 进行GC,存活的对象年龄增1放到这一个Survivor里 每次GC 伊甸园区和其中一个幸存者区都会清空 3. 上面两步这样反复循环,当有对象在GC时,年龄超过了设置得上限就会放入到老年代 ## 老年代的垃圾回收(Major GC)什么时候触发?自动触发的阈值是多少? 老年代空间不足 ## 什么时候发生 Full GC ?Full GC 的过程? 清理整个堆空间—包括年轻代、老年代、元空间 * 老年代空间不足,这个很简单,就是字面上的不足,例如:大对象不停的直接进入老年代,最终造成空间不足。 * 方法区空间不足。 * 空间分配担保策略会触发Full GC , 老年代的连续空间小于新生代对象总大小或者历次晋升的平均大小就会进行整堆的GC ( Full GC) * 调用System.gc ()时,系统建议执行Full Gc,但是不必然执行 ## 描述JVM 堆的一次完整的GC流程? ## 什么是 TLAB?为什么有TLAB? JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。 ## 方法区的字符串常量池(StringTable) 为什么要移到堆中? 字符串也是经常创建,在方法区时,由于方法区GC频率太低效果也不好,字符串被大量创建而没有有效的释放掉,浪费内存影响性能。放到堆中能及时回收释放内存。 ## JVM的永久代中会发生垃圾回收么? 方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。 ## 几种主要的JVM参数? 设置栈的大小:-Xss1024k 设置栈的大小为 1024k(默认也是这么大), 栈过大会导致系统可以用于创建线程的数量变少。 新生代GC最大次数:-XX: MaxTenuringThreshold=15 默认 15 堆的起始内存 -Xms 1G 堆的最大内存 -Xmx 1G ## 方法区jdk版本间的区别 JDK8 以前, 把方法区的叫做永久代。JDK8 开始叫做元空间。 元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。JDK7 开始, 永久代就开始在修改,其中的 字符串常量池,静态变量都转移到了堆中 ## 在Java中,什么是是栈的起始点,同是也是程序的起始点? main方法 ## Java中的参数传递时传值呢?还是传引用? 基本数据类型及其封装类,String传的是值。 其它的引用数据类型(对象+数组)传递的是引用。 ## Java中有没有指针的概念? java中不说指针,说的是引用。引用指向堆中的对象实例。引用也是占内存的。 ## 一个空Object对象的占多大空间? 16字节(byte)(8字节引用 + 8字节对象(64位虚拟机的对象头是64bit, 包括哈希码,垃圾回收分代年龄,锁标记位,类信息引用)) # 4. 对象内存布局 ## 4.1 创建对象的几种方式? 1. 用new关键字创建对象 F f = new F(); 2. 使用反射机制创建对象 使用Class类里的newInstance()方法,权限必须是public,调用的是无参构造方法,使用java.lang.reflect.Constructor类里的newInstance方法,调用的是有参构造方法,无权限要求。 3. 通过object类的clone方法 不调用任何构造器,当前类需要实现Cloneable接口,实现clone(),默认浅拷贝。 4. 使用反序列化 5. 一些第三方库可以使用asm字节码技术动态生产 Constructor 对象 /kənˈstrʌktər/ n. 构造器;构造方法;构造函数 ## 4.2 创建对象的步骤?(new 对象的流程?) 1. 创建对象对应的类是否加载、链接、初始化了。遇到new 指令,检查这个指令的参数是否在元空间(Metaspace) 中的常量池中存在对应符号引用,并且检查符号引用对应的类是否加载初始化完成。即判断类元信息是否存在 * 1. 如果没有,那么在双亲委派模式下,使用当前的类加载器以 ClassLoader + 包名 + 类名为Key 去查找对应的 .class 文件 * 2. 如果没有找到文件,则抛出 ClassNotFoundException 异常 * 3. 如果找到,则进行类加载流程并生成对应的 Class 类对象 * 2. 为对象分配内存。 计算对象占用的空间大小,在堆中划分一块内存给新对象 * 1. 指针碰撞。 内存规整时,已经使用的在一边,未使用的在另一边,中间是一个指针,作为分界点。这时分配一块内存的操作就是将指针向未使用的一边移动与对象大小相同的距离,这就是指针碰撞。 * 2. 空闲列表。内存不规整时,也就是有内存碎片时,已经使用的和未被使用的混杂在一起没法使用指针碰撞需要用到空闲列表。jvm维护一个列表,记录哪些是未被使用的。在分配的时候,需要从列表中找出一块足够大的内存块划分给对象,并更新列表的记录。这就是空闲列表。 3. 处理并发安全问题。 * 1. CAS ( Compare And |Swap ) 失败重试、区域加锁:保证指针更新操作的原子性; * 2. TLAB把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区 4. 初始化分配到的空间。 内存分配结束,虚拟机将分配到的内存空间都初始化为默认值(不包括对象头)。 比如 int 类型赋值为 0 5 设置对象的对象头。 将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。 6. 执行init方法进行初始化。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量 ## 4.3 对象的内存布局? 对象分为 对象头(Header)、实例数据(Instance Data)、对齐填充(Padding) ## 4.4 对象头的内容? 1\. 运行时元数据:普通对象-》 * 哈希值(对象在堆中的引用地址,toString 显示的那个 @xxx )、 * GC分代年龄 * 锁状态标志,在同步中判断该对象是否是锁 * 线程持有的锁 * 线程偏向ID * 偏向时间戳 数组还会有长度,普通对象的长度在对象元数据中 2. 类型指针:连接类在方法区中类元数据的引用 3. 实例数据 它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段、所有父级(父类,父类的父类......)的实例数据(父类私有的也会在这里只是不能访问) 相同宽度的字段总是被分配在一起,父类中定义的变量会出现在子类之前(因为父类的加载是优先于子类加载的) ## 4.5 对象的访问定位? 使用直接指针访问(Hotspot的方式) 栈中存放指向堆中的对象引用,而对象中又通过类型指针连接类在方法区中元数据 # 5. 执行引擎 ## 5.1 执行引擎的作用? 执行引擎是Java虚拟机核心的组成部分之一。 执行引擎的核心功能就是将字节码指令解释/编译为对应平台上的本地机器指令以实现Java程序的运行。 # 6. 垃圾回收 ***** ## 6.1 GC是什么? 在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。 ## 6.2 为什么要有GC? 核心就是内存不足的问题 如果不及时对内存中的垃圾进行清理,那么垃圾会一直占用着内存直到程序结束,这会导致内存越来越少,甚至内存溢出。 ## 6.3 GC回收的是哪部分的垃圾(回收的重点区域)? GC只存在于堆和方法区,其中堆是垃圾回收的核心。频繁的对新生代回收,较少对老年代回收,极少进行全堆或方法区的回收。 ## 6.4 用什么方法判断对象是否死亡? (如何判断一个对象是否存活?) 根据对象是否被引用来判断。当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡 Java使用 可达性分析算法 来判断 ## 6.5 可达性分析算法? 1. 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。 2. 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Rererence Chain)。 3. 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。 4. 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。 优点:实现简单,执行高效,有效的解决循环引用的问题,降低内存泄漏的风险。 为保证一致性可达性算法在判断内存是否可回收时需要对其他线程做暂停处理(Stop The World )( stw) ## 6.6 GC Roots有哪些? 主要包含的几类 * 虚拟机树中引用的对象,比如:各个线程被调用的方法中使用到的参数、局部变量等。 * 类静态属性引用的对象,比如:Java类的引用类型静态变量 * 方法区中常量引用的对象,比如:字符串常量池(string Table)里的引用 * 所有被同步锁synchronized持有的对象 * Java虚拟机内部的引用。基本数据类型对应的Class对象,一些常驻的异常对象(如: NullPointerExceptionoutofMemoryError) ,系统类加载器。 * 反映java虚拟机内部情况的JMXBean,JVMTI中注册的回调、本地代码缓存等。 **由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不·存放在堆内存里面,那它就是一个Root 现在静态变量也放到堆里面了,以前不在堆里** ## 6.7 引用计数算法 Java未使用 原理:对于一个对象A,只要有任何一个对象引用了A ,则A的引用计数器就加1,当引用失效时, 引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。 Java未使用,引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。 ## 6.8 GC的三种收集方法:标记清除、标记整理(标记压缩)、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路? 1. 标记-清除算法,当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行标记和清除。标记的是可达对象(非垃圾),清除的是未标记的对象 * 标记: 从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。 * 清除:对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。这里的清除只是把对象地址保存到空闲列表中,下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放。 * 优点:实现简单,不移动对象,与保守式GC算法兼容 * 缺点:效率比较低:递归与全堆对象遍历两次。清理出来的空闲内存是不连续的,产生内存碎片。GC的时候,需要停止整个应用程序,导致用户体验差 stop the world STW ## 2 复制算法 将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。适合存活对象少、垃圾对象多的场景。 * 优点:没有标记和清除过程,实现简单,在存活对象少、垃圾对象多的前提下的运行高效。复制过去以后保证空间的连续性,不会出现“内存碎片”问题 * 缺点:核心缺点是内存占用大。GC的时候,需要停止整个应用程序,导致用户体验差 stop the world STW ## 3. 标记-压缩(标记整理)算法, * 标记:和标记清除算法一样,从根节点开始标记所有被引用对象。一般是在对象的Header中记录为可达对象。 * 压缩(整理):将所有的存活对象压缩移动到内存的一端,按顺序排放。之后, 清理边界外所有的空间。 * 优点:消除了标记/清除算法当中,内存区域分散的缺点(解决了内存碎片问题),我们需要给新对象分配内存时, JVM只需要持有一个内存的起始地址即可。消除了复制算法当中,内存减半的高额代价。 * 缺点: * 从效率上来说,标记-压缩算法要低于复制算法 * 效率不高,不仅要标记所有在活对象,还要整理所有存活对象的引用地址。 * 对于老年代每次都有大量对象存适的区域来说,极为负重。 * 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。 * GC的时候,需要停止整个应用程序,导致用户体验差 ![](https://img.kancloud.cn/85/26/8526f90f5eebe65b34bcb32770a8df9d_1028x118.png) ## 6.9 JVM的垃圾回收为什么采用分代GC? 不同的对象的生命周期是不一样的。因此,不同生命周期的对象,可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。 ## 6.10 分代垃圾回收过程? 新生代: 大部分可回收的对象都是在新生代, 这里的对象生命周期短、存适率低、回收频繁,使用的是复制算法(因为很多是可回收对象,所以需要复制的对象少) 老年代: 区域较大,对象生命周期长、存活率高、回收不及新生代频繁。这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记一整理的混合实现。 标记(Mark)阶段的开销与存活对象的数量成正比。 清除(Sweep)阶段的开销与所管理区域的大小成正相关。 压缩(Compact)阶段的开销与存活对象的数据成正比。 ## 6.11 什么是内存泄漏和什么是内存溢出?区别是? ### 内存溢出: * Java 虚拟机的堆内存设置不合理,设置得过小 * 代码中创建了大量在用的对象,长时间不能被垃圾收集器收集 ### 内存泄漏: 是垃圾但是没有回收掉。 1. **静态集合类**,生命周期和JVM程序一致。长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。 2. **单例模式**,因为单例的静态特性,它的生命周期和JVM程序一致。所以如果单例对象如果持有外部对象的引用,那么这个外部对象也不会被回收,那么就会造成内存泄漏。 3. **内部类持有外部类**,如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有,外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。 4. **各种连接,如数据库连接、网络连接和IO连接等**。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection,Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。 5. **变量不合理的作用域**,一个变量的定义的作用范围大于其使用范围,很有可能,会造成内存泄漏。另一方面,如果没有及时地把对象设置为nul1,很有可能导致内存泄漏的发生。 6. **改变哈希值** 7. **缓存泄漏** * 内存泄漏的另一个常见来源是缓存,一旦你把对象引用放入到缓存中,他就很容易遗忘。比如:之前项目在一次上线的时候,应用启动奇慢直到夯死,就是因为代码中会加载一个表中的数据到缓存(内存)中,测试环境只有几百条数据,但是生产环境有几百万的数据。 * 可以使用 WeakHashMap (弱引用),此种Map的特点是,当除了自身有对 ,key的引用外,此key没有其他引用那么此map会自动丢弃此值。 8. **监听器和回调** * 内存泄漏另一个常见来源是监听器和其他回调,如果客户端在你实现的API中注册回调,却没有显式的取消,那么就会积聚。 ## 6.12 什么是 STW(Stop the world)? 指的是GC事件发生过程中, 为解决一致性会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。 * 可达性分析算法中枚举根节点(GC Roots) 会导致所有Java执行线程停顿。 * 被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。 * 所有的GC都有这个事件,和采用哪种GC无关,有的是以增加STW频次来减少单次STW的时间。 * STW是JVM在后台GC时自动发起和自动完成的。 ## 6.12 强引用、软引用、弱引用、虚引用的区别? 强引用:不回收 软引用:内存不足即回收区 一般用来实现内存敏感的缓存 比如 高速缓存 弱引用:发现即回收 一般用来实现可有可无的缓存 WeakHashMap 虚引用:对象回收跟踪 ## 6.13 你开发中使用过weakHashMap吗? 弱引用,此种Map的特点是,当除了自身有对 ,key的引用外,此key没有其他引用那么此map会自动丢弃此值。 ## 6.14 System.gc()的作用? 提醒jvm的垃圾回收器执行Full GC,但是不确定是否马上执行GC。 这个方法内部调用 Runtime .getRuntime ().gc(); 所以作用一样。 ## 6.15 finalize() 方法详解? 主要作用是在对象回收前做一些(Socket,文件加载)资源释放操作。 finalize()是object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在首次回收已不可达的对象之前调用对象的该方法。如果首次GC没回收掉,下次也不会在调用。 ## 6.16 垃圾回收器有哪些?都有哪些算法来实现?项目中用的垃圾回收器是什么? 重点 CMS Parallel G1 ### 1. Serial 收集器 新生代收集器 Serial收集器是最基本的、发展历史最悠久的收集器。新生代收集器,目前不怎么用 特点:单线程、简单高效(与其他收集器的单线程相比),使用复制算法。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。 应用场景:适用于Client模式下的虚拟机。单核服务器。 可以用 -XX:+UserSerialGC 来选择 Serial 作为新生代收集器。 ![](https://img.kancloud.cn/b7/b5/b7b50300549ed326601687789a090df7_684x222.png) ### 2. Serial Old 收集器 老年代 Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”(Mark-Compact)算法。 此收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途: * 在JDK1.5 以及之前版本(Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。 * 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。 ### 3. ParNew 收集器 新生代收集器 Serial 的多线程版本。其他和 Servial 区别不大 ![](https://img.kancloud.cn/f2/27/f2277203deec1e7c8f2d95814412e549_698x245.png) ### 4. Parallel Scavenge 收集器 新生代 吞吐量优先 jdk8 默认使用 Parallel Scavenge 也是一款用于新生代的多线程收集器,吞吐量优先 会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种方式称为GC自适应的调节策略。 另外值得注意的一点是,Parallel Scavenge收集器无法与CMS收集器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代选择Parallel Scavenge收集器,老年代只有Serial Old收集器能与之配合使用。 ![](https://img.kancloud.cn/3f/24/3f2409ceb5f6efab54e3ee4b8c9ccffa_691x225.png) ### 5. Parallel Old收集器 老年代 Parallel Old收集器是Parallel Scavenge收集器的老年代版本, 一个多线程收集器,jdk8 默认使用,采用标记-整理算法。可以与 Parallel Scavenge 收集器搭配,可以充分利用多核 CPU 的计算能力。 ### 6. CMS 老年代 (Concurrent Mark Sweep) 低延迟,一种以获取最短回收停顿时间为目标的收集器。 新生代只能选择ParNew或者Serial收集器中的一个 特点:基于标记-清除算法实现。并发收集、低停顿。 应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。 初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。 并发标记:进行GC Roots 追踪 的过程,找出存活对象且用户线程可并发执行。 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。 并发清除:对标记的对象进行清除回收。 CMS收集器的内存回收过程除了初始标记,其他都是与用户线程一起并发执行的。 CMS的优点: * 并发收集 * 低延迟 CMS收集器的缺点: * 对CPU资源非常敏感。其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。 * 无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。 * 因为采用标记-清除算法所以会存在内存碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。 * 因为是并行执行的,没法使用 标记-压缩算法(标记压缩需要移动对象,而GC线程和用户线程同时在用,没法移动) ![](https://img.kancloud.cn/fa/2d/fa2d76f7ea0034157b884a72186f6957_683x204.png) ### 7. G1(Garbage-First) 一款面向服务端应用的垃圾收集器,兼顾吞吐量和停顿时间。在JDK1.7版本正式启用,是JDK 9以后的默认GC选项,取代了CMS回收器。 特点: * **并行与并发**:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。 * ** 分代收集**:G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。 * **空间整合**:G1将内存划分为一个个的区(region)。内存的回收是以区(region)作为基本单位的。区(region)之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)·算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候, G1的优势更加明显。 * **可预测的停顿**:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。 **G1为什么能建立可预测的停顿时间模型?** 因为它有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个区(region)里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的区(region)。这样就保证了在有限的时间内可以获取尽可能高的收集效率。 **G1与其他收集器的区别:** 其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。 **G1收集器存在的问题:** Region不可能是孤立的,分配在Region中的对象可以与Java堆中的任意对象发生引用关系。在采用可达性分析算法来判断对象是否存活时,得扫描整个Java堆才能保证准确性。其他收集器也存在这种问题(G1更加突出而已)。会导致Minor GC效率下降。 **G1收集器是如何解决上述问题的?** 采用Remembered Set来避免整堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用对象是否处于多个Region中(即检查老年代中是否引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆进行扫描也不会有遗漏。 如果不计算维护 Remembered Set 的操作,G1收集器大致可分为如下步骤: **初始标记**:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。) **并发标记**:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行) **最终标记**:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set  Logs里面,把Remembered Set  Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。) **筛选回收**:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行) 适用场景:要求尽可能可控 GC 停顿时间;具有大内存、多处理器的机器。可以用 -XX:+UseG1GC 使用 G1 收集器,jdk9 默认使用 G1 收集器。 ![](https://img.kancloud.cn/b4/82/b482350444d8b207d7f947426e3f69da_706x225.png) ## 6.17 请问吞吐量的优化和响应优先的垃圾收集器是如何选择的呢? ## GC的优点和原理(机制)? 就是把 什么是GC, 回收的重点区域,GC判断垃圾的方式,GC常用算法, 垃圾收集器, # 7. 调优 常用的性能优化方式有哪些?(JVM调优策略?) 栈溢出导致的原因?如何解决? JVM相关的分析工具使用过的有哪些?具体的性能调优步骤如何