目录
把局部变量放到堆里会有什么问题? 没法被正常回收:(内存泄漏、增大垃圾回收的负担)
把对象动态分配到栈中会有什么问题?(生命周期、空间限制、无法共享(线程私有))
什么是 JVM?
JVM,也就是 Java 虚拟机,它是 Java 实现跨平台的基石。
Java 程序运行的时候,编译器会将 Java 源代码(.java)编译成平台无关的 Java 字节码文件(.class),接下来对应平台的JVM 会对字节码文件进行解释,翻译成对应平台的机器指令并运行。
说说 JVM 的组织架构
JVM 大致可以划分为三个部门:类加载器、运行时数据区和执行引擎。
① 类加载器
负责从文件系统、网络或其他来源加载 Class 文件,将 Class 文件中的二进制数据读入到内存当中。
② 运行时数据区
JVM 在执行 Java 程序时,需要在内存中分配空间来处理各种数据,这些内存区域主要包括方法区、堆、栈、程序计数器和本地方法栈。
③ 执行引擎
执行引擎是 JVM 的心脏,负责执行字节码。它包括一个虚拟处理器,还包括即时编译器(JIT Compiler)和垃圾回收器(Garbage Collector)。
内存区域
讲一下JVM内存结构?
JVM内存结构分为5大区域,线程私有:程序计数器、虚拟机栈、本地方法栈,线程共享:堆、方法区。
HotSpot在JDK1.8之前方法区就是永久代,永久代就是方法区。
JDK1.8后删除了永久代,改为元空间,元空间在直接内存中。方法区就是元空间,元空间就是方法区。
创建一个线程,JVM就会为其分配一个私有内存空间,其中包括程序计数器、虚拟机栈和本地方法栈
程序计数器是什么?
程序计数器是一块较小的内存空间,是线程私有的,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址
程序计数器主要有两个作用:
- 当前线程所执行的字节码的行号指示器,通过它实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,当线程被切换回来的时候能够知道它上次执行的位置。
程序计数器是唯一个不会出现OOM的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
如果线程正在执行 Java 方法,计数器记录正在执行的虚拟机字节码指令地址。如果是本地方法,计数器值为Undefined。
Java 虚拟机栈的作用?
Java虚拟机栈也是线程私有的,每个线程都有各自的 Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息(返回地址)。每一次函数调用都会有一个对应的栈帧被压入虚拟机栈,每一个函数调用结束后,都会有一个栈帧被弹出。
---------------------------------------------------------------------------------------------------
局部变量表是用于存放方法参数和方法内的局部变量。
每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,在方法调用过程中,会进行动态链接,将这个符号引用转化为直接引用。
- 部分符号引用在类加载阶段的时候就转化为直接引用,这种转化就是静态链接
- 部分符号引用在运行期间转化为直接引用,这种转化就是动态链接
Java虚拟机栈也是线程私有的,每个线程都有各自的 Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
Java虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。
- 线程请求的栈深度大于虚拟机允许的深度抛出StackOverflowError。
- 如果JVM 栈容量可以动态扩展,栈扩展无法申请足够内存抛出 OutofMemoryError (HotSpot 不可动态扩展,不存在此问题)。
可以通过 -Xss 参数来指定每个线程的虚拟机栈内存大小: java -Xss 2M
本地方法栈的作用?
本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为本地方法Native服务,由 C/C++ 编写。
调用本地方法时虚拟机栈保持不变,动态链接并直接调用指定本地方法
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。
-------------------------------------------------
虚拟机规范对本地方法栈中方法的语言与数据结构无强制规定,虚拟机可自由实现,例如 HotSpot 将虚拟机栈和本地方法栈合二为一。
本地方法栈在栈深度异常和栈扩展失败时抛出StackOverflowError、OutOfMemoryError。
堆的作用是什么?
堆是虚拟机所管理的内存中最大的一块,被所有线程共享的,在虚拟机启动时创建,主要用来存放对象实例,Java 里几乎所有对象实例都在堆分配内存。堆可以处于物理上不连续的内存空间,逻辑上应该连续,但对于例如数组这样的大对象,多数虚拟机实现出于简单、存储高效的考虑会要求连续的内存空间。
- 堆用于存放对象实例,是垃圾收集器管理的主要区域,因此也被称作GC堆。
- 堆可以细分为:新生代(Eden 空间、From Survivor、 To Survivor 空间)和老年代 old Memory。
- 通过-Xms设定程序启动时占用内存大小,通过-Xmx设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。 java - Xms 1M -Xmx 2M
从 JDK 7 开始,JVM 已经默认开启逃逸分析了,意味着如果某些方法中的对象引用没有被返回或者未被方法体外使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。
堆和栈的区别是什么?
堆属于线程共享的内存区域,几乎所有的对象都在堆上分配,生命周期不由单个方法调用所决定,可以在方法调用结束后继续存在,直到不再被任何变量引用,然后被垃圾收集器回收。
栈属于线程私有的内存区域,主要存储局部变量、方法参数、对象引用等,通常随着方法调用的结束而自动释放,不需要垃圾收集器处理。
方法区的作用是什么?
方法区与Java 堆一样,是各个线程共享的内存区域,但是方法区并不真实存在,属于 Java 虚拟机规范中的一个逻辑概念,用于存储已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等。
在 HotSpot 虚拟机中,方法区的实现称为永久代(PermGen),但在 Java 8 及之后的版本中,已经被元空间(Metaspace)所替代。
虚拟机规范对方法区的约束宽松,除和堆一样不需要连续内存和可选择固定大小/可扩展外,还可以不实现垃圾回收。对方法区进行垃圾回收的主要目标是对常量池的回收和对类的卸载, 如果方法区无法满足新的内存分配需求,将抛出 OutOfMemoryError。
- 永久代
方法区是JVM的规范,而永久代 PermGen是方法区的一种实现方式,并且只有HotSpot有永久代。对于其他类型的虚拟机,如JRockit没有永久代。由于方法区主要存储类的相关信息,所以对于动态生成类的场景比较容易出现永久代的内存溢出。(反射)
- 元空间
JDK 1.8的时候,HotSpot的永久代被彻底移除了,使用元空间替代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。两者最大的区别在于: 元空间并不在虚拟机中,而是使用直接内存。
说一下 JDK1.6、1.7、1.8 内存区域的变化?
JDK1.6、1.7/1.8 内存区域发生了变化,主要体现在方法区的实现:
- JDK1.6 使用永久代实现方法区:
- JDK1.7 时发生了一些变化,将字符串常量池、静态变量,存放在堆上
- 在 JDK1.8 时彻底干掉了永久代,而在直接内存中划出一块区域作为元空间,运行时常量池、类常量池都移动到元空间。
为什么要将永久代替换为元空间呢?
永久代内存受限于JVM可用内存,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是相比永久代内存溢出的概率更小。
运行时常量池的作用是什么?
运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容在类加载后存放到运行时常量池。一般除了保存Class 文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。
运行时常量池相对于Class文件常量池的一个重要特征是动态性,Java不要求常量只有编译期才能产生,运行期间也可以将新的常量放入池中,这种特性利用较多的是String的intern 方法。
运行时常量池是方法区的一部分,受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError.
运行时常量池是方法区的一部分,在类加载之后,会将编译器生成的各种字面量和符号引号放到运行时常量池。在运行期间动态生成的常量,如 String类的intern()方法,也会被放入运行时常量池。
直接内存是什么?
直接内存不属于运行时数据区,也不是虚拟机规范定义的内存区域,但这部分内存被频繁使用,而且可能导致内存溢出。
NIO的Buffer提供了DirectBuffer,可以直接访问系统物理内存,避免堆内内存到堆外内存的数据拷贝操作,提高效率。DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制,不受最大堆内存的限制。
直接内存的读写操作比堆内存快,可以提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到直接内存。
直接内存的分配不受Java 堆大小的限制,但还是会受到本机总内存及处理器寻址空间限制,一般配置虚拟机参数时会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使内存区域总和大于物理内存限制,导致动态扩展时出现 OOM。
由直接内存导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现内存溢出后产生的Dump文件很小,而程序中又直接或间接使用了直接内存(典型的间接使用就是NIO),那么就可以考虑检查直接内存方面的原因、
内存溢出和内存泄漏的区别?
内存溢出 OutofMemory, 指程序在申请内存时,没有足够的内存空间供其使用。
内存泄露 Memory Leak, 指程序在申请内存后,无法释放已申请的内存空间,内存泄漏最终将导致内存溢出。
在 Java 中,内存泄漏通常发生在长期存活的对象持有短期存活对象的引用,而长期存活的对象又没有及时释放对短期存活对象的引用,从而导致短期存活对象无法被回收。(跨代引用)
栈溢出的原因?
由于HotSpot不区分虚拟机栈和本地方法栈,设置本地方法栈大小的参数没有意义,栈容量只能由 -Xss 参数来设定,存在两种异常:
- StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度,将产生StackOverflowError,例如一个递归方法不断调用自己。该异常有明确错误堆栈可供分析,容易定位到问题所在。
- OutofMemoryError: 如果jvm栈可以动态扩展,当扩展无法申请到足够内存时会抛出 OutofMemoryError.
HotSpot不支持虚拟机栈扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现OOM,否则在线程运行时是不会因为扩展而导致溢出的。比如线程启动过多就会出现这种情况
运行时常量池溢出的原因?
String的 intern方法是一个本地方法,作用是如果字符串常量池中已包含一个等于此 String 对象的字符串,则返回池中这个字符串的 String 对象的引用,否则将此 String 对象包含的字符串添加到常量池并返回此 String 对象的引用。
在JDK6及之前常量池分配在永久代,因此可以通过-XX:PermSize和-XX:MaxPermsize限制永久代大小,间接限制常量池。如果在循环中不断创建新的字符串对象,并且调用 intern()
方法将其添加到常量池中,而没有及时释放内存,最终会导致常量池溢出。在JDK7后不会出现该问题,因为存放在永久代的字符串常量池已经被移至堆中。
while (true) {
String str = new String("Hello").intern();
}
方法区溢出的原因?
方法区主要存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。只要不断在运行时产生大量类,方法区就会溢出。例如使用JDK反射或 CGLib 直接操作字节码在运行时生成大量的类。很多框架如 Spring、Hibernate 等对类增强时都会使用 CGLib这类字节码技术,增强的类越多就需要越大的方法区保证动态生成的新类型可以载入内存,也就更容易导致方法区溢出。
JDK8 使用元空间取代永久代,HotSpot 提供了一些参数作为元空间防御措施,例如XX:Metaspacesize指定元空间初始大小,达到该值会触发 GC 进行类型卸载,同时收集器会对该值进行调整,如果释放大量空间就适当降低该值,如果释放很少空间就适当提高。
内存泄漏可能由哪些原因导致呢?
内存泄漏可能的原因有很多种,比如说静态集合类引起内存泄漏、单例模式、数据连接、IO、Socket 等连接、变量不合理的作用域、hash 值发生变化、ThreadLocal 使用不当等。
①、静态集合类引起内存泄漏
静态集合的生命周期和 JVM 一致,所以静态集合引用的对象不能被释放。
②、单例模式
和上面的例子原理类似,单例对象在初始化后会以静态变量的方式在 JVM 的整个生命周期中存在。如果单例对象持有外部的引用,那么这个外部对象将不能被 GC 回收,导致内存泄漏。
③、数据连接、IO、Socket 等连接
创建的连接不再使用时,需要调用 close 方法关闭连接,只有连接被关闭后,GC 才会回收对应的对象(Connection,Statement,ResultSet,Session)。忘记关闭这些资源会导致持续占有内存,无法被 GC 回收
④、变量不合理的作用域
一个变量的定义作用域大于其使用范围,很可能存在内存泄漏;或不再使用对象没有及时将对象设置为 null,很可能导致内存泄漏的发生。
⑤、hash 值发生变化
对象 Hash 值改变,使用 HashMap、HashSet 等容器中时候,由于对象修改之后的 Hah 值和存储进容器时的 Hash 值不同,所以无法找到存入的对象,自然也无法单独删除了,这也会造成内存泄漏。说句题外话,这也是为什么 String 类型被设置成了不可变类型。
⑥、ThreadLocal 使用不当
ThreadLocal 的弱引用导致内存泄漏也是个老生常谈的话题了,使用完 ThreadLocal 一定要记得使用 remove 方法来进行清除。
OOM发生在JVM的哪一块内存空间?
- 堆内存溢出:当出现java.lang.OutOfMemoryError:Java heap space异常时,就是堆内存溢出了。原因是代码中可能存在大对象分配,或者发生了内存泄露,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。
- 栈溢出:如果我们写一段程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM 实际会抛出 StackOverFlowError;当然,如果 JVM 试图去扩展栈空间的的时候失败,则会抛出 OutOfMemoryError。
- (方法区)元空间溢出:元空间的溢出,系统会抛出java.lang.OutOfMemoryError: Metaspace。出现这个异常的问题的原因是系统的代码非常多或引用的第三方包非常多或者通过动态代码生成类加载等方法,导致元空间的内存占用很大。
- 直接内存溢出:在使用ByteBuffer中的allocateDirect()的时候会用到,很多javaNIO(像netty)的框架中被封装为其他的方法,出现该问题时会抛java.lang.OutOfMemoryError: Direct buffer memory异常。
把局部变量放到堆里会有什么问题? 没法被正常回收:(内存泄漏、增大垃圾回收的负担)
- 内存泄漏:如果局部变量被放置在堆中,且没有正确地进行释放或管理,可能会导致内存泄漏。内存泄漏指的是不再使用的对象仍然存在于内存中,无法被垃圾回收器回收,从而占用了宝贵的内存资源。
- 性能降低:将局部变量放在堆中会增加垃圾回收的负担。垃圾回收器需要扫描堆中的对象,找到不再使用的对象进行回收。如果堆中存在大量的局部变量对象,垃圾回收的时间会增加,可能会导致程序的性能下降。
把对象动态分配到栈中会有什么问题?(生命周期、空间限制、无法共享(线程私有))
- 生命周期限制:栈中的对象的生命周期与其所在的方法调用相关联。当方法调用结束时,栈中的对象会自动释放,无法在方法之外访问。如果需要在方法之外继续使用对象,就无法将其放置在栈中。
- 空间限制:栈的大小是有限的,并且在编译时就确定了。如果对象较大或者栈空间较小,将对象放置在栈中可能会导致栈溢出的问题。
- 不适用于共享和跨方法访问:栈是线程私有的,栈中的对象无法被其他线程或其他方法访问。如果需要在多个方法之间共享对象,或者在方法调用之外访问对象,将其放置在栈中是不可行的。
什么是Java内存模型(JMM)
- Java 内存模型(Java Memory Model)是一种抽象的模型,简称 JMM,主要用来定义多线程中变量的访问规则,用来解决变量的可见性、有序性和原子性问题,确保在并发环境中安全地访问共享变量。
- 从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系 : 线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。
- 本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
线程A与线程B之间要通信的话,必须要经历下面2个步骤:
a. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
b. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
自己学习整理 借鉴很多博主 感谢他们