Java虚拟机JVM知识点(持续更新)
JVM内存模型
介绍下内存模型
根据JDK8的规范,我们的JVM内存模型可以拆分为:程序计数器、Java虚拟机栈、堆、元空间、本地方法栈,还有一部分叫直接内存,属于操作系统的本地内存,也是可以直接操作的。
详细解释一下
- 程序计数器:可以看作是当前线程所执行的字节码的行号指示器,用于存储当前线程正在执行的Java方法的JVM指令地址。
- Java虚拟机栈:每个线程都有一个独立的Java虚拟机栈,生命周期和线程相同。每个方法在执行时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口等,可能抛出OOM和StackOverflowError异常。
- 本地方法栈:与Java虚拟机栈类型,主要为虚拟机使用到的Native方法服务,在HotSpot虚拟机中和Java虚拟机栈合二为一。本地方法执行时也会创建栈帧,同样也可以抛出OOM和StackOverflowError异常。
- 方法区(元空间):在JDK8以后的版本中,方法区被元空间取代,使用本地内存。用于存储已被虚拟机加载的类信息、常量、静态变量等数据。虽然方法区被描述为堆堆逻辑部分。方法区可以选择不进行垃圾回收。同样可以抛出OOM和StackOverflowError异常。
- Java堆:在JVM最大的一个部分,被所有线程所共享,当虚拟机启动时,用于存放所有的对象实例。从垃圾回收的角度,分为新生代和老年代,新生代分为伊甸区(Eden)和幸存区(Survivor分为From Survivor和To Survivor),如果在堆中没有内存完成实例分配,并且堆也无法扩展时会OOM。
- 运行时常量池:是方法的一部分,用于存放编译期间生成的各种字面量和符号引用,具有动态性。
- 直接内存:不属于JVM运行时数据区的一部分,通过NIO引入,是一种堆外内存,可以显著提高i/o性能。
JVM内存模型中堆和栈的区别
- 用途:栈主要用于存储局部变量、方法参数的调用、方法返回地址以及一些临时数据。每当一个方法被调用,就会创建一个栈帧,用于存储方法的信息,方法执行完毕,栈帧也会被移除。堆用于存储对象的实例,当你使用new去创建一个对象时,对象实例就会在上面分配空间。
- 生命周期:栈中的数据具有确定的生命周期,当一个方法结束调用时,其对应的帧栈就会被移除,栈中存储的局部变量也就会消失。堆中的对象没有固定的生命周期,闲置对象会在垃圾回收机制下被回收。
- 存取速度:栈的存取速度比堆快,因为栈遵循先进后出FIFO的原则,操作简单快速。堆的存取速度较慢,因为对象在对上分配和回收需要更多的时间,垃圾回收机制也会影响性能。
- 存储空间:栈的空间相对较小,且较为固定,由操作系统管理。当栈溢出时,通常因为递归过深或者局部变量过大。堆的空间较大,动态扩展,由JVM管理,堆溢出通常由于创建了太多的大对象未及时收回。
- 可见性:栈中的数据对线程是私有的,每个线程有自己的栈空间。堆中的数据对线程是共享的,所有线程都可以访问堆上的对象。
栈中存储的到底是指针还是对象
在JVM内存模型中,栈主要用于管理线程的局部变量和方法的上下文调用,而堆是粗处所有类的实例和数组。
当我们在讨论存储时,实际上栈中存储的是方法执行时的基本数据类型和对象的引用,这里注意是对象的引用,不是对象本体,指向堆中对象的实例。
堆分为哪几部分呢
- 新生代:新生代分为Eden伊甸区和幸存区。在伊甸区中,大多数新创建的对象都会放在这里,Eden区相对较少,当Eden区满时,会触发一个Minor GC(轻GC)。在幸存区中,通常分为两个大小相等的部分,每次Minor GC时,存活下来的对象会被移动到其中的一个幸存区,以继续他们的生命周期。
- 老年代:存放过一次或多次Minor GC仍存活的对象会被移动到老年代。老年代的生命周期较长,因此Full GC发生频率较低,但是执行时间比Minor GC长,老年代空间比新生代长。
- 元空间:从Java8开始,永久代被元空间取代,用于存储类的元信息,如类的结构信息等。元空间不在堆红,而是使用了本地内存,解决了永久代OOM的问题。
- 大对象区:在某些JVM实现中引入了大对象区,指需要大量连续的内存空间的对象,如大数组,这类对象直接放在老年代,避免年轻代的晋升而导致内存碎片化。
方法区的执行过程
- 解析方法调用:JVM会根据方法的符号引用找到实际方法地址。
- 栈帧创建:在调用一个方法前,JVM会在当前线程的Java虚拟机栈中为该方法分配一个新的栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口信息。
- 执行方法:执行方法内的字节码命令,涉及的操作可能包括局部变量的读写等操作。
- 返回处理:方法执行完毕后,可能会返回一个结果给调用者,并清理当前栈帧,恢复调用者的执行环境。
String保存在那里?
String是包存在字符串常量池中,不同于其他对象,他的值是不可变的,可以被多个引用共享。
引用类型有哪些?有什么区别
- 强引用类型:是代码中普遍的赋值方式,比如A a = new A();这样的发过誓。强引用关联的对象,永远不会被垃圾回收器回收。
- 软引用类型:可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会针对这样的对象进行回收。
- 弱引用:可以用WeakReference来描述,他的强度比软引用低,弱引用的对象下一次GC的时候一定会被回收,不管内存是否足够。
- 虚引用:幻影引用,是最弱的引用关系,他必须和ReferenceQueue一起使用,当GC发生时,虚引用也会被回收。可以用虚引用来管理堆外内存。
弱引用了解吗?举例说明
Java中的弱引用是一种引用类型,他不会阻止一个对象被垃圾回收。
- 缓存系统:弱引用常用来实现缓存,特别是当希望缓存项能够在内存压力下自动释放时。如果缓存的大小不受控制,可能会导致内存溢出。使用弱引用来维护缓存,可以让JVM在需要更多内存的时候自动清理这些对象。
- 对象池:在对象池中,弱引用可以用来管理那些暂时不用的对象。当对象不再被强引用时,他可以被垃圾回收,释放内存。
- 避免内存泄漏:当一个对象不应该被长期引用时,使用弱引用可以防止该对象被意外的保留,避免的潜在的内存泄漏。
内存泄漏和内存溢出的理解
内存泄露:内存泄露通常是在程序运行中不再使用的对象仍然被引用,从而无法被垃圾回收器回收,从而导致可用内存逐渐变少,虽然在Java中,垃圾回收机制会自动回收不再引用的对象,但是仍有对象不会被回收,最终导致程序内存不断增加。
导致内存泄漏的原因
- 静态集合:使用静态的数据结构存储对象,且没有清理。
- 事件监听:未取消对事件的监听,导致对象被持续引用
- 线程:未停止的线程可能持有对象的引用,无法被回收
内存溢出:内存溢出指的是Java虚拟机在申请内存时,无法找到足够的内存,最终引发OOM。
内存溢出主要原因
- 大量对象创建:程序中不断创建大量对象,超出JVM堆的限制。
- 持久引用:大型数据结构长时间持有对象的引用,导致内存积累
- 递归调用:深度递归导致栈溢出
JVM内存结构中有哪几种内存溢出的情况
- 堆内存溢出:当出现OOM时,就是堆内存溢出了,原因是代码中可能存在大对象分配。或者发生了内存泄漏,导致多次GC之后,仍无法找到一个合适的空间存放当前对象。
- 栈内存溢出:如果我们写一个程序不断的递归调用,而且没有退出条件,就回导致不断的进行压栈。类似于这种情况会JVM会抛出:SOF。如果JVM试图扩展栈空间失败,则直接报出OOM。
- 元空间溢出:出现这个异常是系统的代码非常多或者引用了过多的第三方包或者通过动态代码加载类的操作,导致元空间内存被压满
- 直接内存溢出:在使用ByteBuffer会使用到,很多JavaNIO的框架(Neety和Vert.x)被封装为其他的方法会抛出OOM。