jvm
1. 什么是JVM?
- JVM,Java虚拟机,Java实现跨平台的基石。Java 程序运行的时候,编译器会将 Java 源代码(.java)编译成平台无关的 Java 字节码文件(.class),接下来对应平台的 JVM 会对字节码文件进行解释,翻译成对应平台的机器指令并运行。
2. Jvm主要组成部分及其作用?
类加载器(ClassLoader):负责从文件系统、网络或其他来源加载Class文件,将Class文件中的二进制数据读入到内存中
运行时数据区(Runtime Data Area)JVM 在执行 Java 程序时,需要在内存中分配空间来处理各种数据,这些内存区域主要包括方法区、堆、栈、程序计数器和本地方法栈。
执行引擎(Execution Engine)负责执行class文件中包含的字节码指令,包括一个虚拟处理器,还包括即时编译器(JIT Compiler)和垃圾回收器(Garbage Collector)。
本地库接口(Native Interface)调用C或C++实现的本地方法的代码返回结果
各组件的作用:首先通过类加载器(ClassLoader)把Java代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,由特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由CPU执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能
3. 谈谈对运行时数据区的理解?
- 程序计数器(Program Counter Register)(线程私有) 当前线程所执行的字节码的行号指示器
- java虚拟机栈(Java Virtual Machine Statcks)(线程私有)java方法执行的内存模型,生命周期与线程相同。当线程执行一个方法时,会创建一个对应的栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,然后栈帧会被压入栈中。当方法执行完毕后,栈帧会从栈中移除。
- 本地方法栈(Native Method Stack)(线程私有) 执行虚拟机使用到的native方法服务。存放了native方法的局部变量、动态链接和方法出口等信息
- java堆(Java Heap)(线程共享)主要用于存放对象实例,几乎所有的对象实例都在这分配内存
- 方法区(Method Area)和运行时常量池(Runtime Constant Pool)(线程共享)并不真实存在,属于Java 虚拟机规范中的一个逻辑概念,存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。在 HotSpot 虚拟机中,方法区的实现称为永久代(PermGen),在Jdk1.8后,已经被元空间(Metaspace)所替代。
4. Java内存堆和栈区别?
- 堆属于线程共享的内存区域,栈属于线程私有的内存区域
- 堆存储大部分对象,栈存储局部变量、方法参数、对象引用等
- 堆对象生命周期可以在方法调用结束后继续存在,直到不再被任何变量引用,然后被垃圾收集器回收。栈对象通常随着方法调用的结束而自动释放,不需要垃圾收集器处理
5. JDK678内存区域的变化
- JDK1.6 使用永久代实现方法区
- JDK1.7 时发生了一些变化,将字符串常量池、静态变量,存放在堆上
- 在 JDK1.8 时彻底干掉了永久代,而在直接内存中划出一块区域作为元空间,运行时常量池、类常量池都移动到元空间。
6. java对象(普通Java对象,不包括数组和Class对象等)创建过程?
- 1.类加载检查:虚拟机遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号的引用,以及这个符号引用代表的类是否已被加载,解析和初始化过,如果没有,则执行类加载过程
- 2.分配内存:为新生对象分配内存,内存大小在类加载完成后便可完全确定。为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来。有2种分配方法
- 3.初始化:将分配到的内存空间都初始化为零值(不包括对象头),可提前至TLAB分配时进行。保证了对象实例字段在java代码中不赋初始值就能访问到这些字段的数据类型所对应的零值
- 4.设置对象头信息:对对象进行设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息,对象的哈希码,对象GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)中
- 5.上面的工作完成后,从虚拟机的角度看,一个新的对象已经产生了,但从java程序的视角看还需要执行init方法。由字节码中是否跟随invokespecial指令所决定,执行new指令之后会接着执行init方法,把对象初始化,这样一个真正可用的对象才算完全生产出来。
7. 对象的销毁过程了解吗?
- 对象创建完成后,就可以通过引用来访问对象的方法和属性,当对象不再被任何引用指向时,对象就会变成垃圾。垃圾收集器会通过可达性分析算法判断对象是否存活,如果对象不可达,就会被回收。垃圾收集器会通过标记清除、标记复制、标记整理等算法来回收内存,将对象占用的内存空间释放出来。常用的垃圾收集器有 CMS、G1、ZGC 等,它们的回收策略和效率不同,可以根据具体的场景选择合适的垃圾收集器。
8. 什么是指针碰撞?什么是空闲列表?java内存分配方法
- 内存分配有两种方式,指针碰撞(Bump The Pointer)、空闲列表(Free List)。
- 指针碰撞(Bump the Pointer):假设堆内存是一个连续的空间,分为两个部分,一部分是已经被使用的内存,另一部分是未被使用的内存。在分配内存时,Java 虚拟机维护一个指针,指向下一个可用的内存地址,每次分配内存时,只需要将指针向后移动(碰撞)一段距离,然后将这段内存分配给对象实例即可。
- 空闲列表(Free List):如果java堆中的内存不连续,已使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,记录可用的内存块,从列表中找到一块足够大的空间分配给对象实例,并更新列表上的记录,这种分配方式称为空闲列表(Free List)
- 指针碰撞适用于管理简单、碎片化较少的内存区域(如年轻代),而空闲列表适用于内存碎片化较严重或对象大小差异较大的场景(如老年代)。
9. JVM里new对象时,堆会发生抢占吗?JVM 是怎么设计来保证线程安全的?
- 解决分配内存时线程不安全(多线程并发时正在给对象A分配内存,还没来得及修改指针,对象B又用这个指针分配内存)方案
- 方案1对分配内存空间动作进行同步处理,采用CAS配上失败重试的方式保证更新操作的原子性
- 方案2把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定,通过-XX:+/-UseTLAB参数使用TALB,
10. 对象在内存中的存储布局
- 对象在堆的内存布局是由 Java 虚拟机规范定义的,在 HotSpot 中可以划分为三个部分:
- 对象头(Object Header)存储堆对象的布局、类型、GC状态、同步状态和标识哈希码的基本信息。
- 实例数据(Instance Data)存储对象的数据信息,父类的信息,对象字段属性信息。
- 对齐填充(Padding)可选,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全,为了计算机高效寻址。Hotspot的自动内存管理系统要求对象的真实地址和大小必须是8字节整数倍。否则有可能出现跨缓存行的字段。效率低
- 对象头
- mark word存储对象自身的运行数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID等.在64位操作系统下占8个字节,-XX:+/-UseCompressedClassPointers //开启/关闭压缩类指针,-XX:+/-UseCompressedOops//开启/关闭压缩普通对象指针
- 锁标志位(lock):区分锁状态,11时表示对象待GC回收状态, 最后2位锁标识(11)有效
- biased_lock:是否偏向锁,由于无锁和偏向锁的锁标识都是01,没办法区分,引入一位的偏向锁标识位
- 分代年龄(age):表示对象被GC的次数,当次数到达阈值的时候,对象就会转移到老年代
- 对象的hashcode(hash):对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中
- 偏向锁的线程ID(JavaThread):偏向模式的时候,当某个线程持有偏向锁对象的时候,对象这里就会被置为该线程的ID。 后续操作无需再进行尝试获取锁的动作。
- epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
- ptr_to_lock_record:轻量级锁状态下,JVM通过CAS操作在对象的标题字中设置指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。
- 类型指针Klass Pointer,指向对象所属类的元数据的指针,用于确定这个对象是哪个类的实例。并不是所有虚拟机实现都必须在对象数据上保留类型指针,也就是查找对象的元数据信息不一定通过对象本身。在开启指针压缩的情况下占4个字节,否则占8个字节。java -XX:+PrintFlagsFinal -version | grep UseCompressedOops 命令来查看当前 JVM 是否开启了压缩指针。jdk8默认开启
- Length field如果对象是java数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小,但是从数组的元数据中无法确定数组的大小。占4个字节。
- mark word存储对象自身的运行数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID等.在64位操作系统下占8个字节,-XX:+/-UseCompressedClassPointers //开启/关闭压缩类指针,-XX:+/-UseCompressedOops//开启/关闭压缩普通对象指针
11. 一个java对象占用多大内存
在操作系统是64位的,并且JDK8中的压缩指针是默认开启的,因此new Object()的大小是 16 字节(12 字节的对象头(8字节markword+4字节类型指针) + 4 字节的对齐填充)。
- 使用 JOL 工具来查看对象的内存布局
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
public class JOLSample {
public static void main(String[] args) {
// 打印JVM详细信息(可选)
System.out.println(VM.current().details());
// 创建Object实例
Object obj = new Object();
// 打印Object实例的内存布局
String layout = ClassLayout.parseInstance(obj).toPrintable();
System.out.println(layout);
}
}
![javaobjectsize.png](https://290ff162.telegraph-image-eg9.pages.dev/file/cf23960d893800508fdad.png)
- OFFSET:偏移地址,单位字节;
- SIZE:占用的内存大小,单位字节;
- TYPE DESCRIPTION:类型描述,其中 object header 为对象头;
- VALUE:对应内存中当前存储的值,二进制 32 位;
- 对象头是 12 个字节,还有 4 个字节的 padding,一共 16 个字节。
12. 对象引用占多少大小?
- HotSpot JVM 默认开启了压缩指针,在 64 位JVM 上,对象引用占用 4 字节。关闭则是8字节
- ReferenceHolder.reference字段位于偏移量12,为4字节。这表明在64位JVM下且压缩指针开启,对象引用占用的内存大小为4字节
class ReferenceSizeExample {
private static class ReferenceHolder {
Object reference;
}
public static void main(String[] args) {
System.out.println(VM.current().details());
System.out.println(ClassLayout.parseClass(ReferenceHolder.class).toPrintable());
}
}
![javaobjectreferencesize.png](https://290ff162.telegraph-image-eg9.pages.dev/file/915406ceed01532cc31a6.png)
13. 对象怎么访问定位?
- Java 程序会通过栈上的 reference 数据来访问堆上的具体对象。reference 类型是一个指向对象的引用,是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种:
- 如果使用句柄访问的话,Java 堆中将可能会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,最大好处就是 reference 中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。
- 如果使用直接指针访问的话,Java 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在 Java 中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本。
- HotSpot 虚拟机主要使用直接指针来进行对象访问。
14. 内存溢出和内存泄漏是什么意思?
- 内存溢出:指当程序请求分配内存时,由于没有足够的内存空间满足其需求,会抛出 OutOfMemoryError。内存溢出可能是由于内存泄漏或者程序一次性尝试分配大量内存,内存直接就干崩溃了导致的
- 内存泄漏:是指程序在使用完内存后,未能释放已分配的内存空间,导致这部分内存无法再被使用。随着时间的推移,内存泄漏会导致可用内存逐渐减少,最终可能导致内存溢出。
- 内存泄漏通常发生在长期存活的对象持有短期存活对象的引用,而长期存活的对象又没有及时释放对短期存活对象的引用,从而导致短期存活对象无法被回收。
15. 举几个可能发生内存泄漏的情况?java内存泄漏的例子有哪些?举例并且说明解决方法
- 静态集合类:静态集合的生命周期和JVM一致,所以静态集合引用的对象不能被释放。尽量不要使用static成员变量,减少生命周期
- 单例模式:单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏。
- 各种连接:比如数据库连接,网络连接(socket) 和IO连接,除非调用close()方法将其连接关闭,否则是不会自动被GC回收;及时关闭资源
- 变量不合理的作用域:变量的定义作用域大于其使用范围或不再使用对象没有及时将对象设置为 null,很可能导致内存泄漏的发生。不用的对象,手动设置为null
- hash值发生变化:对象修改后的Hash值和存储进容器时的Hash值不同,所以无法找到存入的对象,无法单独删除
- ThreadLocal 使用不当。使用完 ThreadLocal 一定要记得使用 remove 方法来进行清除。
- 监听器:释放对象的时候没有删除监听器;
- 内部类:内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放;
16. jvm垃圾回收机制及垃圾判断算法
垃圾回收(Garbage Collection,GC),就是释放垃圾占用的空间,防止内存爆掉。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
垃圾收集器在对堆区和方法区进行回收前,首先要确定这些区域的对象哪些可以被回收,哪些暂时还不能回收,就要用到判断对象是否存活的算法。
引用计数算法(Reference Counting)给对象中添加一个引用计数器,初始值为1,每当一个地方引用它时,计数器值+1.引用失效时(一个对象实例的某个引用超过了生命周期或者被设置为一个新值时),计数器值-1;当计数器为0则直接回收。简单,判定效率高,实时性较高,无需等到内存不够的时候,才开始回收,但不能解决对象间相互循环引用问题。
可达性分析算法(Reachability Analysis)通过一系列称为GC Roots的对象为起始点,从这些节点向下搜索,搜索所走过的路径称为引用连(Reference Chain),当一个对象到GC Roots没有任何引用链相连(从GC Roots到这个对象不可达)时,证明此对象是不可用的。
GC Roots的对象包括:
- java虚拟机栈(栈中的本地变量表)中引用的对象(方法的参数、局部变量等),
- 本地方法栈中JNI(native方法)引用的对象,
- 类静态变量,
- 运行时常量池中的常量(String 或 Class 类型)。
17. 强软弱虚引用
- 强引用(Strong Reference) Object obj = new Object(),垃圾回收器永远不会回收掉被强引用的对象,即使抛出OutOfMemoryError
- 软引用(Soft Reference)描述一些还有用但非必须的对象,在系统将要发生内存溢出异常之前,将会对弱引用对象进行二次回收,如果这次回收还没有足够内存,才会抛出内存异常。SoftReference类实现软引用。可用来实现内存敏感的高速缓存。可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JVM就会把这个软引用加入到与之关联的引用队列中
- 弱引用(Weak Reference)描述非必须对象,当垃圾回收时都会回收掉只被弱引用关联的对象。Weakreference类实现弱引用、可以和引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中
- 虚引用(Phantom reference) 主要用来跟踪对象被垃圾回收的活动。虚引用不影响对象存活,也不能通过虚引用获得对象实例。PhantomReference类实现虚引用、虚引用必须和引用队列(ReferenceQueue)联合使用。如果虚引用所引用的对象被垃圾回收,JAVA虚拟机就会把这个虚引用加入到与之关联的引用队列中。程序如果发现虚引用已经被加入到引用队列,就可以在所引用的对象被回收前采取行动
18. 被标记为垃圾的对象一定会被回收吗?finalize()方法作用?
- 在可达性分析算法中不可达的对象.至少要经历两次标记过程。
- 第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,将会被第一次标记;
- 第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行 finalize() 方法。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。第二次标记成功的对象将真的会被回收,如果对象在 finalize() 方法中重新与引用链建立了关联关系,那么将会存活。建议在这个方法中释放该对象持有的资源,不建议重写该方法。该方法有且仅会被调用一次
19. Java 堆的内存分区
Java堆根据对象存活周期的不同将内存划分为
- 新生代(Young Generation)包括一块较大的Eden空间和两块较小的Survivor空间,大小比例是8:1:1 (参数–XX:SurvivorRatio设定),新生代的垃圾收集主要采用标记-复制算法,因为新生代的存活对象比较少,每次复制少量的存活对象效率比较高。每次分配内存只使用 Eden 和其中一块 Survivor。发生垃圾收集时,将 Eden 和 Survivor 中仍然存活的对象一次性复制到另外一块 Survivor 空间上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。
- 老年代(Tenured Generation)因为对象存活率高、没有额外空间对它进行分配担保,使用“标记—清理”或者“标记—整理”算法来进行回收。默认新生代与老年代的比例的值为 1:2 (参数–XX:NewRatio指定)
- 永久代(Permanet Generation)存储class,method,filed对象,一般不会内存溢出,jdk1.8替换为Metaspace(元数据空间)。所占用的内存空间不在虚拟机内部,而是在本地内存空间中。修改原因:由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen
- Virtual区:最大内存和初始内存的差值,就是Virtual区
20. 常用的垃圾回收算法,JVM中的垃圾回收了解吗?为什么新生代不用标记整理?√
标记-复制算法(Copying):将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后对这一块内存空间进行回收,
不会产生连续内存碎片,内存分配时只要移动堆顶指针,按顺序分配内存即可,效率高;但内存减半,空间利用率低。适合只有少量对象存活的场景,适合新生代
标记-清除算法(Mark-Sweep):标记所有需要回收的对象,在标记完成后统一回收所有标记的对象
会产生不连续的内存碎片,不利于分配大对象。适合对象不多的情况。标记和清除两个过程的效率都不高.适合老年代
标记-整理算法(Mark-Compact)标记所有需要回收的对象,在标记完成后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
不会产生内存碎片,缺点是需要移动对象,清除效率比标记清除低,适合老年代
21. Minor GC/Young GC、Major GC/Old GC、Mixed GC、Full GC都是什么意思?
- 新生代收集(Minor GC/Young GC):指新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指老年代的垃圾收集。CMS 收集器的特有行为。
- 混合收集(Mixed GC):指G1 垃圾收集器特有的一种 GC 类型,它在一次 GC 中同时清理年轻代和部分老年代
- 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。会STOP THE WORD
22. Minor GC/Young GC 什么时候触发?
- 新创建的对象优先在新生代 Eden 区进行分配,如果 Eden 区没有足够的空间时,就会触发 Young GC 来清理新生代。
23. 什么时候会触发 Full GC?
- Young GC 之前检查老年代:Young GC时发现老年代可用的连续内存空间 < 新生代历次Young GC后升入老年代的对象总和的平均大小,说明本次 Young GC 后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,那就会触发 Full GC。
- Young GC 之后老年代空间不足:执行 Young GC 之后有一批对象需要放入老年代,老年代没有足够的内存空间存放这些对象,就会触发 Full GC。
- 老年代空间不足,老年代内存使用率过高,达到一定比例,也会触发 Full GC。
- 空间分配担保失败( Promotion Failure),新生代的 To 区放不下从 Eden 和 From 拷贝过来对象,或者新生代对象 GC 年龄到达阈值需要晋升这两种情况,老年代如果放不下的话都会触发 Full GC。
- 方法区内存空间不足:如果方法区由永久代实现,永久代空间不足 Full GC。
- System.gc()等命令触发:System.gc()、jmap -dump 等命令会触发 full gc。
24. 对象什么时候会进入老年代?
- 长期存活的对象进入老年代。在对象的对象头信息中存储着对象的迭代年龄,迭代年龄会在每次 YoungGC 之后对象的移区操作中增加,每一次移区年龄加一.当这个年龄达到 15(默认)之后,这个对象将会被移入老年代。- XX:MaxTenuringThreshold设置
- 大对象直接分配到老年代。避免在Eden区和两个Survivor区之间发生大量的内存拷贝-XX:PretenureSizeThreshold设置
- 动态对象年龄判定。如果在survivor区中相同年龄的所有对象大小大于survivor空间的一半,则大于或等于该年龄的对象,可进入老年区
- 空间分配担保。Young GC后新生代仍然有大量对象存活,就需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代
25. 分代收集算法(Generational Collection)
![heapmemoryallocation.png](https://290ff162.telegraph-image-eg9.pages.dev/file/e84e053a2828ccee7473c.png)
- JVM垃圾回收机制采用的分代回收
- 对象优先在新生代Eden区上进行分配,每次使用Eden和其中一块Survivor。
- 如果Eden区空间不够时,尝试放入到Survivor0,发起Minor GC,
- 如果Survivor0可以放入,那么放入之后清除Eden区。
- 如果Survivor0不可以放入,那么尝试把Eden和Survivor0的存活对象放到Survivor1中。年龄+1,增加到一定年龄则移动到老年代中
- 如果Survivor1可以放入,那么放入Survivor1之后清除Eden和Survivor0 ,之后再把Survivor1中的对象复制到Survivor0中,保持Survivor1一直为空。
- 如果Survivor1不可以放入,那么直接把它们放入到老年代中,并清除Eden和Survivor0,这个过程也称为分配担保
- 如果Eden区空间不够时,尝试放入到Survivor0,发起Minor GC,
- 对象优先在新生代Eden区上进行分配,每次使用Eden和其中一块Survivor。
26. jvm垃圾回收器有哪些?作用是什么?
自动管理 Java 应用程序的运行时内存。它负责识别哪些内存是不再被应用程序使用,并释放这些内存以便重新使用。减少了程序员手动管理内存的负担,降低了内存泄漏和溢出错误的风险。
STW(stop the world):所有工作线程暂停
JVM 的垃圾收集器主要分为两大类:分代收集器和分区收集器。分代收集器的代表是 CMS,分区收集器的代表是 G1 和 ZGC。
Serial/Serial Old收集器,单线程,进收集时会STW。-XX:+UseSerialGC:serial新生代使用标记复制,Serial Old老年代标记整理
ParNew/Serial Old收集器,Serial多线程版,-XX:+UseParNewGC:ParNew新生代使用标记复制,Serial Old老年代标记整理。
Parallel Scavenge/Parallel Old收集器,多线程.可设置吞吐量参数 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
- -XX:+UseParallelGC年轻代使用ParallelGC垃圾回收器标记复制,老年代使用串行回收器
- -XX:+UseParalledlOldGC年轻代使用ParallelGC垃圾回收器标记复制,老年代使用ParallelOldGC垃圾回收器标记-整理
- -XX:MaxGCPauseMillis=n:设置并行收集最大暂停毫秒数
- -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比0~100,默认值为99,也就是垃圾回收时间不能超过1%
- -XX:UseAdaptiveSizePolicy自适应GC模式,垃圾回收器将自动调整年轻代、老年代等参数,达到吞吐量、堆大小、停顿时间之间的平衡
CMS (Concurrent Mark Sweep)多线程,回收时间短,标记—清除,-XX:+UseConcMarkSweepGC进行设置
G1 收集器(Garbage-First Garbage Collector) 年轻代标记复制,老年代标记整理。jdk9后默认。G1 有五个属性:分代、增量、并行、标记整理、STW。
G1 垃圾回收模式
- Eden空间耗尽时会触发Young GC。Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。
- 当大量对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发Mixed GC,回收整个Young Region和一部分的Old Region,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。当老年代大小占整个堆大小百分比达到该-XX:InitiatingHeapOccupancyPercent=n时触发。默认45%.它的GC步骤如下
- 1.全局并发标记(global concurrent marking),执行过程分为五个步骤:
- 初始标记(initial mark,STW)标记从根节点直接可达的对象,这个阶段会执行一次年轻代GC,会产生全局停顿
- 根区域扫描(root region scan)G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才开始下一次STW年轻代垃圾回收
- 并发标记(Concurrent Marking)G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断。
- 重新标记(Remark,STW)该阶段是 STW 回收,因为程序在运行,针对上一次的标记进行修正。
- 清除垃圾(Cleanup,STW)清点和重置标记状态,该阶段会STW,这个阶段并不会实际上去做垃圾的收集,等待evacuation阶段来回收。
- 2.拷贝存活对象(evacuation):该阶段是全暂停的。把一部分Region里的活对象拷贝到另一部分Region中,从而实现垃圾的回收清理
- 1.全局并发标记(global concurrent marking),执行过程分为五个步骤:
- Full GC
- Remembered Set(已记忆集合)其作用是跟踪指向某个堆内的对象引用。每个Region初始化时,会初始化一个RSet,该集合用来记录并跟踪其它Region指向该Region中对象的引用,每个Region默认按照512Kb划分成多个Card,所以RSet需要记录的东西应该是 xx Region的 xx Card。
G1参数设置 G1 GC 的吞吐量目标是 90% 的应用程序时间和 10%的垃圾回收时间,MaxGCPauseMillis不要设置太小
- -XX:+UseG1GC使用G1垃圾收集器
- -XX:MaxGCPauseMillis设置期望达到的最大GC停顿时间毫秒(尽力保证),默认200
- -XX:G1HeapRegionSize=n设置G1区域的大小。值是2的幂,1-32MB。目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000
- -XX:ParallelGCThreads=n设置STW工作线程数的值。n的值与逻辑处理器的数量相同,最多8
- -XX:ConcGCThreads=n设置并行标记的线程数。将n设置为并行垃圾回收线程数 (ParallelGCThreads)的 1/4 左右
- -XX:InitiatingHeapOccupancyPercent=n设置触发标记周期的Java堆占用率阈值。默认45%
ZGC收集器 低延迟垃圾收集器,适用于大内存低延迟服务的内存管理和回收。ZGC 的两个关键技术:指针染色和读屏障,不仅应用在并发转移阶段,还应用在并发标记阶段:将对象设置为已标记,传统的垃圾回收器需要进行一次内存访问,并将对象存活信息放在对象头中;而在 ZGC 中,只需要设置指针地址的第 42-45 位即可,并且因为是寄存器访问,所以速度比访问内存更快。
27. 什么是 OopMap ?
- 在 HotSpot 中,有个数据结构(映射表)称为OopMap。一旦类加载动作完成的时候,HotSpot 就会把对象内什么偏移量上是什么类型的数据计算出来,记录到 OopMap。在即时编译过程中,也会在安全点生成 OopMap,记录下栈上和寄存器里哪些位置是引用。
28. 什么是安全点?
- Safe Point指Java线程执行到某个位置时JVM能够安全、可控的回收对象。如果一个Java线程分配一个对象,此时对象的地址还在寄存器中,这时候这个线程失去了CPU时间片,而此时STW GC发现没有任何GC ROOTS 与该对象关联起来,被认为是垃圾并被回收了,之后CPU重新获得时间片后发现此时对象已经不存在了,程序出错
- SafePoint 指的特定位置主要有:
- 循环的末尾 (防止大循环的时候一直不进入 Safepoint ,而其他线程在等待它进入 Safepoint )。
- 方法返回前。
- 调用方法的 Call 之后。
- 抛出异常的位置
29. cms执行过程?
- 初始标记(Initial Mark):标记所有从 GC Roots 直接可达的对象,这个阶段需要 STW,但速度很快。
- 并发标记(Concurrent Mark):从初始标记的对象出发,遍历所有对象,标记所有可达的对象。这个阶段是并发进行的,STW。
- 重新标记(Remark):完成剩余的标记工作,包括处理并发阶段遗留下来的少量变动,这个阶段通常需要短暂的 STW 停顿。
- 并发清除(Concurrent Sweep):清除未被标记的对象,回收它们占用的内存空间。
30. G1收集器?
- 多线程,缩短STW时间。
- G1 把 Java 堆划分为多个大小相等的独立区域(Region),每个区域都可以扮演Eden、survivor、老年代角色,另外Humongous区存放巨型对象(超过分区容量50%)如果一个大对象超过了一个 Region 大小的 50%,就会被放入 Humongous 中。区域化管理使得 G1 可以更灵活地进行垃圾收集,只回收部分区域而不是整个新生代或老年代。
- G1 收集器的运行过程:
- 并发标记,通过并发标记的方式找出堆中的垃圾对象。并发标记阶段与应用线程同时执行,不会导致应用线程暂停。
- 混合收集,在并发标记完成后,G1 会计算出哪些区域的回收价值最高(也就是包含最多垃圾的区域),然后优先回收这些区域。这种回收方式包括了部分新生代区域和老年代区域。选择回收成本低而收益高的区域进行回收,可以提高回收效率和减少停顿时间。
- 可预测的停顿,G1 在垃圾回收期间仍然需要「Stop the World」。不过,G1 在停顿时间上添加了预测机制,用户可以 JVM 启动时指定期望停顿时间,G1 会尽可能地在这个时间内完成垃圾回收。
31. 有了 CMS,为什么还要引入 G1?
- CMS最主要的优点是并发收集、低停顿。
- CMS缺点。
- Mark Sweep 算法会导致内存碎片比较多
- CMS 的并发能力比较依赖于 CPU 资源,并发回收时垃圾收集线程可能会抢占用户线程的资源,导致用户程序性能下降。
- 并发清除阶段,用户线程依然在运行,会产生所谓的理“浮动垃圾”(Floating Garbage),本次垃圾收集无法处理浮动垃圾,必须到下一次垃圾收集才能处理。如果浮动垃圾太多,会触发新的垃圾回收,导致性能降低。
- G1 主要解决了内存碎片过多的问题。
32. G1 和 CMS 的区别?
- CMS主要步骤:初始收集,并发标记,重新标记,并发清除(删除)、重置。
- G1主要步骤:初始标记,并发标记,重新标记,复制清除(整理)
- CMS缺点是对CPU要求高。G1将内存化成了多块,所有对内存要求高
- CMS是清除,存在内存碎片。G1是整理,碎片空间较小。
- G1和CMS都是响应时间优先,都是尽量控制 STW 时间。
33. 线上用的什么垃圾收集器?为什么要用它?
- jdk8默认收集器java -XX:+PrintCommandLineFlags -version查看->-XX:+UseParallelGC表示的是新生代用的Parallel Scavenge收集器,老年代用的是Parallel Old 收集器。
- 采用Parallel Scavenge + Parallel Old的组合。系统业务相对复杂,但并发并不是非常高,希望尽可能利用处理器资源,出于提高吞吐量的考虑
- 采用Parallel New+CMS的组合,我们比较关注服务的响应速度,所以采用了 CMS 来降低停顿时间。
- 采用 G1 垃圾收集器,因为它不仅满足我们低停顿的要求,而且解决了 CMS 的浮动垃圾问题、内存碎片问题
34. 垃圾收集器应该如何选择?
- Serial :如果应用程序有一个很小的内存空间(大约 100 MB)亦或它在没有停顿时间要求的单线程处理器上运行。
- Parallel:如果优先考虑应用程序的峰值性能,并且没有时间要求要求,或者可以接受 1 秒或更长的停顿时间。
- CMS/G1:如果响应时间比吞吐量优先级高,或者垃圾收集暂停必须保持在大约 1 秒以内。
- ZGC:如果响应时间是高优先级的,或者堆空间比较大
35. 对象一定分配在堆中吗?有没有了解逃逸分析技术?好处?
对象不一定分配在堆中。在编译期间,JIT 会对代码做很多优化。其中有一部分优化的目的就是减少内存堆分配压力,其中一种技术叫做逃逸分析。
逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和外形分析相关联。当变量(或者对象)在方法中分配后,其指针有可能被返回或者被全局引用,这样就会被其他方法或者线程所引用,这种现象称作指针(或者引用)的逃逸(Escape)。通俗点讲,当一个对象被 new 出来之后,它可能被外部所调用,如果是作为参数传递到外部了,就称之为方法逃逸。如果对象还有可能被外部线程访问到,例如赋值给可以在其它线程中访问的实例变量,这种就被称为线程逃逸。
逃逸分析的好处
- 栈上分配:如果确定一个对象不会逃逸到线程之外,那么久可以考虑将这个对象在栈上分配,对象占用的内存随着栈帧出栈而销毁,这样一来,垃圾收集的压力就降低很多。
- 同步消除:线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施也就可以安全地消除掉。
- 标量替换:如果一个数据是基本数据类型,不可拆分,它就被称之为标量。把一个 Java 对象拆散,将其用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么可以不创建对象,直接用创建若干个成员变量代替,可以让对象的成员变量在栈上分配和读写。
36. 有哪些常用的命令行性能监控和故障处理工具?
- 操作系统工具
- top:显示系统整体资源使用情况
- vmstat:监控内存和 CPU
- iostat:监控 IO 使用
- netstat:监控网络使用
- JDK 性能监控工具
- jps:虚拟机进程查看
- jstat:虚拟机运行时信息查看
- jinfo:虚拟机配置查看
- jmap:内存映像(导出)
- jhat:堆转储快照分析
- jstack:Java 堆栈跟踪
- jcmd:实现上面除了 jstat 外所有命令的功能
37. 了解哪些可视化的性能监控和故障处理工具?
- JConsole
- VisualVM
- Java Mission Control
- MAT Java 堆内存分析工具。
- GChisto GC 日志分析工具。
- GCViewer GC 日志分析工具。
- JProfiler 商用的性能分析利器。
- arthas 阿里开源诊断工具。
- async-profiler Java 应用性能分析工具,开源、火焰图、跨平台。
38. 常使用的jvm -XX参数 √
- 堆设置
- -Xms:初始堆大小 -Xms512m:等价于-XX:InitialHeapSize,设置JVM初始堆内存为512M。
- -Xmx:最大堆大小 -Xmx2048m:等价于-XX:MaxHeapSize,设置JVM最大堆内存为2048M。
- -Xmn 新生代大小
- -XX:NewSize=n:设置年轻代大小
- -XX:PermSize 初始化永久代大小
- -XX:MaxPermSize=n:设置持久代大小
- -XX:NewRatio=n:年轻代和年老代的比值。为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的 1/4
- -XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。如:3,表示 Eden:Survivor=3:2,一个Survivor区占整个年轻代的 1/5
- -XX:Xmn 设置年轻代大小
- 收集器设置
- -XX:+UserSerialGC 串行垃圾收集器
- -XX:+UserParrallelGC 并行垃圾收集器
- -XX:+UseParalledlOldGC:设置并行年老代收集器
- -XX:+UseConcMarkSweepGC 并发标记扫描垃圾回收器
- -XX:+UseG1GC G1垃圾回收器
- 并行收集器参数设置
- -XX:ParallelCMSThreads=n 设置并行收集器收集时使用的 CPU 数。并行收集线程数
- -XX:MaxGCPauseMillis=n:设置并行收集最大的暂停时间(如果到这个时间了,垃圾回收器依然没有回收完,也会停止回收)
- -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为:1/(1+n)
- -XX:+CMSIncrementalMode:设置为增量模式。适用于单 CPU 情况
- -XX:ParallelGCThreads=n:设置并发收集器年轻代手机方式为并行收集时,使用的 CPU 数。并行收集线程数
- 垃圾回收统计信息 (https://gceasy.io)
- -XX:+PrintGC:开启打印gc信息
- -XX:+PrintGCDetails:打印 gc 详细信息
‐ -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
‐ -XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013‐05‐04T21:53:59.234+0800) - -Xloggc:filename ../logs/gc.log 日志文件的输出路径(GC Easy工具可以直接选择日志文件查看)
- ‐XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
- 设置系统属性参数
- -D<名称>=<值> 可通过System.getProperty("名称");获得
- 查看正在运行的java进程所有参数jinfo ‐flags pid
- 查看正在运行的java进程具体参数jinfo ‐flag MaxHeapSize pid
- 完整命令ps -ef |grep java |grep -w x.jar|grep -v 'grep'|awk '{print $2}'| xargs -i{} jinfo ‐flags {}
45. 有没有处理过内存溢出问题?
- 内存泄漏和内存溢出二者关系非常密切,内存溢出可能会有很多原因导致,内存泄漏最可能的罪魁祸首之一。
- 排查过程和排查内存泄漏过程类似。
46. 解释执行和编译执解释和编译的区别:
- 解释:将源代码逐行转换为机器码。
- 编译:将源代码一次性转换为机器码。
- 解释执行:程序运行时,将源代码逐行转换为机器码,然后执行。
- 编译执行:程序运行前,将源代码一次性转换为机器码,然后执行。
- Java被称为“解释型语言”,因为Java代码执行前,需要先将源代码编译成字节码,然后在运行时,再由 JVM 的解释器“逐行”将字节码转换为机器码,然后执行。这也是 Java 被诟病“慢”的主要原因。
- JIT 的出现打破了这种刻板印象,JVM 会将热点代码(即运行频率高的代码)编译后放入 CodeCache,当下次执行再遇到这段代码时,会从 CodeCache 中直接读取机器码,然后执行。这大大提升了 Java 的执行效率。
47. 类的加载机制?
- JVM 的操作对象是 Class 文件,JVM 把 Class 文件中描述类的数据结构加载到内存中,并对数据进行校验、解析和初始化,最终形成可以被 JVM 直接使用的类型,这个过程被称为类加载机制。
48. 类的生命周期吗?
- 一个类从被加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading),其中验证、准备、解析三个部分统称为连接(Linking)。
49. 类加载器是如何加载Class文件?
![processofloadingclass.png](https://290ff162.telegraph-image-eg9.pages.dev/file/6db427fa24fe468568e6b.png)
- 类加载过程有:载入、验证、准备、解析、初始化。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段会发生在初始化阶段之后。
- 加载(Loading),找到.class文件并把文件包含的字节码加载到内存中。使用系统提供或者的类加载器来完成加载。加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个 java.lang.Class 类的对象,这样便可以通过该对象访问方法区中的这些数据。在加载阶段,虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取其定义的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在Java堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口。
- 验证:对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。
- 准备:对类变量(也称为静态变量,static 关键字修饰的变量)分配内存并初始化,初始化为数据类型的默认值,如 0、0L、null、false 等。
- 解析:是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、成员方法等。
- 初始化:类变量将被赋值为代码期望赋的值。是执行类的构造方法(javap 中看到的
<clinit>
() 方法)的过程。
50. jvm类加载器?
- 类加载器(ClassLoader),负责加载类文件,将类文件加载到内存中,生成 Class 对象。
- BootstrapLoader:启动类加载器,负责加载 JVM 的核心类库,如 rt.jar和位于JAVA_HOME/jre/lib目录下的类。其无法被Java程序直接引用。
- ExtClassLoader:扩展类加载器 加载扩展类(就是继承类和实现类),加载\lib\ext目录或者被 java.ext.dirs系统变量指定的路径中的所有类库
- AppClassLoader:加载来自Java命令的-classpath选项、java.class.path系统属性或CLASSPATH环境变量所指定的jar包和类路径。编写的任何类都是由应用程序类加载器加载的,除非显式使用自定义类加载器。
- 用户自定义类加载器 (User-Defined ClassLoader),通过继承java.lang.ClassLoader类来创建自己的类加载器。通常用于加载网络上的类、执行热部署(动态加载和替换应用程序的组件)或为了安全目的自定义类的加载方式。
51. 什么是双亲委派模型(Parent Delegation Mode)?
- 工作过程:ClassLoader先从自己已经加载的类缓存中,查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。如果没有则委托父类加载器去加载,父类加载器采用同样的策略,一直到bootstrap ClassLoader。当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。
- 保证Java程序的稳定运行,避免类重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),
- 保证Java的核心API不被篡改。 假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class
- 双亲委派模型的主要代码实现:在ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
- 自定义类加载器继承ClassLoader类并重写loadClass()、findClass()方法;最主要的是重写loadClass方法,因为双亲委派机制的实现都是通过这个方法实现的,源码里会直接找到根加载器,重写了这个方法以后就能自己定义加载的方式了
52. 你觉得应该怎么实现一个热部署功能?
- 实现一个热部署(Hot Deployment)功能通常涉及到类的加载和卸载机制,使得在不重启应用程序的情况下,能够动态替换或更新应用程序的组件。
-第一步,使用文件监控机制(如 Java NIO 的 WatchService)来监控类文件或配置文件的变更。当监控到文件变更时,触发热部署流程。
class FileWatcher {
public static void watchDirectoryPath(Path path) {
// 检查路径是否是文件夹
try {
Boolean isFolder = (Boolean) Files.getAttribute(path, "basic:isDirectory", LinkOption.NOFOLLOW_LINKS);
if (!isFolder) {
throw new IllegalArgumentException("Path: " + path + " is not a folder");
}
} catch (IOException ioe) {
// 文件 I/O 错误
ioe.printStackTrace();
}
System.out.println("Watching path: " + path);
// 我们获得文件系统的WatchService对象
FileSystem fs = path.getFileSystem();
try (WatchService service = fs.newWatchService()) {
// 注册路径到监听服务
// 监听目录内文件的创建、修改、删除事件
path.register(service, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
// 开始无限循环,等待事件发生
WatchKey key = null;
while (true) {
key = service.take(); // 会阻塞直到有事件发生
// 对于每个发生的事件
for (WatchEvent<?> watchEvent : key.pollEvents()) {
WatchEvent.Kind<?> kind = watchEvent.kind();
// 获取文件路径
@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>) watchEvent;
Path fileName = ev.context();
System.out.println(kind.name() + ": " + fileName);
}
// 重置watchKey
boolean valid = key.reset();
// 退出循环如果watchKey无效
if (!valid) {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 监控当前目录
Path pathToWatch = Paths.get(".");
watchDirectoryPath(pathToWatch);
}
}
- 第二步,创建一个自定义类加载器,继承自java.lang.ClassLoader,重写findClass()方法,实现类的加载。
public class HotSwapClassLoader extends ClassLoader {
public HotSwapClassLoader() {
super(ClassLoader.getSystemClassLoader());
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 加载指定路径下的类文件字节码
byte[] classBytes = loadClassData(name);
if (classBytes == null) {
throw new ClassNotFoundException(name);
}
// 调用defineClass将字节码转换为Class对象
return defineClass(name, classBytes, 0, classBytes.length);
}
private byte[] loadClassData(String name) {
// 实现从文件系统或其他来源加载类文件的字节码
// ...
return null;
}
}
53. Tomcat 的类加载机制了解吗?
- Tomcat 实际上也是破坏了双亲委派模型的。Tomact 是 web 容器,可能需要部署多个应用程序。不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。如多个应用都要依赖 hollis.jar,但是 A 应用需要依赖 1.0.0 版本,但是 B 应用需要依赖 1.0.1 版本。这两个版本中都有一个类是 com.hollis.Test.class。如果采用默认的双亲委派类加载机制,那么无法加载多个相同的类。
- 所以,Tomcat 破坏了双亲委派原则,提供隔离的机制,为每个 web 容器单独提供一个 WebAppClassLoader 加载器。每一个 WebAppClassLoader 负责加载本身的目录下的 class 文件,加载不到时再交 CommonClassLoader 加载,这和双亲委派刚好相反。
==================================================================================================
54. 类加载发生的时机?
- 遇到new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类还没进行初始化,则需要先触发其初始化。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类还没进行初始化,则需要先触发其初始化。
- 当初始化了一个类的时候,如果发现其父类还没进行初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时的主类,即调用其 #main(String[] args) 方法,虚拟机则会先初始化该主类。
- 当使用JDK7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
55. 为什么要有不同的引用类型?
- 为了控制对象被回收的时机,
- 利用软引用和弱引用解决OOM问题。通过软引用和hashMap实现Java对象的高速缓存。如用HashMap保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收缓存图片对象所占用的空间,有效避免OOM问题
56. OOM有哪些异常类型?√
Java heap space堆空间溢出,最常见的(对象创建太多)
java.lang.StackOverflowError栈空间溢出,栈管运行,每个方法就是一个栈帧,循环调用方法,会出现这种问题
Direct buffer memory 由于ByteBuffer. allocteDirect(capability)分配操作系统本地内存,不属于GC 管辖范围。不需要内存拷贝所以速度相对较快。分配太多内存不够
GC overhead limit exceeded GC连续多次GC都只回收了不到2%的极端情况下会抛出。
unable to create new native thread;多线程linux系统默认允许单个进程可以创建的线程数是1024个,应用创建超过这个数量,就会报
控制台查看错误日志。
使用JDK自带的jvisualvm工具查看系统的堆栈日志。
定位出内存溢出的空间:堆,栈还是永久代(JDK8 以后不会出现永久代的内存溢出)。- 如果是堆内存溢出,看是否创建了超大的对象。
- 如果是栈内存溢出,看是否创建了超大的对象,或者产生了死循环。
57. 代码优化
- 尽可能使用局部变量
- 尽量减少对变量的重复计算 如遍历时i小于list.size()可以改为i小于length
- 异常不应该用来控制程序流程.异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。
- 尽量采用懒加载的策略,即在需要的时候才创建
- 不要将数组声明为public static final 因为这毫无意义,数组的内容还是可以随意改变的,
- 不要创建一些不使用的对象,不要导入一些不使用的类
- 程序运行过程中避免使用反射
- 使用数据库连接池和线程池.重用对象,前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程。
- 容器初始化时尽可能指定长度。避免容器长度不足时,扩容带来的性能损耗。
- ArrayList随机遍历快,LinkedList添加删除快
- 使用Entry遍历Map
- 不要手动调用System.gc();
- String尽量少用正则表达式。其效率较低,replace() 不支持正则。replaceAll() 支持正则。如果仅仅是字符的替换建议使用replace()。
- 日志的输出要注意级别
- 对资源的close()建议分开操作