一、JVM体系结构
灰色为线程私有,橙色为所有线程共享
二、各个模块介绍
1. 类装载器ClassLoader
1.1 概念
负责加载Class文件,class文件在文件开头有特定的文件标识【cafe babe】, 将class文件加载到内存中,并将这些内容转换成方法区中运行的运行时数据结构,并且ClassLoader只负责class文件的加载,以至于它是否可以运行,则由Execution Engine决定。
1.2 ClassLoader的种类
- 虚拟机自带
- 启动类加载器[Bootstrap]:由C++编写,程序中自带的类,存储在 JAVA_HOME/jir/lib/rt.jar【rt–RunTime】中
- 扩展类加载器[Extension]:由java编写,加载java发展过程中发展出来一些扩展包,存储在 JAVA_HOME/jir/lib/ext/*.jar 中
- 应用程序类加载器[Application]: 如果用户没有自定义类加载器,默认使用该加载器,可以加载程序中所有自定义的类
- 用户自定义加载器
- 如果程序有额外的需求,需要自定义类加载器,可以通过继承抽象类java.lang.ClassLoader实现
1.3 双亲委派机制
- 概念:当一个类收到类加载请求的时候,他不会先尝试自己去自己加载这个类,而是先把这个请求委派给父类执行,每一层都是如此,因此所有的加载请求都是先送到启动类加载器,启动类在他的ClassPath下没有找到所有需加载的Class时,子类才会尝试加载。
- ClassLoader 加载结构图【子类到父类】
1.4 类加载器中的沙箱机制
- 沙箱概念:
- 默认情况下,一个应用程序可以访问机器上的所有资源,但是这样是不安全的【通过双亲委派机制解决该问题】,所以我们需要为程序提供一个受限的运行环境。
- 举例:创建一个java.lang.String类,并运行,会运行出错
package java.lang; public class String { public static void main(String[] args) { System.out.println("哈哈"); } } // 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: // public static void main(String[] args) // 否则 JavaFX 应用程序类必须扩展javafx.application.Application // 结论: 先会从启动类加载器中加载此类,而不会执行上面的类,避免了外部类对内部资源的污染
1.5 Java 代码获取类加载器
public class MyObject {
public static void main(String[] args) {
Object object = new Object();
// 获取是哪一个类装载器加载进来的
System.out.println(object.getClass().getClassLoader());
// null 由于bootstrap ClassLoad 是由C++编写,并不继承java.lang.ClassLoader,所以返回的时候为null
MyObject myObject = new MyObject();
System.out.println(myObject.getClass().getClassLoader().getParent().getParent());
//null
System.out.println(myObject.getClass().getClassLoader().getParent());
//sun.misc.Launcher$ExtClassLoader@49476842 Launcher类加载
System.out.println(myObject.getClass().getClassLoader());
//sun.misc.Launcher$AppClassLoader@18b4aac2 Launcher类加载
}
}
2. 执行引擎
执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者
3. PC寄存器
概念:每个线程都有一个程序技术器,是线程私有的,它就是一个指针,用来指向方法区中的字节码,由执行引擎读取下一条命令,它是一个非常小的内存,几乎可以忽略不记。【简而言之就是用来存储下一条指令的地址】
4. Native方法
- 概念: native方法也就是一个非java代码编写的接口。由非java代码【c/c++等】实现
- 例子: 【java线程调用start()就会立即运行吗?不是。先需要调用native方法start0(),获取操作系统的资源调度,然后再由初始状态到运行状态】
private native void start0();// Thread 源码 public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } }
5. 方法区
- 概念:
供各线程共享的运行时内存区域。它存储了每一个类得结构信息【也就是类的模板】,例如运行时常量池(RuTime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。注意方法区只是一套规范,在不同的虚拟机实现不一样,最典型的就是永久代(PermGen space)和元空间(Metaspace)。 - 规范的理解,
List list = new ArrayList, List为模板,有不同的实现
Son son = new Son(); Son为模板,son对应的是自己的实现
方法区 f1 = new 永久代(); 永久代的实现数据是存储在jvm内存,容易经常oom
方法区 f2 = new 元空间(); 元空间的实现是存储在本地内存,可以通过设置大小,一般不会oom
5. 栈
- 概念:
栈也叫栈内存,主管java程序的运行,是在线程创建的时候创建,它的生命期是跟随线程的,线程结束,栈内存也就结束,所以对于栈来说是不存在垃圾回收的问题的。生命周期和线程一致,是线程私有的。它包含:8中基本的类型变量+对象的引用变量+实例方法都是在函数的栈内存中分配的 - 栈帧:对应的是java的方法
- 栈存储的内容:
- 输入参数和输出参数以及方法内部的变量
- 记录出栈、入栈的操作
- 栈帧数据:包括文件、方法等等
- 栈异常
public class HeadTest { private static void m(){ m(); } public static void main(String[] args) { m(); } // 抛出异常 【错误】 // Exception in thread "main" java.lang.StackOverflowError[错误] }
6. 堆
6.1 堆结构图
注: java7之前永久存储区(Permanment Space)在堆内存中,但是容易造成堆溢出,java8已经将永久区改名为元空间,并移除堆内存中,数据直接存储在本地内存中。
6.2 堆内存设置参数
- 参数说明:
-Xms 设置初始堆的大小。默认为物理内存的: 1/64 -Xmx 最大分配内存,最大分配内存,默认为物理内存的 1/4 -XX:+PrintGCDetails 输出详细的GC处理日志
- 在idea中设置参数,初始内存为10m,最大内存为10m,输出DC日志
6.3 获取当前系统的堆内存大小
public static void main(String[] args) {
long maxMemory = Runtime.getRuntime().maxMemory(); //最大使用的堆内存
long totalMemory = Runtime.getRuntime().totalMemory();//使用的堆内存
System.out.println(maxMemory/(double)1024/1024+"M"); //3623M--本机16G内存,
System.out.println(totalMemory/(double)1024/1024+"M"); //245.5M--使用的堆内存
}
6.4 垃圾回收机制
- 当Eden区满的时候会触发第一次GC,GC后把还活着的对象拷贝到SurvivorFrom区,此后,当Eden区满时,再次触发GC的时候会扫描Eden区和SurvivorFrom区,对这两块区域进行垃圾回收,经过两次回收都还存活的对象将拷贝的SurvicorTo区,拷贝后两块区域不留任何对象。
- 交换From和To区:
在第二次拷贝完对象后,原来的SurviorFrom区将变成SurivivorTo区,原来的SurvivorTo区将变成原来的SurvicorFrom区,即每次拷贝完后,新的SurivivorTo区是没有任何对象的,新的SurviorFrom区保留者上一次GC存活着的对象; - 紧接着,当进Eden区再次触发GC时,GC依旧是每次都扫描Eden和SurvivorFrom区,并重复 步骤2 的操作
- 对象从第一次到eden区到fron区、from区到to区,每次拷贝后年龄都会加1,知道年龄达到配置默认15岁【-XX:MaxTenuringThreshold=X】,就可以进入老年区。
6.5 堆溢出代码
- 设置类T2的VM参数:-Xms10m -Xmx10m -XX:+PrintGCDetails
- 代码
public class T2 { public static void main(String[] args) { String str = "sdfsdfsdfsdfsdfsdfsdf"; while (true){ str += str + new Random().nextInt(88888888)+new Random().nextInt(9999999); } Exception in thread "main" java.lang.OutOfMemoryError: Java heap space } }
- 日志输出
[GC (Allocation Failure) [PSYoungGen: 2041K->496K(2560K)] 2041K->833K(9728K), 0.0011555 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2173K->496K(2560K)] 2511K->1575K(9728K), 0.0008043 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2297K->512K(2560K)] 4511K->3064K(9728K), 0.0007522 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 1715K->0K(2560K)] [ParOldGen: 7088K->1868K(7168K)] 8803K->1868K(9728K), [Metaspace: 3226K->3226K(1056768K)], 0.0041516 secs] [Times: user=0.13 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 1173K->0K(2560K)] 5310K->4136K(9728K), 0.0002383 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 4136K->4136K(9728K), 0.0004707 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 4136K->4094K(7168K)] 4136K->4094K(9728K), [Metaspace: 3226K->3226K(1056768K)], 0.0056231 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 4094K->4094K(8704K), 0.0004452 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 4094K->4074K(7168K)] 4094K->4074K(8704K), [Metaspace: 3226K->3226K(1056768K)], 0.0093429 secs] [Times: user=0.11 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 1536K, used 104K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
eden space 1024K, 10% used [0x00000000ffd00000,0x00000000ffd1a3f8,0x00000000ffe00000)
from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 7168K, used 4074K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
object space 7168K, 56% used [0x00000000ff600000,0x00000000ff9fa988,0x00000000ffd00000)
Metaspace used 3272K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
at java.lang.StringBuilder.append(StringBuilder.java:208)
at com.jvm.类装载器.T2.main(T2.java:9)
6.6 GC日志信息描述
- 备注:GC后的括号里跟的是GC产生的原因,GC原因可能有:(Allocation Failure、System.gc()等
- GC日志
对应注解为:[GC (Allocation Failure) [PSYoungGen: 2041K->496K(2560K)] 2041K->833K(9728K), 0.0011555 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[ Moitor GC (年轻代中没有足够的空间存储新的数据了) [GC类型: GC之前新生代内存占用大小 -> GC后新生代占用内存大小 (GC后新生代总共的大小) ]] GC前JVM堆内存大小->GC后JVM堆内存使用(JVM堆总大小) [时间信息: 用户使用时间为=0.00 系统使用时间为=0.00, 总共使用的时间为=0.00 secs]
- Full GC日志
[Full GC (Ergonomics) [PSYoungGen: 1715K->0K(2560K)] [ParOldGen: 7088K->1868K(7168K)] 8803K->1868K(9728K), [Metaspace: 3226K->3226K(1056768K)], 0.0041516 secs] [Times: user=0.13 sys=0.00, real=0.00 secs]
[ Full GC (jvm进行自适应调整产生的Full GC) [GC类型: GC之前新生代内存占用大小 -> GC后新生代占用内存大小 (GC后新生代总共的大小) ]] [GC类型ParOldGen: GC前老年区大小->GC后老年区大小(老年区总大小)] GC前JVM堆内存大小->GC后JVM堆内存使用(JVM堆总大小), [Metaspace: GC前内存占用->GC后内存占用(Perm区总大小)], GC耗时] [时间信息: 用户使用时间为=0.00 系统使用时间为=0.00, 总共使用的时间为=0.00 secs]
6.7 可达性算法
对象到GC Root对象之间是否存在直接或者间接的引用关系(即:以GC Root的对象为起点,由这些节点开始向下进行依次搜索,向下搜索的过程中如果能搜索相关对象,则说明对象可达,反之不可达)
6.8 GC Roots 是那些东西
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈内JNI引用的对象(native方法)
- 方法区中常量引用的对象
- 方法区中类静态属性引用的对象
- 所有被同步锁synchronized
6.9 GC 回收算法
- 引用计数法:每个对象都有一个引用计数属性,新增一个引用就加1,释放引用就减1,计数为0就进行回收。【基本不用该算法】
缺点:- 循环依赖问题难以解决,例如A中引用B,B中引用A,数量都为1,难以判断存活
- 每个对象都有一个计数属性,堆资源的消耗较大
- 标记-清除算法: 遍历所有的GC Roots,然后将所有的GC Roots可以达对象标记成存活,然后将所有没有标记的对象清除
缺点:- 需要扫描两次
- 会造成内存不连续,产生内存碎片
- 标记-复制算法: 将内存空间分为两个区间,一个区间为活动区间,另外一个空间区间。GC时会将活动区间存活的对象,的应用地址一一拷贝到空闲区间。
优点:存储空间连续
缺点:
- 如果存活的变量很多,需要将所有的对象都拷贝过去,非常的消耗时间
- 浪费了一半的空间 - 标记-整理(标记-压缩算法):比标记-清除算法多一步整理,将存活的对象都向一端进行移动。
缺点: 效率不高,耗时长
7. 堆、栈、方法区的联系
-
创建一个对象
-
关系图
三. 相关配置参数总结
1 常用命令汇总
1.1 基础启动参数
-Xms512M
- Jvm启动的最小堆内存为512M
-Xms1024M
- Jvm的最大堆内存为1024M,将-Xms和-Xmx的内存设置成一样可以避免堆内存的自动扩展
-Xmn20M
- 标识年轻代的大小为20M
-Xss1M
- JDK5.0后栈大小默认为1024k
-XX:SurvivorRatio=8
- eden:survivorFrom:survivorTo=8:1:1
-XX:MaxTenuringThreshold=15
- 表示对象经过多少次GC后进入老年代
-XX:+HeapDumpOnOutOfMemoryError
- 系统发生oom的时候,自动dump出当前的堆内存快照
-XX:+PrintGCDetails
- 表示在控制台上打印出GC具体细节
1.2 性能监控相关参数
jps
jps 输出当前运行中的java程序的PID
jps -l 输出类的全路径名字
jps -m 输出jvm启动的时候通过main方法传递过来的参数
jps -v 输出jvm的参数
jinfo
jinfo #jinfo的命令帮助面板
jinfo #1 查看运行程序PID为1的参数列表
jinfo -flags 1 #查看运行程序PID为1的所有启动参数列表
jinfo -sysprops 1 #查询系统环境变量列表
java -XX:+PrintFlagsFinal -version | grep manageable #查看那些可以使用jinfo命令来进行管理
jinfo -flag +PrintGCDetails 11666 #设置打印GC日志详情
jstat
jstat -class 1 #查询PID为1的程序的类加载情况
jstat -gcutil 1 #打印垃圾回收概况
jstat -gcutil 11666 1000 3 #每隔一秒打印一次垃圾回收概况,累计打印三次
jstat -gc 1 #打印垃圾回收概况
jstat -gc 1 1000 3 #每隔一秒打印一次垃圾回收概况,累计打印三次
命令2,3中对应字段意思:
- S0 survivor0使用百分比
- S1 survivor1使用百分比
- E Eden区使用百分比
- O 老年代使用百分比
- M 元数据区使用百分比
- CCS 压缩使用百分比
- YGC 年轻代垃圾回收次数
- YGCT 年轻代垃圾回收消耗时间
- FGC 老年代垃圾回收次数
- FGCT 老年代垃圾回收消耗时间
- GCT 垃圾回收消耗总时间
命令3,4中对应字段意思:
- S0C survivor0大小
- S1C survivor1大小
- S0U survivor0已使用大小
- S1U survivor1已使用大小
- EC Eden区大小
- EU Eden区已使用大小
- OC 老年代大小
- OU 老年代已使用大小
- MC 方法区大小
- MU 方法区已使用大小
- CCSC 压缩类空间大小
- CCSU 压缩类空间已使用大小
- YGC 年轻代垃圾回收次数
- YGCT 年轻代垃圾回收消耗时间
- FGC 老年代垃圾回收次数
- FGCT 老年代垃圾回收消耗时间
- GCT 垃圾回收消耗总时间
jstack
jstack #帮助面板,该命令主要用来打印线程快照的
jstack 1 #查询程序PID为1的锁快照
jstack命令详情:http://www.hollischuang.com/archives/110
jmap
jmap -dump:format=b,file=d:\1.hprof 1 #导出程序的dump文件
+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\m.hprof #程序启动命令中一般都会添加该参数,在程序报错后会自动生成dump文件
四 四种引用
强引用
/**
* 强引用:
* 只要引用还存在,对象就不会被回收,哪怕内存不足时,也会直接抛出OutOfMemoryError,不回去回收
*/
public static void main(String[] args) {
// 默认强引用 即使oom 也不会回收
Object obj1 = new Object();
System.out.println(obj1);
Object obj2 = obj1;
obj1 = null;
System.gc();
// 虽然obj1执行为空了,但是obj2依旧指向new Object()
System.out.println(obj2);
}
软引用
/**
* 软引用:
* 在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,
* 如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常
* VM参数:-Xms5m -Xmx5m -XX:+PrintGCDetails
*/
public static void main(String[] args) {
softRef_Memory_enough();
System.out.println("=================");
softRef_Memory_NotEnough();
}
public static void softRef_Memory_enough() {
Object obj = new Object();
SoftReference<Object> softReference = new SoftReference<>(obj);
System.out.println(obj);
System.out.println(softReference.get());
obj = null;
System.gc();
System.out.println(obj);
System.out.println(softReference.get());
}
public static void softRef_Memory_NotEnough() {
Object obj = new Object();
SoftReference<Object> softReference = new SoftReference<>(obj);
System.out.println(obj);
System.out.println(softReference.get());
obj = null;
System.gc();
try {
byte[] bytes = new byte[30 * 1024 * 1024];
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
//bytes已经被回收了,所以下面会输出两个null值
System.out.println("回收后:");
System.out.println(obj);
System.out.println(softReference.get());
}
}
软引用
/**
* 软引用:无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收
* WeakHashMap
*/
public static void main(String[] args) {
Object obj = new Object();
WeakReference<Object> weakReference = new WeakReference<>(obj);
Object obj2 = weakReference;
System.out.println(obj);
System.out.println(weakReference.get());
System.out.println(obj2);
// obj = null;
System.gc();
System.out.println("======================================");
System.out.println(obj);
System.out.println(weakReference.get());
System.out.println(((WeakReference)obj2).get());
}
虚引用
/**
* 虚引用: 如果一个对象仅持有虚引用,那么他就和没有任何引用一样,在任何时候都有可能被垃圾回收器回收
* 使用时候 需要和ReferenceQueue 联合使用
* jvm 允许我们 将对象回收后 做一些自己想做的事情
*
* @param args
*/
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// 只要参数了gc 就会添加到引用队列中
PhantomReference<Object> weakReference = new PhantomReference<>(obj, referenceQueue);
System.out.println(obj);
System.out.println(weakReference.get());
System.out.println(referenceQueue.poll());
System.out.println("=====================================");
obj = null;
System.gc();
Thread.sleep(500);
System.out.println(obj);
System.out.println(weakReference.get());
System.out.println(referenceQueue.poll());
}
五 常见OOM错误汇总
java.lang.OutOfMemoryError: Java heap space
public class JavaHeapSpaceDemo {
/**
* Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
* @param args
*/
public static void main(String[] args) {
String str = "hhahahhah";
while (true) {
str += str + new Random().nextInt(11111111) + new Random().nextInt(222222222);
str.intern();
}
}
}
java.lang.StackOverflowError
public class StackOverflowErrorDemo {
public static void main(String[] args) {
// Exception in thread "main" java.lang.StackOverflowError
// jvm 的错误
stackOverflowError();
}
private static void stackOverflowError() {
stackOverflowError();
}
}
java.lang.OutOfMemoryError: GC overhead limit exceeded
public class GcOverheadLimitExceededDemo {
/**
* 98%的大量资源都用去做GC,并且回收了不到 2%的堆内存
*
* VM参数: -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
* JDK11: -Xms10m -Xmx10m -Xlog:gc* -XX:MaxDirectMemorySize=5m
* Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
*/
public static void main(String[] args) {
int i = 0;
List<String> list = new ArrayList<>();
try {
while (true) {
list.add(String.valueOf(++i).intern());
}
} catch (Exception e) {
System.out.println("******************" + i);
e.printStackTrace();
throw e;
}
}
}
java.lang.OutOfMemoryError: Direct buffer memory
public class DirectBufferMemoryDemo {
/**
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
* 堆外内存(本地内存)使用完了发生如下错误:
* Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
* 堆外内存的作用主要是减少 java堆和Native堆的来回内存交互
*
*/
public static void main(String[] args) {
System.out.println("maxDirectMemorym默认大小" + VM.maxDirectMemory() / 1024 / 1024 + "MB");
//分配最大本地内存是1000000MB
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100 * 1024 * 1024);
}
}
java.lang.OutOfMemoryError: unable to create new native thread
public class UnAbleCreateNewThreadDemo {
/**
* 不能够创建更多的线程了
*
* 导致原因:
* 1.你的应用创建了太多线程了,一个应用进程创建了多个线程,超过了系统承载的极限
* 2.你的服务器并不允许你的应用程序创建这么多线程,Linux系统默认允许单个进程可以创建的线程数是1024个
* 你的服务器并不允许你超过这个数量,就会报java.lang.OutOfMemoryError: unable to create new native thread
* <p>
* 解决办法
* 1.想办法降低你的应用程序创建线程的数量,分析应用是否真的需要创建这么多的线程,如果不是:改代码将线程降到最低
* 2.对于有的应用,确实需要创建很多的线程,远超过Linux系统默认的1024个线程的限制,可以通过修改Linux服务器的配置,扩大Linux默认限制
*
* linux 查询线程上限:ulimit -u
* 编辑线程的上限: vim /etc/security/limits.d/90-nproc.conf
* linux 的 root用户无上限,其它默认1024
*/
public static void main(String[] args) {
for (int i = 1; ; i++) {
System.out.println("********** 线程数量 = " + i);
new Thread(() -> {
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "" + i).start();
}
}
}
java.lang.OutOfMemoryError: Metaspace
public class MetaspaceOomTest {
static class OOMTest {
}
/**
* -XX:MetaspaceSize=20m -XX:MaxMetaspaceSize=20m
* 动态生成类导致:Caused by: java.lang.OutOfMemoryError: Metaspace
* 永久代(数据存储在本地内存中,java8后被元空间Metaspace取代了,存放了以下的信息)
* 虚拟机加载的类信息
* 常量池
* 静态变量
* 即时编辑后的代码
* @param args
*/
public static void main(String[] args) {
int i = 0; // 每生产成一个对象就+1
try {
while (true) {
i++;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMTest.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, args);
}
});
enhancer.create();
}
} catch (Throwable e) {
System.out.println("****** 多少次后发生了异常: " + i);
e.printStackTrace();
}
}
}
六 垃圾收集器
- serial 串行 它为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程
- Parallel 并行垃圾搜集器,多个垃圾回收线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理等弱交互情形
- CMS(Concurrent Mark Sweep) 并发 用户线程和垃圾收集线程同时执行(不一定是并行,可能交替执行),不需要用户停顿所有线程。互联网公司多用他,适用响应时间有要求的场景
- G1 CMS并发标记清除,存在内存碎片,所以G1垃圾回收器将堆内存分割成不同的区域然后并发的对其进行垃圾回收
public class chacter06_jvm2 {
/**
* 查看JVM的启动参数:java -XX:PrintCommandLineFlags -version
* java8默认垃圾会搜器-XX:+UseParallelGC
*
* DefNew:Default New Generation 新生代默认的串行垃圾回收器
* Tenured: old
* ParNew:新生区用并行垃圾回收器
* PSYoungGen: Parallel Scavenge
* PSOldGen: Parallel Old Generation
* *
* 1. -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC (DefNew + Tenured)
* Serial(Young区用) + Serial Old(Old区用)搜集器的组合
* 新生代、老年代都会使用串行回收搜集器,新生代使用复制算法,老年代使用:标记-整理算法
*
* @Deprecated
* 2. -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC (ParNew + Tenured)
* 可以通过-XX:ParalleelGCThreads 来限制线程的数量,默认开启和CPU数量相同的线程数
* 启动 PArNewGC线程,只影响新生代收集,不影响老年代
* 开启上述垃圾回收线程后会使用 ParNew(Young区用) + Serial Old的收集器组合 新生代使用复制算法,老年代采用标记-整理算法
* 备注:警告日志,这个已经过期了 -> Java HotSpot(TM) 64-Bit Server VM warning: Using the ParNew young collector with the Serial old collector
* is deprecated and will likely be removed in a future release
*
* 3. -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC (PSYoungGen + ParOldGen)
* 新生代和老年代的并行化 可以用: -XX:+UseParallelGC 或 -XX:+UseParallelOldGC (可以互相激活) 使用Parallel Scanvenge 搜集器,使用复制算法
* 多说一句:-XX:ParallelGCThreads=数字N 标识启动多少个GC线程
* cpu > 8 N = 5 / 8
* cpu < 8 n = 实际的个数
*
* 4 和 3 互相激活
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC (PSYoungGen + ParOldGen)
*
* 5
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC (par new generation + consurrent)
* Concurrent Mark Sweep 并发标记清除,并发收集低停顿,并发指的是与用户线程一起执行
* 开启该收集器的JVM参数: -XX:+UseConcMarkSweepGC 开启该参数后会自动将 -XX:+UseParNewGC打开
* 开启该参数后,使用ParNew(Young区用) + CMS(Old区用) + Serial Old收集器, Serial Old作为后备收集器使用
* 过程: 1.初始标记,全部工作线程停止(标记一下GC Roots能【直接关联】的对象,)
* 2.并发标记 和用户一起 (从第一步标记的对象触发,并发的标记可达对象)
* 3.重新标记,部工作线程停止(二次确认的过程,第一次标记为清除,可能后续又被使用了起来)
* 4.并发清除和用户线程一起
* 优点:并发收集延迟低
* 缺点: 1.并发执行,对CPU的压力比较大
* 2.采用标记清除算法会导致大量的碎片(标记清除算法无法整理空间碎片,老年代空间会随着应用时长被逐步耗尽,最后将不得不通过担保机制对内存进 行压缩。
* CMS也提供了参数-XX:CMSFullGCsBeForeCompaction[默认为0,即每次后都进行内存整理])来指定多少次CMS收集之后,进行一次压缩的Full GC
*
* 6.
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseG1GC
* 每一块被划分为1-32M,G1算法会将堆划分为若干个区域(Eden,Survivor,Old,Humongous[超大对象,如果不够可以将多个快连接起来])
*
* G1 收集器在停顿事件上加了预测机制,用户可以指定期望的停顿时间
* 2.G1 整体采用标记-整理算法,局部是通过复制算法,不会产生内存碎片
* 3.宏观上看G1不再区分年轻代和老年代,
*
* 7 (理论知道即可,实际已经被优化掉了,没有了)
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialoldGC
*
*
* @param args
*/
public static void main(String[] args) {
String str = "hhahahhah";
while (true) {
str += str + new Random().nextInt(11111111) + new Random().nextInt(222222222);
str.intern();
}
}
/**
* 如何 选择垃圾回收器
* 单个CPU或者小内存,单机程序 -XX:+UseSerialGC
*
* 多CPU,需要最大吞吐量,如后台计算型应用: -XX:+UseParalleGC 或者 -XX:+UseParalleOldGC
*
* 多CPU,追求低停顿时间,需快速响应如互联网应用
* -XX:+UseConcMarkSweepGC
* -XX:+ParNewGC
*
*/
}
七 排查服务性能相关命令
top
top命令提供了实时的对系统处理器的状态监视,一般用做性能总的查询:
load average详解:https://www.jianshu.com/p/57cd563bbe58
uptime
top命令的简版,即top命令的第一行
mpstat
查询所有cpu的核数信息
- mpstat 安装:yum install sysstat
- mpstat -P ALL 2 #每隔两秒打印一次所有CPU的详情
各个字段的详情:https://blog.csdn.net/breakout_alex/article/details/91490371
vmstat
查看cpu总的空闲率,相对于top命令是查看单个进程的CPU使用率
- vmstat -n 2 3 每隔两秒打印一次,累计打印三次
- r:运行和等待CPU时间片的进程数,原则上1核的CPU的运行队列不要超过2,整个系统的运行队列不能超过总核数的2倍,否则代表系统的压力大
- b:等待资源的进程数,比如正在等待磁盘I/O、网络I/O等
- us:用户进程消耗cPU时间的百分比,us值高,用户进程消耗CPU时间多,如果长期大于50%,需要优化程序
- sy:内核进程消耗的CPU时间的百分比
- us + sy参考值为80%,如果us+sy大于80%,则说明可能存在CPU不足
- id:处于空闲的CPU百分比
- wa:系统等待IO的CPU时间百分比
- st:虚拟机偷取CPU时间的百分比
pidstat
查看每个进程CPU的分量信息
- pidstat #默认显示所有进程的cpu使用率
- pidstat -u 1 -p 1241 #每隔一秒打印1241这个进程的利用率详情
- %user:进程在用户空间占用的百分比
- %system:进程在内核空间占用cpu的百分比
- %guest:进程在虚拟机中占用时间的百分比
- %CPU:进程占用cpu的百分比
- CPU:处理进程的CPU编号
- Command:当前进程对应的命令
free
显示当前系统已使用的系统内存
- free -m #查看内存使用相关信息更加精确和直观,free -g向下取整了
iostat
- iostat -xdk 2 3
- rkb/s 每秒读取数据量kb
- wkB/s 每秒写入数据量kb
- svctm I/O请求的平均服务时间,单位是毫秒
- await I/O请求的平均等待时间,单位毫秒.值越小,性能越好
- util 一秒钟有百分之几的时间用户I/O操作.接近100%时,表示磁盘带宽跑满,需要优化程序或者增加磁盘
- rkB/s、wkB/s根据系统应用不同会有不同的值,但有规律遵循:长期、超大数据读写,坑定不正常,需要优化程序的读取
- svctm的值与await的值很接近,表示几乎没有I/O等待,磁盘性能好
- 如果await的值远高于svctm的值,则表示I/O队列等待太长,需要优化程序或更换更快的磁盘
ifstat
查看是否有网络IO
查看CPU较高的代码位置
- 先用top命令找出cpu占比最高的
- ps -ef | grep java或者jps进一步定位得到程序的PID
- 定位到具体的线程或者代码
- -m 显示所有的线程
- -p pid进程使用cpu的时间
- -o 该参数后是用户自定义的格式字段
- 将需要的线程ID转换为16进制格式(字母需要小写)
- jstack 进程ID | grep tid(16进制线程ID小写因为) -A60 #展示前60行