1、JVM内存结构
Java中的JVM内存模型是指JVM在运行时对内存的组织和管理方式。它包括了不同的内存区域,每个区域有不同的作用和生命周期。
- 程序计数器:保存当前线程执行的字节码指令的地址。
- 方法区:方法区是存储静态变量、常量这些还有其他已经被加载的类信息等。
- 本地方法栈:执行本地方法。
- 堆:堆是线程共享的,用来存储对象和数组,堆是Java中最大的一块内存区域(发生GC的位置)
- Java虚拟机栈:每个线程在运行时都会创建一个栈,用于存储局部变量、方法参数、返回地址等。栈以帧(Frame)为单位存储方法的调用和返回信息。
- 运行时常量池:运行时常量池是方法区的一部分,用于存储编译期生成的字面量和符号引用。
- 直接内存:直接内存并不是JVM内部的一部分,它是通过使用NIO(New I/O)库分配的,对应于操作系统的堆外内存,用于提高IO操作的性能。
堆,一个JVM只有一个堆内存,堆内存的大小是可以调节的
类加载器读取类文件后,一般会把什么东西放到堆中?类,方法,常量,变量,保存我们所有引用类型的真实对象
JVM内存划分为堆内存和非堆内存,堆内存分为年轻代、老年代,非堆内存就一个永久代。年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。
2、类加载器
启动类加载器(Bootstrap ClassLoader) 扩展类加载器(Extension ClassLoader) 应用程序类加载器(负责加载用户类路径上所指定的类库)(Application ClassLoader)
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类必定不相等。类只需加载一次就行,因此要保证类加载过程线程安全,防止类加载多次。
Java程序的类加载器采用双亲委派模型,实现双亲委派的代码集中在java.lang.ClassLoader的loadClass()方法中,实现的大致逻辑是:先检查是否已经被加载,若没有加载则调用父类加载器的loadClass()方法,若父类加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载失败,抛出ClassNotFoundException异常。
2、JVM类加载机制(双亲)
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都这样的,所以所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
3、JVM的垃圾回收
当对象不再被使用时,垃圾回收器会先对这个对象进行一个标记操作,标记为待清理状态,然后下次遍历的时候才将这些被标记的对象清除掉。垃圾回收它是自动回收的。自动释放那些不再有引用指向的对象所占用的内存。
怎么判断对象死没死?
一种是引用计数器,它为每个对象分配一个计数器,当对象被引用时,计数器加1;当对象不再被引用时,计数器减1。当计数器为0时,表示对象不再被使用,可以被垃圾回收器回收。
另一种是可达性分析,这个比较复杂,想不起来了,只记得当一个对象没有和任何引用链相连,就称为不可达,反正最后都是不会有任何引用指向他,就能认为对象死亡
4、垃圾回收算法
他有很多种回收算法,比如标记清楚法,它是分代收集的代表,它将内存分为两个区域:老年代和新年代。垃圾回收时,首先遍历所有对象,将活动对象标记为“活动”,然后遍历标记过的对象,将不再使用的对象清除。这个方法的缺点是会产生内存碎片。、
分代收集就是根据对象的年龄来划分内存区域,年轻代和老年代。年轻代的对象是最近创建的,而老年代的对象是最早创建的。垃圾回收时,首先收集年轻代中的对象,然后将年轻代中仍然活动的对象移动到老年代。这种方式可以有效地减少内存碎片和垃圾回收停顿时间。
其他还有引用计数算法、复制算法等等。
JVM的垃圾回收机制主要包括以下几种:
- 引用计数器:引用计数器是一种简单的垃圾回收机制,它为每个对象分配一个计数器,当对象被引用时,计数器加1;当对象不再被引用时,计数器减1。当计数器为0时,表示对象不再被使用,可以被垃圾回收器回收。
- 复制:复制机制将内存分为两个区域,每次只使用其中一个区域。垃圾回收时,将活动对象复制到另一个区域,然后清空原区域。这种机制可以确保内存不会被碎片化,但需要额外的内存空间。
- 标记-清除:标记-清除机制是分代收集的代表,它将内存分为两个区域:老年代和新年代。垃圾回收时,首先遍历所有对象,将活动对象标记为“活动”,然后遍历标记过的对象,将不再使用的对象清除。标记-清除机制的缺点是会产生内存碎片。
- 分代收集:分代收集是基于程序运行时对象的年龄来划分内存区域,年轻代和老年代。年轻代的对象是最近创建的,而老年代的对象是最早创建的。垃圾回收时,首先收集年轻代中的对象,然后将年轻代中仍然活动的对象移动到老年代。分代收集可以有效地减少内存碎片和垃圾回收停顿时间。
- 增量收集:增量收集是分代收集的一种改进,它将内存划分为多个小块,每个小块都有自己的新生代和老年代。垃圾回收时,只回收活动对象,并将活动对象移动到下一个小块的新生代中。增量收集可以减少内存碎片,提高垃圾回收效率。
- 并发收集:并发收集是在垃圾回收过程中,允许程序继续执行的一种垃圾回收机制。它通过将内存划分为多个小块,并使用多个线程同时进行垃圾回收,从而在保证程序运行的同时完成垃圾回收。
5、几种引用
-
强引用是Java中最常见的引用类型(普通变量赋值就是强引用),如果一个对象具有强引用,垃圾回收器在内存不足的情况下,宁愿抛出OutOfMemoryError错误,也不会回收该对象。如果希望垃圾回收器在内存不足时回收具有强引用关联的对象,可以通过将引用设置为null来实现。
-
软引用是一种较弱的引用,如果一个对象具有软引用,在系统内存不足时,垃圾回收器会尽可能地回收这些对象。软引用通常用于实现内存敏感的对象池或者内存敏感的缓存。
-
弱引用比软引用更弱,如果一个对象具有弱引用,在系统内存不足时,垃圾回收器会尽可能地回收这些对象。弱引用通常用于实现对象注册表和线程局部变量表等。
-
虚引用是最弱的引用类型,它主要用于在对象被垃圾回收器回收时收到一个系统通知或者执行一些清理操作。虚引用不会影响垃圾回收器的工作,它主要用于满足一些特殊的应用需求,如对象生命周期监控和内存泄漏检测等。
1、对于强引用,平时在编写代码时会经常使用。
2、而其他三种类型的引用,使用得最多就是软引用和弱引用,这两种既有相似之处又有区别,他们都来描述非必须对象。
3、被软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。
4、Java中4种引用的级别由高到低依次为:强引用 > 软引用 > 弱引用 > 虚引用
6、GC工作原理
GC就是垃圾回收,回收那些不再使用的对象占用的内存空间,主要有几个步骤,第一个是标记阶段,标记那些可达对象,就是活动对象,还没有被回收,第二个是清除没有被标记的对象,第三个是将刚才标记的那些活动对象移动到一起,方便下次回收,第四个就是再次标记,然后再次清除。
Java中的GC(垃圾回收)是指自动内存管理系统在运行Java程序时,自动回收不再使用的对象占用的内存空间,从而避免内存泄漏,提高程序运行效率。
7、判断是不是垃圾
一种是引用计数器,它为每个对象分配一个计数器,当对象被引用时,计数器加1;当对象不再被引用时,计数器减1。当计数器为0时,表示对象不再被使用,可以被垃圾回收器回收。
另一种是可达性分析,这个比较复杂,想不起来了,只记得当一个对象没有和任何引用链相连,就称为不可达,就能认为对象死亡
8、内存泄露和内存溢出
内存泄露是在程序运行过程中分配内存给临时变量,用完后没有被GC回收,一直占用着内存,不能被使用也不能分配给其他程序
内存溢出是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存。
9、哪些地方会发生OOM,怎么触发OOM
堆溢出、虚拟机栈和本地方法栈溢出、方法区和运行时常量池溢出、本地直接内存溢出。
OOM 全称 “Out Of Memory”,表示内存耗尽。当 JVM 因为没有足够的内存来为对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出这个错误
为什么会出现 OOM,一般由这些问题引起
- 分配过少:JVM 初始化内存小,业务使用了大量内存;或者不同 JVM 区域分配内存不合理
- 代码漏洞:某一个对象被频繁申请,不用了之后却没有被释放,导致内存耗尽
内存泄漏:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了。因为申请者不用了,而又不能被虚拟机分配给别人用
内存溢出:申请的内存超出了 JVM 能提供的内存大小,此时称之为溢出
内存泄漏持续存在,最后一定会溢出,两者是因果关系
1、Java 堆溢出Java 堆用于储存对象实例,我们只要不断地创建对象,并且保证GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆的容量限制后就会产生内存溢出异常。2、虚拟机栈和本地方法栈溢出HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈,如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常。3、方法区和运行时常量池溢出方法区溢出也是一种常见的内存溢出异常,在经常运行时生成大量动态类的应用场景里,就应该特别关注这些类的回收状况。这类场景常见的包括:程序使用了CGLib字节码增强和动态语言、大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)、基于0SGi的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。在JDK6或更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,即常量池是方法去的一部分,所以上述问题在常量池中也同样会出现。而HotSpot从JDK7开始逐步“去永久代”的计划,并在JDK8中完全使用元空间来代替永久代,所以上述问题在JDK8中会得到避免。4、本地直接内存溢出直接 内存(Direct Memory)的容量大小可通过-xX:MaxDirectMemorySize参数来指定,如果不去指定,则默认与Java 堆最大值(由-Xmx 指定)一致。如果直接通过反射获取Unsafe 实例进行内存分配,并超出了上述的限制时,将会引发00M异常。