1.Java代码编译过程
- 准备过程:初始化插入式注解处理器
- 解析与填充符号表过程
- 词法、语法分析,将字符流转为标记集合,构造抽象语法树
- 填充符号表,产生符号地址和符号信息
- 插入式注解处理器的注解处理
- 分析与字节码生成过程
- 标注检查,静态信息检查
- 数据流与控制流分析,动态检查
- 解语法糖
- 字节码生成
2.JVM组成和运行
2.1.组成
- 类加载器:加载字节码,即class文件,只负责加载不管是否可以运行
- 运行时数据区:存放数据的,Java内存方面的问题都集中在这里
- 执行引擎:将class文件中的指令解释给操作系统,将JVM指令集翻译为操作系统指令集
- 本地库接口:负责调用本地接口,调用不同语言的接口给Java用,如Java驱动,地图制作引擎等。这种方式主键被Socket通信、WebService等方式取代。
2.2.运行
- JVM的装入环境和配置,使用java.exe找JRE
- 自己目录下的JRE
- 父目录下的JRE
- 注册中注册的JRE
- 装载JVM,通过LoadJavaJVM装入JVM文件,LoadLibrary装载JVM动态连接库等。
- 初始化JVM,获得本地调用接口。
- 运行Java程序,jar包的话先从Manifest中获得Main-Class的值,然后main函数调用LoadClass装载该主类。Class的话直接LoadClass装载类。
3.类加载机制
3.1.类的生命周期与类加载阶段
类是在运行期间第一次使用时动态加载的,而不是一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。
1.加载
加载过程完成以下三件事:
- 通过类的完全限定名称获取定义该类的二进制字节流。
- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构。
- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口。
其中二进制字节流可以从以下方式中获取:
- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
- 从网络中获取,最典型的应用是 Applet。
- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
2.验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。包括文件格式、元数据验证、字节码验证和符号引用验证。
3.准备
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值(设置为0等),如果是final的话就直接设置为真实值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
4.解析
将常量池的符号引用替换为直接引用的过程。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
5.初始化
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器 <clinit>() 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
<clinit>() 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。
接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
3.2.对象实例化过程
- <init>()方法可以重载多个,类有几个构造器就有几个<init>方法
- <init>()方法中的代码执行顺序为:父类变量初始化->父类代码块->父类构造器->子类变量初始化->子类代码块->子类构造器
- 静态变量->静态代码块->普通变量->普通代码块->构造器
3.3.类初始化时机
1.主动引用
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
2.被动引用
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
- 子类引用父类静态字段,子类不会初始化。
- 通过数组引用类,不会引起初始化。
- 常量引用时 ConstClass.HELLOWORLD
3.3.类与类加载器
两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
3.4.类加载器分类
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分;
- 所有其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
- 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
3.5.双亲委派模型
类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L15GQkYW-1670858358621)(/home/zekdot/桌面/note/java/figure/parent.png)]
1. 工作过程
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
2.好处
- 避免类重复加载:父加载器加载某个类后子加载器不用重复加载
- 避免篡改核心类:如自定义的String类不会加载,只会加载java自带String类
3. 实现
以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。
4.破坏
并不是具有强制约束的模型,而是推荐的类加载器实现方式。Java模块化出现为止,出现过3次较大规模“被破坏”情况。
- 双亲委派模式出现之前的实现。
- 模型自身缺陷导致的,如JNDI服务,逆向使用类加载器。
- 追求程序动态性,热代码替换等,OSGi。
5.SPI和打破双亲委派机制
打破:
- 集成classLoader自定义类加载器,重写loadClass方法
- 使用线程上下文加载器(SPI实现)
SPI
- Java提供的一套用来被第三方实现或者扩展的接口
- 遵守约定,类名写到/META-INF里,启动后扫描jar包里符合约定的类名,然后forName加载
4.运行时数据区域
4.1.线程内部
- 程序计数器:记录正在执行的虚拟机字节码指令的地址(执行本地方法则为空)。
- Java虚拟机栈:每个Java方法执行同时会创建栈帧用于存储局部变量表、操作数栈、常量池引用等信息。可以通过-Xss指定Java虚拟机栈内存大小,1.4默认256K,1.5以上默认1M。可能存在StackOverflowError异常和OutOfMemoryError异常。
- 本地方法栈:本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
4.2.堆
所有对象都在这里分配内存,是垃圾收集的主要区域(“GC 堆”)。
- 新生代 1
- Eden 区8
- Survivor 区1,为了解决太多对象放入老年区的问题,解决内存碎片
- 老年代 2
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
4.3.方法区
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
方法区是一个 JVM 规范,永久代与元空间都是其一种实现方式。在 JDK 1.8 之后,原来永久代的数据被分到了堆和元空间中。元空间存储类的元信息,静态变量和常量池等放入堆中。
运行时常量池
运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
4.4.直接内存
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆外内存来回拷贝数据。
5.垃圾收集
针对堆和方法区进行的,线程生命周期内的在其结束后就会释放,所以不需要回收。
5.1.判断一个对象是否可以回收
1.引用计数算法
对象增加一个引用计数器加一,失效时减一,但是可能存在循环引用。
2.可达性算法
以GC Roots为起始点进行搜索,可达存活,不可达回收。
RC Root包含以下内容。
- 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中 JNI 中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
3.方法区回收
方法区存放永久代,回收性价比不高,主要是常量池回收和类卸载(反射和动态代理中)。
- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
4.finalize
类似析构函数,用于关闭外部资源,但是不如try-finally方式,代价高,不确定性大。可以实现方法的自救。
5.2.引用类型
判定对象是否可以回收。
1.强引用
被强引用关联的对象不会被回收,使用new方式创建的就是强引用。
2.软引用
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
只有内存不够的时候才会回收,使用SoftReference来创建。
3.弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。使用 WeakReference 类来创建弱引用。
4.虚引用
又称为幽灵引用或者幻影引用,一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。使用 PhantomReference 来创建虚引用。
5.3.垃圾收集算法
1.标记-清除
在分配时,程序会搜索空闲链表寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。
- 标记和清除过程效率都不高;
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
2.标记-整理
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。老年代用法。
优点:
- 不会产生内存碎片
缺点:
- 需要移动大量对象,处理效率比较低。
3.复制
内存划分相等两块,每次用其中一块,用完之后将存活对象复制到另一块,然后清理。
主要不足是只使用了内存的一半。现在的商业虚拟机采用这种算法回收新生代,但是不是相等两块,而是一块较大Eden空间和两块较小的Survivor空间。每次使用Eden和其中一个Survivor,回收时将Eden和Survivor存活对象都放到另一块Survivor中。默认比例为8:1,内存利用率达到90%,如果Survivor不够用,需要借用老年代空间。
4.分代收集
现代商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代。
- 新生代使用:复制算法
- 老年代使用:标记 - 清除 或者 标记 - 整理 算法
5.4.垃圾收集器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NNmJKzGV-1670858358622)(/home/zekdot/桌面/note/java/figure/GC1.jpg)]
连线可以配合使用。
- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行
1.Serial收集器
-
串行的方式执行。是单线程的收集器,只会使用一个线程进行垃圾收集工作。
-
是 Client 场景下的默认新生代收集器
2.ParNew收集器
- Serial 收集器的多线程版本。
- Server场景下的默认新生代收集器。
3.Parallel Scavenge收集器
- 多线程收集器。
- 吞吐量优先。这里的吞吐量指 CPU 用于运行用户程序的时间占总时间的比值。
4.Serial Old 收集器
是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
5.Parallel Old收集器
Parallel Scavenge收集器的老年代版本。在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
6.CMS收集器
Concurrent Mark Sweep
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除:不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
7.G1收集器
一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n9ig3yOs-1670858358623)(/home/zekdot/桌面/note/java/figure/G1.png)]
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qDdxYBEq-1670858358623)(/home/zekdot/桌面/note/java/figure/G12.png)]
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
- 初始标记:暂停用户线程,标记GCRoot直接关联的对象
- 并发标记:从GCRoot进行可达性分析,并标记存活对象,与用户线程并发。
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
具备如下特点:
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
6.内存分配与回收策略
6.1.Minor GC触发条件
Eden区空间耗尽时,存活下来的对象直接送到Survivor区。
6.2.Minor GC 和 Full GC
- Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
- Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
6.3.内存分配策略
1.对象优先在Eden分配
大多数情况下在Eden上分配,空间不够时,发起Minor GC。
2.大对象直接分配在老年代
需要连续存储空间,比如大字符串或者数组。-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。
3.长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,在Survior区中每熬过一次Minor GC,年龄就增加 1 岁,增加到一定年龄(默认15)则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
4.动态对象年龄绑定
如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
5.空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
6.4.Full GC 的触发条件
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件
1.调用System.gc
只是建议虚拟机执行,但是不一定真的执行。
2.老年代空间不足
大对象直接进入老年代、长期存活的对象进入老年代等。
应当尽量不要创建过大的对象以及数组
通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代
通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间
3.空间分配担保失败
担保失败时进行Full GC。
4.JDK 1.7之前永久代空间不足
HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
5.Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
6.5.内存泄露与内存溢出
1.内存泄露
程序运行过程中分配内存给临时变量,用完之后没有被GC回收,始终占用内存,不能使用也不能分配给其他程序。
分类:
- 常发性:每次执行都要导致一块内存泄露
- 偶发性:某些特定环境或者操作才会发生
- 一次性:发生内存泄露的代码只会执行一次,且一块内存发生泄露
- 隐式:没有及时释放导致内存不可用
建议
- 尽早释放无用对象引用
- 避免循环中创建对象
- 使用字符串处理避免使用String,使用StringBuffer
- 少用静态变量
2.内存溢出
申请内存大于系统可以提供的内存。
原因
- 内存中加载的数据量过大
- 集合类中引用对象,使用完后未清空
- 代码中存在死循环或循环产生过多重复对象实体
- 第三方软件的BUG
- 启动参数内存值设定过小
方案
- 修改JVM启动参数,直接增加内存
- 检查错误日志,查看是否有其他错误或者异常
- 对代码走查分析,找出可能发生内存溢出的位置
- 使用内存查看工具动态查看内存使用情况
除了程序计数器外,虚拟机内存的其他几个运行区域都有可能OOM异常。
7.JVM调优
7.1.需要关注的指标
- 内存使用情况
- 延迟:垃圾回收造成的延迟
- 吞吐量:程序运行时间占包括运行时间和垃圾回收时间总时间之比
7.2.调优正常运行的系统
- 通过jmap查看各个区域的使用情况
- 通过jstack查看线程的运行情况,是否有阻塞,是否出现死锁
- 通过jstat查看垃圾回收情况,比如fullgc次数过多,有可能是年轻代太小,大对象直接放入老年代,可以增加年轻代大小
- 通过监控工具(基于命令等)
- 发现异常后进行定位优化,比如找到占用CPU 多的线程,对方法进行优化
7.3.对于已经发生OOM的系统
- 通过JVM配置,让系统发生OOM后生成dump文件
- 使用工具分析dump文件
- 根据dump文件找到异常的实例对象和异常线程,比如某个实例占用太多内存
- 进行调优