参考:
https://snailclimb.gitee.io/javaguide/#/?id=jvm-%e5%bf%85%e7%9c%8b-1
https://www.bilibili.com/video/BV18b411M7xz?p=102
运行时数据区域
本地方法栈和程序计数器
比如说我们现在点开Thread类的源码,会看到它的start0方法带有一个native关键字修饰,而且不存在方法体,这种用native修饰的方法就是本地方法,这是使用C来实现的,然后一般这些方法都会放到一个叫做本地方法栈的区域。
程序计数器其实就是一个指针,它指向了我们程序中下一句需要执行的指令,它也是内存区域中唯一一个不会出现OutOfMemoryError的区域,而且占用内存空间小到基本可以忽略不计。这个内存仅代表当前线程所执行的字节码的行号指示器,字节码解析器通过改变这个计数器的值选取下一条需要执行的字节码指令。
如果执行的是native方法,那这个指针就不工作了。
堆
主要放了一些存储的数据,比如**对象实例,数组···**等,它和方法区都同属于 线程共享区域 。也就是说它们都是 线程不安全 的。
栈
线程私有的,它的生命周期和线程相同,后进先出。
Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。
方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
方法区和永久代的关系
《Java 虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。 方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。
常用参数
为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
运行时常量池
直接内存(堆外内存)
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 错误出现。
JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
对象的创建过程
垃圾回收
内存分配
-
对象优先在 eden 区分配
大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。 -
大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。为什么要这样呢?
为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。 -
长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
-
动态对象年龄判定
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
判断确定垃圾
-
引用计数法
-
可达性分析算法
垃圾收集算法
标记-清除算法
标记清除算法就是分为“标记”和“清除”两个阶段。标记出所有需要回收的对象,标记结束后统一回收。
标记和清除的效率比较低下,且这种做法会让内存中的碎片非常多。这个导致了如果我们需要使用到较大的内存块时,无法分配到足够的连续内存。比如下图
标记整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
复制算法
它将可用内存按容量划分成两等分,每次只使用其中的一块。和survivor一样也是用from和to两个指针这样的玩法。fromPlace存满了,就把存活的对象copy到另一块toPlace上,然后交换指针的内容。这样就解决了碎片的问题。
这个算法的代价就是把内存缩水了,这样堆内存的使用效率就会变得十分低下了。
分代算法
- 在新生代-复制算法
每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量
存活对象的复制成本就可以完成收集。 - 在老年代-标记整理算法
因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标
记—整理” 算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存。
垃圾收集器
串行垃圾回收器
它为单线程环境设计,且只使用一个线程进行垃圾回收,会暂停所有的用户线程,所以不适合服务器环境。
新生代采用复制算法,老年代采用标记-整理算法。
- Serial 收集器
- Serial Old 收集器 它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
并行垃圾回收器
多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算/大数据处理首台处理等弱交互场景。
新生代采用复制算法,老年代采用标记-整理算法。
- ParNew 收集器
- Parallel Scavenge 收集器(关注点是吞吐量(高效率的利用 CPU))
- Parallel Old 收集器 Parallel Scavenge 收集器的老年代版本。
并发垃圾回收器
用户线程和垃圾收集线程同时执行(不一定并行,可能交替执行)不需要停顿用户线程。互联网公司多用,适用对响应时间有要求的场景。
CMS 收集器 老年代 标记-清除
优点:并发收集停顿低。
缺点:并发执行,对CPU压力较大,采用的标记清除算法会产生大量碎片。
初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;
并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
G1
G1垃圾回收器将堆内存分割成不同的区域然后并发地对其进行垃圾回收。
四个步骤
概括图
垃圾收集器组合参数设定
-XX:+UseSerialGC = Serial New+ Serial Old
-XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认)
-XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
-XX:+UseConcurrentMarkSweepGC = ParNew + CMS + Serial Old
-XX:+UseG1GC = G1
JVM参数
OOM
java.lang.StackOverflowError 栈溢出
public class StackOverflowErrorDemo {
public static void main(String[] args) {
stackOverflowError(0);
}
private static void stackOverflowError(int i){
System.out.println(i);
stackOverflowError(++i);
}
}
java.lang.OutOfMemoryError:Java heap space 堆溢出
public class JavaHeapSpaceDemo {
public static void main(String[] args) {
byte[] b = new byte[1024*1024*10];
}
}
java.lang.OutOfMemoryError:GC overhead limit exceeded
public class OverheadLimitDemo {
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:"+i);
e.printStackTrace();
throw e;
}
}
}
java.lang.OutOfMemoryError:Direct buffer memory
public class DirectBufferMemoryDemo {
public static void main(String[] args) {
ByteBuffer bb = ByteBuffer.allocateDirect(6*1024*1024);
}
}
java.lang.OutOfMemoryError:unable to create new native thread
linux
普通用户限制1024 可以通过配置修改
root用户无上限
java.lang.OutOfMemoryError:Metaspace
public class MetaspaceDemo {
static class OOMTest {
}
public static void main(String[] args) {
int i = 0;
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();
}
}
}
性能分析和JVM监控
Linux命令
top/top -H/(按 1 可以查看每个CPU情况)/top -Hp pid(查看进程的线程的情况)
uptime,系统性能命令精简版
主要查看 CPU MEM使用情况
load average说明:
是一段时间内系统的平均负载,三个参数分别代表时间段为 1分钟、5分钟、15分钟。
什么样的Load值得警惕(单核)?
Load < 0.7时:系统很闲,马路上没什么车,要考虑多部署一些服务
0.7 < Load < 1时:系统状态不错,马路可以轻松应对
Load == 1时:系统马上要处理不多来了,赶紧找一下原因
Load > 5时:马路已经非常繁忙了,进入马路的每辆汽车都要无法很快的运行
vmstat 查看CUP情况
free -m 查看内存情况
df -h 查看磁盘情况
iostat 查看io情况
ifstat 查看网络情况
pidstat -p 进程号 -r 采样间隔秒数 查看细节
ps ef|grep xxx
查看进程,用于获取进程ID
JDK命令
jps/jps -l 查看所有 Java 进程
jinfo vmid 实时地查看和调整虚拟机各项参数
输出当前 jvm 进程的全部参数和系统属性 (第一部分是系统的属性,第二部分是 JVM 的参数)。
jstat 监视虚拟机各种运行状态信息
jmp 生成堆转储快照
jhat 分析 heapdump 文件
jstack :生成虚拟机当前时刻的线程快照
arthas在线排查工具
参考:
https://www.cnblogs.com/qiaoyihang/p/10533672.html
dashboard 监控板
thread/thread id 线程详情
通过thread命令可以查看当前jvm进程的线程详情。可以查看线程的cpu使用时间占比,通过指定各种参数可以找出最忙的几个线程,以及阻塞其他线程的线程。具体如何使用这里不多做介绍,大家可以去看arthas的官方文档。
jvm jvm信息
通过jvm命令直接输出当前jvm的各种信息。
dump
将已加载类的字节码dump到本地磁盘上。
heapdump 生成堆的dump文件
…
jvisualvm
生产环境服务器变慢,诊断思路和性能评估
- top 查看整机情况
- vmstat 查看CPU
- free 查看内存
- df 查看硬盘
- iostat 查看磁盘
- ifstat 查看网络
- pidstat -p 进程号 -r 采样间隔秒数 查看细节
假如生产环境出现CPU占用过高,请谈谈你的分析思路和定位
CPU100%那么一定有线程在占用系统资源
-
找出哪个进程cpu高(top)
-
该进程中的哪个线程cpu高(top -Hp pid)
-
jstack 查看线程详情
jstack 进程ID | grep tid(16进制线程ID小写英文)-A60(打印前60行)
jstack 4639 | grep 123c -A60
类加载
参考:
https://mp.weixin.qq.com/s/eHqFONXXNc-LD4ugaKM6UA
加载过程
-
加载
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。
-
验证
确保加载的类信息符合JVM规范,没有安全方面的问题。 -
准备
为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
-
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。 -
初始化
初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 < clinit > ()方法的过程。对于< clinit >() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 < clinit >() 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。
类加载器
不是继承,是组合实现父子关系
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载
%JAVA_HOME%/lib ⽬录下的jar包和类或者或被 -Xbootclasspath 参数指定的路径中的所
有类。 - ExtensionClassLoader(扩展类加载器) :主要负责加载⽬录 %JRE_HOME%/lib/ext ⽬录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。
- AppClassLoader(应⽤程序类加载器) :⾯向我们⽤户的加载器,负责加载当前应⽤classpath下的所有jar包和类。
双亲委派
过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传递到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
双亲委派模型带来了什么好处呢?
双亲委派模型保证了Java程序的稳定运⾏,可以避免类的重复加载(JVM 区分不同类的⽅式不仅仅根据类名,相同的类⽂件被不同的类加载器加载产⽣的是两个不同的类),也保证了 Java 的核⼼ API 不被篡改。如果不⽤没有使⽤双亲委派模型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为 java.lang.Object 类的话,那么程序运⾏的时候,系统就会出现多个不同的Object 类。