文章目录
JVM虚拟机
一、基础
1、JVM定义
规范,不同实现hotspot,作用将class字节码文件加载到JVM中,通过解释器解释为操作系统认识的机器码。
1.垃圾回收机制
2.一次编码,到处运行,跨平台。
3.JIT对热点代码优化(锁消除)
2、JVM知识图谱
1.类加载器,双亲委派,加载字节码文件的过程
2.内存结构,放入jvm内存结构的不同地方
3.解释器解释执行,GC回收
4.本地方法接口,本地方法,与操作系统交流的native方法
二、类加载机制
1、class文件
使用javac xx.java编译为xxx.class二进制字节码文件,再由java xxx.class,使用类加载器将java二进制字节码文件加载到JVM内存中,解释器将字节码解释为操作系统识别的机器码(对热点代码进行编译,即解释+编译)
编译期优化,运行期优化
魔数,0-3字节表示是否为class类型的文件,cafe babe
javap -v xxx.class 将java字节码文件反解析我们看得懂的字节码命令信息。
javap可以反编译(即对javac编译的文件进行反编译),也可以查看java编译器生成的字节码。用于分解class文件。
i++和++i的区别从底层字节码角度分析可参照https://blog.csdn.net/u013541707/article/details/112513620
2、类加载过程
-
加载:将class文件加载到jvm中,同时会生成一个Class类对象。
类文件的相关信息存储在方法区中 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
-
链接-验证:验证字节码文件是否符合JVM规范,比如魔数是否为class文件
-
链接-准备:正式为类变量分配内存并设置类变量初始值的阶段
-
链接-解析:将符号引用转为直接引用,这里是静态链接
1.魔数cafe babe,java版本等 2.jdk1.7之后类变量在存储在类对象里(堆),之前在方法区 3.将符号引用转为直接引用(静态链接和方法区中动态链接),关注点在class文件常量池 什么是解析?把符号引用变为直接引用。比如com.test.Car里面有一个com.test.Wheel类,在编译时Car类并不知道Wheel类的实际内存地址,此时com.test.Wheel只是一个符号,如#2,javap -v 常量池中可以查看到。“解析”的意思就是把被引用的类加载入内存,然后将com.test.Wheel这个符号变成一个指针,能够定位到内存中目标。
-
初始化-cinit方法:初始化阶段是执行类构造器()方法的过程
clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的 https://blog.csdn.net/qq_31156277/article/details/80188110
3、类加载器
-
分类
启动类加载器:jdk自带的一些常用类,比如Object,String,List等各种jdk里面的类
扩展类加载器,javax是拓展类,比如zipfs.jar,Zip File System Provider(文件压缩包)
系统类加载器(应用程序类加载器),自己写的类
自定义类加载器
-
Java虚拟机的入口
当我们运行一个Java程序时,首先是JDK安装目录下的jvm.dll启动虚拟机,而sun.misc.Launcher类就是虚拟机执行的第一段Java代码。之前提到,除BootstrapClassLoader以外,其他的类加载器都是用Java实现的——在Launcher里你就可以看到它们。
-
双亲委派
双亲委派:一句话,有事找我最高的爹启动类加载器,找的到用,找不到就下一层找
如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载
https://zhuanlan.zhihu.com/p/73359363
确保类的全局唯一性。
如果你自己写的一个类与核心类库中的类重名,会发现这个类可以被正常编译,但永远无法被加载运行。因为你写的这个类不会被应用类加载器加载,而是被委托到顶层,被启动类加载器在核心类库中找到了。如果没有双亲委托机制来确保类的全局唯一性,谁都可以编写一个java.lang.Object类放在classpath下,那应用程序就乱套了
-
类的唯一性和防止重复加载,类与类加载器
在Java中任意一个类都是由这个类本身和加载这个类的类加载器来确定这个类在JVM中的唯一性. 也就是你用你A类加载器加载的com.aa.ClassA和你B类加载器加载的com.aa.ClassA它们是不同的,也就是用instanceof这种对比都是不同的。所以即使都来自于同一个class文件但是由不同类加载器加载的那就是两个独立的类。 在JVM中表示两个class对象是否为同一个类对象存在两个必要条件: 类的完整类名必须一致,包括包名。 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。 也就是说,在JVM中,即使这个两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的,这是因为不同的ClassLoader实例对象都拥有不同的独立的类名称空间,所以加载的class对象也会存在不同的类名空间中, 主要保证避免重复加载 + 避免核心类篡改 Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心JavaAPI发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
-
沙箱安全机制
是由基于双亲委派机制上,采取的一种JVM的自我保护机制,假设自己写一个java.lang.String 的类,在类中自定义方法,由于双亲委派机制的原理,此请求会先交给Bootstrap试图进行加载,但是Bootstrap在加载类时首先通过包和类名查找rt.jar中有没有该类,有则优先加载rt.jar包中的类,没有自定义方法会报错,因此就保证了java的运行机制不会被破坏。
三、内存结构
jvm内存结构,也就是jvm运行时数据区
1、程序计数器
- 抽象的概念,实际物理是寄存器。记录的是当前当前当前当前当前当前当前当前线程执行的字节码指令地址 ,用于执行下一条指令。若是Native 方法,则这个技术器值为空(Undefine d)。
- 特点,线程是有的,唯一一个,没有内存溢出。
2、虚拟机栈
-
线程虚拟机栈是用于描述java方法执行的内存模型。私有的,一个栈帧(Frame)对应运行一个方法的调用时需要的内存空间。
-
栈是先进后出。只有一个活动栈帧,就是线程正在执行的方法,就是最上面的栈帧。
-
栈帧的结构分为“局部变量表、操作数栈、动态链接、方法出口”
各个部分的作用: 1、局部变量表(Local Variable Table) 是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位。javap -v 可以看到 2、操作数栈(Operand Stack)也常称为操作栈 计算的地方,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者 iload iadd等 3、动态链接: java的字节码文件中的常量池存放了大量的符号引用, 比如main方法中System.out.println("1"),其中类System,类变量out和println方法,在编译之后的文件中都是符号引用,代表本类需要它,但是并没有把他们全部加到本类的这个信息中,因为一个类中的需要的信息太多了,不能全部加进来,等加载的时候变为直接引用即可。 这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接,也就是栈中的一个栈帧有的动态链接。 具体可以参考https://blog.csdn.net/u013541707/article/details/112389533 4、方法出口 当一个方法开始执行时,可能有两种方式退出该方法:正常退出和异常退出方法。
-
栈溢出,StackOverFlowError,可能是栈的内存少,而栈帧数量过多或者栈帧的大小过大
-
线程的诊断方法。
问题:CPU占用过多;很久没运行结果,主要就是找运行cup高进程里面的线程 1、top,找进程,然后使用top -Hp pid看线程或者ps –efL | grep java看线程。 2、top 和jstack 进程号 3、jconsole jvisualVM等动态检查工具
-
常见问题
1.GC不涉及栈内存的回收,设计堆中的无用对象。 2.栈的大小-Xss,并不是越大越好,反而线程会越少,Linux默认1M,win动态确定的。 3.线程的局部变量是否线程安全。安全,但是不出这个方法的作用管理范围。若是static变量,就是对线程共享的,就会有线程安全性问题。
3、本地方法栈
-
本地方法栈
用于描述native方法执行的内存模型。调用本地方法的时候,分配的内存空间,与虚拟机栈是一样的 这些native方法,又C或者C++编写的本地代码与操作系统底层Api打交道。 这些本地方法很多,比如Objec方法的clone方法,hashCode,wait,notify等都是native的方法。线程的start方法底层也是start0为本地方法。 我们常用的HotSpot虚拟机选择合并了虚拟机栈和本地方法栈。
4、堆
下图是jdk1.6,1.8取消永久代为元空间,并在系统内存中。
-
线程共享的,存放对象和数组的地方。因为对象是共享,所以必须考虑线程安全性问题(每个线程有一个自己的缓存区或者加锁);有GC回收无用的对象。
-
堆内存溢出,OutOfMemoryError:Java heap space,就是内存不够,就是对象过大过多等问题产生的,比如很多的大对象,无限的new 对象等。
-
堆内存分为2个大部分,1个是新生代(伊甸区,2个生存区,默认比例为8:1),1个老年代
**1)年轻代(Young Gen 新生代):**年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(命名为A和B)。当对象在堆创建时,将进入年轻代的Eden Space。垃圾回收器进行垃圾回收时,扫描Eden Space和A Suvivor Space,如果对象仍然存活,则复制到B Suvivor Space。再次GC时会扫描伊甸区和B到A中。A和B相互复制。
**2)年老代(Tenured Gen):**年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁(譬如可能几个小时一次)。
1.为什么会有年轻代
我们先来屡屡,为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。
2.什么情况下存入老年代
1.大对象,可以设置一个参数确定。
2.年龄阈值:虚拟机给每个对象定义了一个对象年龄(Age)计数器,经过一次GC就加1
3.动态年龄: Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半
4.在一次安全Minor GC 中,仍然存活的对象不能在另一个Survivor 完全容纳
5、担保机制,主要是新生成的对象新生代装不下,可能把新生的,也可能把它之前的对象放入老年代,不同gc实现不同。
3.何时GC
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:老年代空间不足。
更详细见下面链接
https://blog.csdn.net/yhyr_ycy/article/details/52566105
-
1.8,永久代换成了元空间,Hotspot在1.7之前,方法区的实现是永久代,有jvm管理的内存中,1.8之后实现为元空间,它在系统内存中了。
-
堆的诊断方法
1.jps查询当前java进程。
$> jps
23991 Jps
23651 Resin
2.jmap 堆内存的使用情况快照,静态的,某点的。jmap -heap 进程号
动态:jconsole.jvisualvm检查工具
jmap -heap 进程号
jconsole工具
jvisualvm工具
5、方法区
-
存放类信息(名字,修饰符,方法信息等)、运行时常量池,类加载器.即JIT编译后的热点代码等消息。
-
方法区逻辑上是堆的一部分,但是不同的JVM厂商实现不同。
1.6之前使用永久代,在jvm内存里,但是很GC慢,被废弃了。 1.7之后使用元空间,在操作系统内存了。依然存那些,但是静态变量已经跟着class对象放在堆中了。字符串常量池也在堆中了。 判断方法:OutOfMemoryError:PermGen space和OutOfMemoryError:Metaspace。只加载,但是不运行。
-
常量池、运行时常量池,和字符串常量池,可以参考简单理解常量、常量池、运行时常量池和字符串常量池
常量池(class文件常量池)的是一张表,包含了字面量和符号引用。就是为字节码指令提供一些序号,然后在该类对应的常量表找到它代表的意思,比如类名,方法名,参数类型,字面量等信息。属于某个*.class,某个类的字节码文件。
运行常量池,就是当*.class文件被加载到虚拟机之后,会把常量池放入内存,这个地方叫做运行时常量池之中,并把符号引用转为真实地址引用。
字符串常量池也就是串池StringTable,实际上就是一个hash表,固定长度,数组加链表。
位置
1.6之前,方法区(概念)放入叫做永久代的实现中,串池就在常量池中。但是GC的慢。
1.7之后,就把串池单独放入堆中。
判断,就是把2个区域内存设置小一点,然后创建一个大的字符串对象。
前者OutOfMemoryError:PermGen space;后者OutOfMemoryError:Java heap space
串池也是可以被垃圾回收的GC。
字符串延迟加载的特性,执行到某一行,才会创建对象。
懒惰式的。动态拼接的字符串,只会在堆中创建一个对象。
String s1 =new String("a") 会创建2个对象,一个对象a放入串池,一个对象a在堆中,且s1引用的就是后者。
intern方法:可以参考https://blog.csdn.net/u013541707/article/details/112390993
1.7、1.8之后,intern将字符串对象尝试放入串池,若串池中已经有了,则自己本身依然还是引用的堆的字符串对象,若串池没有,则把该对象放入串池,这样自己的引用就变成了串池的对象。但是无论失败,返回依然是串池中的对象。
1.6之前,大部分一样,只是无论串池中有没有,自己本身不会有任何改变。
-
字符串常量池,串池(StringTable),一个表
javap -v test。javap 反解析class二进制字节码文件,得出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
D:\java\test\out\production\test>javap -verbose mainTest
Classfile /D:/java/test/out/production/test/mainTest.class
Last modified 2019年4月22日; size 507 bytes
MD5 checksum 08699c6d713bc8967a8daea381ce23c1
Compiled from "mainTest.java"
public class mainTest
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #4 // mainTest
super_class: #5 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #5.#25 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#26 // mainTest.age2:I
#3 = String #27 // sss
#4 = Class #28 // mainTest
#5 = Class #29 // java/lang/Object
#6 = Utf8 age2
#7 = Utf8 I
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 LmainTest;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 name
#20 = Utf8 Ljava/lang/String;
#21 = Utf8 nick
#22 = Utf8 age
#23 = Utf8 SourceFile
#24 = Utf8 mainTest.java
#25 = NameAndType #8:#9 // "<init>":()V
#26 = NameAndType #6:#7 // age2:I
#27 = Utf8 sss
#28 = Utf8 mainTest
#29 = Utf8 java/lang/Object
{
public mainTest();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 22
7: putfield #2 // Field age2:I
10: return
LineNumberTable:
line 1: 0
line 2: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this LmainTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: ldc #3 // String sss
2: astore_1
3: ldc #3 // String sss
5: astore_2
6: bipush 25
8: istore_3
9: return
LineNumberTable:
line 4: 0
line 5: 3
line 6: 6
line 7: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 name Ljava/lang/String;
6 4 2 nick Ljava/lang/String;
9 1 3 age I
}
SourceFile: "mainTest.java"
字节码,符号引用下面的常量池
常量池
局部变量表
JIT和解释器
JAVA代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。简单的工作原理为解释器读取装载入内存的字节码,按照顺序读取字节码指令。读取一个指令后,将该指令“翻译”成固定的操作,并根据这些操作进行分支、循环、跳转等流程。
https://www.cnblogs.com/manayi/p/9290490.html
四、GC垃圾回收机制
1、定义
-
javaGC是清理jvm堆中的无用对象,释放内存,防止内存溢出。
-
如何判断对象是否可以被回收
1.引用计数法:快,缺点,相互引用。 2.可达性分析:从GCRoot出发,没有被直接引用和间接引用的对象就可以被回收。
-
GCRoot有哪些
a. java虚拟机栈(栈帧中的本地变量表)中的引用的对象。 b.方法区中的类静态属性引用的对象。 c.方法区中的常量引用的对象。 d.本地方法栈中JNI本地方法的引用对象
-
四种引用方式
1.强引用,默认,即new对象 = 赋给变量。 不会被回收,不行就跑内存溢出。 2.软引用,中间层SoftReference 一般GC不会被回收,只有内存不足时才会被回收。 SoftReference<String> sr = new SoftReference<String>(new String("hello")); 3.弱引用,中间层WeakReference 只要GC就会被回收,无论内存是否不足。 WeakReference<String> sr = new WeakReference<String>(new String("hello")); 4.虚引用,作为一个标记使用。没有真实的引用。主要用于直接内存的释放。 引用队列,配合软引用,弱引用,虚引用使用,当引用的对象将要被JVM回收时,会将其加入到引用队列中
直接内存,nio操作时候用的,在系统内存中,不在jvm管理的内存中,用于大文件和频繁的nio。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据,大家都可以直接访问着一块缓存区。
NIO(New input/output)是JDK1.4中新加入的类,引入了一种基于通道(channel)和缓冲区(buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过堆上的DirectByteBuffer对象对这块内存进行引用和操作。
jdk1.8的直接内存和元空间是使用jvm内存体系外的系统内存的2个部分。
2、算法
-
标记-清除
标记清除算法分为“标记”和“清除”两个阶段:首先标记出需要回收的对象,标记完成之后统一清除对象。 优点:只是记录到空白空间列表中,所以效率高 确定:标记清除之后会产生大量不连续的内存碎片。
-
标记-整理
标记操作和“标记-清除”算法一致,后续操作不只是直接清理对象,而是在清理无用对象完成后让所有存活的对象都向一端移动,并更新引用其对象的指针。 优点:不会产生内存碎片。 缺点:在标记-清除的基础上还需进行对象的移动,成本相对较高。慢。
-
复制
它将可用内存容量划分为大小相等的两块,每次只使用其中的一块。当这一块用完之后,就将还存活的对象复制到另外一块上面,然后在把已使用过的内存空间一次理掉 优点:没有内存碎屏 缺点:内存缩小为原来的一半
4. 分代算法
根据对象的存活周期的不同将内存划分为几块。一般把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-整理”算法进行回收。
无论哪个垃圾回收期的实现的所有GC都会stop the world,暂停其他用户的线程。
Minor GC触发条件:当Eden区满时,触发Minor GC。
老年代不足时,先minorGC,任然不足,再MajorGC(FUll GC)。
为什么会stop,保证此时垃圾都能被回收,若不暂停,则本来回收的同时还有一个栈帧执行完了,那么它引用的对象就变成了垃圾,而gc已经本来就是清理垃圾的,现在又多了,这样就会导致在垃圾回收的过程中还会不断的产生新的垃圾,所以直接暂停,此刻全部清理。
被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样, 所以我们需要减少STW的发生。
STW事件和采用哪款GC无关,所有的GC都有这个事件。
哪怕是G1也不能完全避免Stop一the一world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
G1,可以设置低延迟时间设置,不一定等到Eden区满了才GC
3、垃圾回收器
-
串行
单线程,堆内存小,适合个人电脑
-
吞吐量优先,gc单位时间内GC次数据少,STW多点
多线程,堆内存大,多核CPU 使单位时间内STW总时间最短,1分钟,2个0.2秒
-
低延迟优先,gc单位时间每次GC次数多,STW少点
多线程,堆内存大,多核CPU 使每一次的STW暂停时间最短 1分钟,5个0.1秒
4、优化
打印出gc日志分析,使用GCeasy工具分析gc日志
2个目的,低延迟优先(STOP THE WORLD 时间少点)和吞吐量优先(单位时间,gc自己用的时间少点)。
有不同的垃圾回收器比如G1和parallelGC。
调优,就是上面的2个目的,
-Xmn 所以先从新生代开始,新生代大一点,比较好,因为很多对象朝生夕死,但是不是越大越好,会占用老年代的大小。而由于老年代内存不足导致的FULLGC耗费时间更久。默认是1:2,即33%。最好是25-50%之间。
接下来就是存活区的对象尽快进入老年代,就需要设置年龄阈值门槛少一点,默认是15。
接下来是老年代,若没有fullGC,最好,发生了也要从新生代开始,若非要处理老年代,则将老年代内存增加。
还有元空间也大一点。
根据工具分析各个部分的情况
内存泄漏:(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。比如static变量内存占用巨大,但是没人使用
内存溢出:(out of memory)通俗理解就是内存不够,通常在运行大型软件或游戏时,软件或游戏所需要的内存远远超出了你主机内安装的内存所承受大小,就叫内存溢出。
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 代码中存在死循环或循环产生过多重复的对象实体;
- 使用的第三方软件中的BUG;
- 启动参数内存值设定的过小
一次两次泄漏不要紧,若长期多次memory leak会最终会导致out of memory!
解决办法
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。递归或者数据量太大。
第四步,使用内存查看工具动态查看内存使用情况
【完,喜欢就点个赞呗】
正在去BAT的路上修行