目录
JVM介绍:
解释:
特点:
整体构成:
执行过程:
运行时数据区:
Java堆剖析:
堆内存区域划分
为什么要分代呢?
内存分配:
新生区与老年区配置比例:
分代收集思想 Minor GC、Major GC、Full GC:
堆空间的参数设置 :
方法区:
方法区大小:
垃圾回收:
垃圾回收:
GC主要干什么?
垃圾回收相关算法:
垃圾标记阶段算法
对象的 finalization 机制 :
垃圾回收阶段算法:
标记-复制算法 :
标记-清除算法:
标记-压缩算法:
内存泄漏:
内存溢出:
Stop the World:
JVM常见问题以及决解方法:
1. 内存溢出(OutOfMemoryError):
2. JVM 启动慢或性能瓶颈:
3. 线程问题(死锁、线程过多):
4.内存泄漏(Memory Leak):
5.JVM 启动失败(JVM Crash):
JVM介绍:
解释:
Java虚拟机,Java的所有程序都会在这个虚拟机上执行。
特点:
1. 一次编译到处运行
2.自动内存管理
3.自动垃圾回收
整体构成:
- 类加载器(ClassLoader)
- 运行时数据区(Runtime Data Area)
- 执行引擎(Execution Engine)
- 本地库接口(Native Interface)
执行过程:
1. java 代码转换成字节码(class 文件)
2. jvm 首先需要把字节码通过一定的方式 类加载器(ClassLoader) 把文件加载到内存中的运行时数据区(Runtime Data Area)
3. 而字节码文件是 jvm 的一套指令集规范,并不能直接交个底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine) 将字节码翻译成底层系统指令再交由CPU 去执行,而这个过程中需要调用其他语言的接口 本地库接口(Native Interface) 来实现整个程序的功能。
通常所说的 JVM 组成指的是 运行时数据区(Runtime Data Area) ,因为通常需要调试分析的区域就是“运行时数据区”,或者更具体的来说就是“运行时数据区”里面的 Heap(堆)模块。
接下来我将直接介绍运行时数据区:
运行时数据区:
- 程序计数器(Program Counter Register)
程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。
- Java 虚拟机栈(Java Virtual Machine Stacks)
描述的是 Java 方法执行的内存模型,每个方法在执行的同时都会创建一个线帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每个方法从调用直至执行完成的过程,都对应着一个线帧在虚拟机栈中入栈到出栈的过程。
- 本地方法栈(Native Method Stack)
与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的。
- Java 堆(Java Heap)
是 Java 虚拟机中内存最大的一块,是被所有线程共享的,在虚拟机启动时候创建,Java 堆唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存.
- 方法区(Methed Area)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。方法区是很重要的系统资源,是硬盘和 CPU 的中间桥梁,承载着操作系统和应用程序的实时运行.
线程间共享:堆,方法区. 线程私有:程序计数器,栈,本地方法栈.
Java堆剖析:
- 堆在jvm中只存在一个实例,它被所有线程所共享。
- 堆可以设置大小Xms(堆的起始大小)-----Xmx(堆的最大值),但一般设置起始值和最大值相等,这样就可以减少GC(垃圾回收)后的内存程序分配,可以提高效率。
- 堆可以是在物理上不连续的空间,但在逻辑上是连续的。
- 方法结束后,堆中的对象会马上失效,但是不会马上被移除,只有GC后才会移除。
- 堆是GC(垃圾回收器)重点回收的区域。
- 所有对象实例都在运行时分配到堆上。
堆内存区域划分
Java8 及之后堆内存分为 :新生区(新生代)+老年区(老年代)
新生区分为 Eden(伊甸园)区和 Survivor(幸存者)区
为什么要分代呢?
将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及 GC 频率。
针对分类进行不同的垃圾回收算法,对算法扬长避短。
内存分配:
- new的对象会被放到新生区的Eden区,该区域有大小限制。
- 当Eden区满了后,又有新的对象要被创建,这时候就会触发GC,对Eden区进行垃圾回收,将没有被引用的对象进行回收,这时候就为新的对象清理出新的空间。
- 这时候如果没有被GC回收的,会被放入到Survivor0区当中。
- 当下一次触发垃圾回收时,Survivor0区当中没有被清理的对象会被移动到Survivor1区当中,每次都会保证他们2个当中有一个幸存者区是空的。
- 再次经历垃圾回收,剩余的对象又会被从Survivor1区当放入到Survivor0区当中。
- 那什么时候去养老去呢?这个我们是可以通过-XX:MaxTenuringThreshold=<N>参数设置的,默认是15次垃圾回收后,对象头当中有4位数据对GC年龄进行保存,1111,所有默认是15次。
- 当老年区是相对很悠闲的,当老年区满了后会再次触发垃圾回收,如果回收后还是满的话,就会产生 OOM 异常. Java.lang.OutOfMemoryError:Java heap space。内存溢出。
新生区与老年区配置比例:
配置新生代与老年代在堆结构的占比(一般不会调)
默认**-XX:NewRatio**=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1/3
可以修改**-XX:NewRatio**=4,表示新生代占 1,老年代占 4,新生代占整个堆的 1/5
当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整老年代的大小,来进行调优。
分代收集思想 Minor GC、Major GC、Full GC:
JVM在GC时,并非是每次都新生区的老年区一起回收,更多的是对新生区进行回收,针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类型:一种是部分收集,一种是整堆收集。
部分收集,不是整个Java堆的收集:分为:老年区收集,新生区(Eden,S0,S1)收集 。
整堆收集:对整个Java堆和方法区的垃圾进行收集。 整堆收集的情况分为:system.gc;不足时;老年区空间不足时;方法区空间不足时;开发期间应该避免整堆收集。
堆空间的参数设置 :
-XX:+PrintFlagsInitial 查看所有参数的默认初始值
-XX:+PrintFlagsFinal 查看所有参数的最终值(修改后的值)
-Xms 初始堆空间内存(默认为物理内存的 1/64)
-Xmx 最大堆空间内存(默认为物理内存的 1/4)
-Xmn 设置新生代的大小(初始值及最大值)
-XX:NewRatio 配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio 设置新生代中 Eden 和 S0/S1 空间比例
-XX:MaxTenuringTreshold 设置新生代垃圾的最大年龄
XX:+PrintGCDetails 输出详细的 GC 处理日志
方法区:
方法区用来保存类,如果系统定义了太多的类,导致方法区溢出, 虚拟机同样会抛出内存溢出的错误。
方法区大小:
- 方法区大小也是不一定是固定的,可以设置大小。
- 他的大小可以由-XX:MetaspaceSize 和 -XX:MaxMataspaceSize 指定。
- 默认值依赖于平台,windows 下,-XXMetaspaceSize 是 21MB。
- -XX:MaxMetaspaceSize 的值是-1,最大值默认为无限制。
- 而-XX:MetaspaceSize 初始值是 21M 也称为高水位线 一旦触及就会触发 Full GC。
- 为了避免频繁的Full GC,我们可以把-XX:MetaspaceSize 初始值设置大一点。
垃圾回收:
方法区的垃圾收集主要回收两部分内容:运行时常量池中废弃的常量和不再使用的类型。
废弃常量:没有任何对象引用它。
废弃类:
- 该类的所有实例被回收。
- 该类的类加载器被回收。
- 该类对象没有在任何地方被引用。
垃圾回收:
垃圾是指没有被任何引用指向的对象,如果不对垃圾进行清理,那么垃圾就会一直占用内存空间,直到应用结束,导致其他对象使用不到内存空间,甚至内存溢出。
GC主要干什么?
它将不被使用到的对象释放,也可以清理内存里的记录碎片,将所有的对象移动到堆的一段。
垃圾回收相关算法:
垃圾标记阶段算法
- 垃圾标记阶段算法:
那些不再被使用到的对象就被标记为垃圾,只有标记为垃圾对象,在GC阶段才会被回收。
标记垃圾对象的算法有2种:引用计数算法和可达性分析算法。
* 引用计数算法
每个对象都有一个引用计数器属性,只要对象被引用1次,计数器就会+1。但会增加存储空间的消耗,并且每次计数都会增加时间的消耗,最重要的是,如果2个对象相互引用,形成循环的话,是无法处理的。
* 可达性分析算法
可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
基本思路如下:
1. 从根"GCRoots”开始,从上至下的搜索被根对象连接到目标对象是否可达 。
2. 使用可达性分析算法后,目标内存中的存活对象都会被根直接或间接连接着,搜索走过的路径成为引用链。
3.没有被引用链相连的就是不可达的,标记为垃圾对象。
”GCRoots“可以由:如:方法区中所有类的静态成员变量(包括静态字段); 常量池中的字符串常量、数字常量; 当前活动的线程在执行过程中可能会引用其他对象,因此活动线程可以作为 GC Roots; 被同步锁 synchronized 持有的对象; 基 本 数 据 类 型 对 应 的 Class 对 象 , 一 些 常 驻 的 异 常 对 象 ( 如 : NullPointerException、OutofMemoryError)。
对象的 finalization 机制 :
Java提供对象终止机制,它是对象销毁之前的自定义处理逻辑。对象销毁之前终会先调用对象的finalize()方法,每个finalize()方法只能被调用一次。
finalize()方法它允许被子类重写,一般是用来释放资源、清理工作,比如关闭文件,套接字,数据库连接等。
为此,定义虚拟机中的对象可能的三种状态 。如下:
- 可触及状态:从根节点可触及
- 可复活状态:对象的所有引用都被释放,但在finalize()方法中可能被复活
- 不可触及状态:对象的 finalize()被调用,并且没有复活,那么就会进入不可触及状态。
上面三种状态,当finalize()存在进行区分,只有不可复活状态才会被回收。
例:判断一个对象A是否被回收,至少需要判断2次:
- 判断根节点无法触及对象,标记为一次。
- 判断对象的finalize()方法,有没有必要执行:
①对象的finalize()方法没有被重写,或者finalize()方法已经被执行了,虚拟机认为没有必要执行,A就被判定为不可触及状态
②对象的finalize()方法被重写,并且还没有被执行,会被加入到finalizer队列当中去等待执行finalize()方法,当执行finalize()方法时,如果与任何对象建立联系,那么A对象就会被移除“即将回收”集合。当下次这个情况,finalize()方法不会被再次调用,而是会直接判定为不可触及状态。
垃圾回收阶段算法:
标记-复制算法 :
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:
- 没有标记和清除阶段,运行简单。
- 复制过去后保证空间的连续性,不会出现“碎片”问。
缺点:
- 需要2倍的内存空间。
应用场景:
一般用在新生代当中,因为新生代中垃圾对象很多,需要复制的对象相对较少,这样效率较高。
标记-清除算法:
执行过程
当堆中的有效内存空间被耗尽的时候,然后进行这项工作.
标记:在这个阶段,JVM会遍历所有的对象,标记那些被根对象(GC Root)可达的对象。
清除:在标记阶段完成后,JVM会遍历堆中的所有对象,清除那些未被标记的对象。未被标记的对象意味着它们不再被任何根对象引用,因此可以回收它们的内存。
标记-压缩算法:
第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
优点:
- 消除了复制算法当中,内存减半的高额代价。
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可。
缺点:
- 移动过程中,需要全程暂停用户应用程序。即:STW
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
内存泄漏:
内存溢出是指程序在运行过程中请求的内存超过了JVM可用内存的限制,导致无法为新的对象分配内存。JVM会抛出 OutOfMemoryError
错误,表示堆内存或其他区域的内存不足,无法继续分配。
内存溢出:
内存泄漏是指程序中已经不再使用的对象由于某些原因,仍然持有引用,导致这些对象无法被垃圾回收器回收,从而造成内存资源的浪费。虽然程序的运行没有超出可用内存的限制,但由于内存被不断占用,最终可能导致系统性能下降,甚至内存溢出。
例:
- Threadlocal:
ThreadLocal
在某些情况下会导致内存泄漏,尤其是当线程池中的线程没有被正确清除时,ThreadLocal
的数据不会被回收。 - 一些提供 close()的资源未关闭导致内存泄漏
数据库连接 dataSourse.getConnection(),网络连接 socket 和 io 连接必须手动 close,否则是不能被回收的。
Stop the World:
指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为 STW。
可达性分析算法中枚举根节点(GC Roots)会导致所有 Java 执行线程停顿:
- 分析工作必须在一个能确保一致性的快照中进行
JVM常见问题以及决解方法:
1. 内存溢出(OutOfMemoryError):
解决方法:
- 使用
-Xms
和-Xmx
设置堆内存的初始值和最大值,确保堆内存足够大。 - 启用 GC日志(
-Xlog:gc*
),查看GC频率和停顿时间,了解垃圾回收的情况。 - 分析内存泄漏:可以使用
jmap
和 Heap Dump 分析工具来识别没有被垃圾回收的对象,检查是否有不再使用的对象被错误地保留引用。 - 检查 JVM 配置:合理配置堆内存,避免频繁 Full GC。适当调整 新生代 和 老年代 的比例(如
-XX:NewRatio
)。
2. JVM 启动慢或性能瓶颈:
- 分析启动参数:使用合适的 JVM启动参数,比如
-XX:+UseG1GC
、-Xms
、-Xmx
,确保堆内存设置合理。 -
类加载问题:应用启动时大量的类加载可能会拖慢启动速度。可以通过调整类加载器、类缓存等方式优化类加载速度。
-
代码优化:使用合适的数据结构和算法,减少不必要的计算,优化程序逻辑。
3. 线程问题(死锁、线程过多):
- 死锁通常表现为两个或多个线程互相等待对方释放锁。分析线程转储中的锁信息,找出死锁源。
- 检查线程池配置:确保线程池大小和队列容量合适,避免线程池溢出。可以调整
ThreadPoolExecutor
的核心线程数、最大线程数等参数。
4.内存泄漏(Memory Leak):
- Heap Dump 分析:在内存消耗过多时,生成 Heap Dump 文件,并使用 MAT 或 VisualVM 等工具分析内存使用情况,找出占用内存的对象。
- Threadlocal在线程池的情况下数据没有被清理;数据库连接,socket,io等没有close。
5.JVM 启动失败(JVM Crash):
- 内存配置问题:确保内存配置合理,过大的堆内存或直接内存可能导致 JVM 崩溃。
- JVM版本兼容性问题:确认使用的 JVM 版本与操作系统、硬件架构等兼容,确保没有已知的 JVM bug。
- 查看 Crash Log:JVM 崩溃时,通常会生成 hs_err_pid.log 文件,查看其中的错误堆栈信息,查找崩溃的原因。