1、jvm入门
jvm、jre、jdk他们的关系如下图
2、class文件结构
2.1、class文件的格式,
类名.class文件是二进制字节码文件,用于保存 Java类的二进制编码以及Class对象,每一个 Java类都有一个解释该类特征的 Class对象。
格式大体如下图所示:
数据类型
u1 | 1个字节,无符号类型 |
u2 | 2个字节,无符号类型 |
u4 | 4个字节,无符号类型 |
u8 | 8个字节,无符号类型 |
_info | 表类型, _info的来源是hotspot源码中的写法 |
classFile的构成
详细信息参考:ClassFileFormat_查看16进制文件的工具-CSDN博客
2.2、JVM的8个原子指令
lock(锁定)、read(读取)、load(载入)、use(使用)
assign(赋值)、store(存储)、write(写入)、unlock(解锁)
3、内存加载过程
编译后的class文件是怎么加载到内存的尼?
大概流程如下:
3.1、装载
查找和导入class文件
3.2、链接
执行检验、准备、解析;其中解析步骤可选
3.2.1、检验:检查载入class文件数据的准确性
检查文件开头格式等;
3.2.2、准备:给类的静态变量分配存储空间,赋默认值
如:static int i=8;这个时候给i赋默认值0;
3.2.3、解析: 将符号引用转换成直接内存地址,可以直接访问到内容
3.3、初始化
将类的静态变量、静态代码块执行初始化工作
3.4、类加载器
3.4.1、Bootstrap
最顶层的加载器,他是用来加载lib里jdk最核心的内容,如:rt.jar、charset.jar等核心类
3.4.2、Extension
加载拓展包里的各种各样文件,这些在jdk安装目录jre/lib/ext下
3.4.3、Application
加载classpath下所指定的内容
3.4.5、Custom
自定义加载器
3.4.5、双亲委派机制
以上的加载机制逐层向parent提交,parent看是自己这层该加载的,就返回,如果没有返回就自己记载,这种加载机制就叫双亲委派,先给parent优先。
双亲委派能避免,如:自定义java.lang.String类就不行,保证核心代码的安全性;
4、运行时内存结构
-
JDK1.8前:
-
JDK1.8后:
5、GC
5.1、如何判断一个对象没有引用
5.1.1、引用计数法:
对象中添加一个引用计数器,每当有一个地方引用计数器就增加1,引用失效就减少1,计数器为0就不可用;缺点就在于无法处理对象直接相互引用的问题,因为相互引用以后无法使计数器为0,所以无法回收;
5.1.2、可达性分析算法
也就是我们常说的GC Root,,当一个对象没有与任何引用链相连的时候,就可以对该对象进行回收,下面是Java中GC Root对象使用的几个地方
5.2、什么时候触发GC
当内存空间不足的时候就需要触发GC,GC回收的时候采用的是分代收集的算法,
主要分为年轻代和老年代,接下来我们简单介绍一下这2种方式:
年轻代:当一个对象被创建的时候,内存分配首先分配在年轻代,大部分对象创建以后都不再使用,对象很快变得不可达,就是对象无用,由于垃圾是被年轻代清理掉的,所以被叫做Minor GC或者Young GC。
老年代:对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。
1.当一个对象被创建的时候(new)首先会在年轻代的Eden区被创建,直到当GC的时候,根据可达性算法,看一个对象是否消亡,没有消亡的对象会被放入年轻带的Survivor区,消亡的直接被Minor GC Kill掉;
2.进入到Survivor区的对象也不是安全的,当下一次Minor GC来的时候还是会检查Enden和Survivor存放对象区域中对象是否存活,存活放入另外一块Survivor区域;
3.当2个Survivor区切换几次以后,会直接进入老年代,当然进入到老年代也不是安全的,当老年代内存空间不足的时候,会触发Major GC,已经消亡的依然还是被Kill掉
5.3、GC算法
5.3.1、标记清除法
两次扫描第一次找到有用的,第二次找到没用的进行清理、容易产生碎片
存活对象比较多时效率较高
5.3.2、复制算法
适用于存活对象少的情况,扫描一次,效率提高,没有碎片空间浪费
5.3.3、标记压缩算法
扫描2次、需要移动对象、效率较低
不会产生碎片,方便对象分配、不会产生内存减半
5.4、垃圾回收器
- 并发(concurrent) vs 并行(parallel)
-
- 并行是同时进行(多 CPU)
- 并发可交替
- Minor GC vs Major GC vs Full GC
-
- Minor GC:只回收新生代
- Major GC:只回收永久代
- Full GC: 回收整个堆。相当于 Minor GC + Major GC
jvm默认是Parallel Scavenge和Parallel old(PS+PO)
- serial。单线程,简单高效。复制算法
- serial old。serial 的永久代版本。采用标记整理算法。
- parallel Scavenge。 复制算法、多线程、并行。但侧重吞吐量,拥有自适应调节的能力。适合用在后台不需要太多用户交互的地方。
- parallel old。parallel Scavenge 的老年代版本,采用标记整理算法。与 parallel scavenge 搭配可以用在注重吞吐量及 CPU 资源敏感的地方。
- PerNew。3的新版本做了些增强与CMS适配。
- CMS(concurrent mark sweep)。并发低停顿,使用标记清理算法。非常优秀的一款收集器,但还是有几个缺点:
- 对 CPU 资源敏感,当其小于数量小于 4 个是可能会对用户程序有较大影响。默认启动回收线程数 = (CPU 数 + 3)/ 4
- 无法处理浮动垃圾。浮动垃圾:在垃圾回收期间生成的垃圾
- 回收后会留有大量的空间碎片
- G1.逻辑分区,物理不分区
- ZGC
- Java 8及之前版本:Parallel GC是默认的垃圾回收器。它通过多线程并行地进行垃圾收集,适用于多核处理器,并且通常用于处理大型堆内存。
-
Java 9及以后版本:G1垃圾回收器成为了默认的垃圾回收器。G1垃圾回收器是一种面向服务端应用的垃圾回收器,它采用了分代的垃圾回收策略,可以更加灵活地管理堆内存,并且能够在不牺牲太多吞吐量的情况下实现更加可预测的垃圾回收。
5.5、GC配置参数
java启动参数共分为三类;
其一是标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;
其二是非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;
其三是非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用;
-
-Xms
:设置JVM启动时的初始堆内存大小。例如,-Xms512m
。 -
-Xmx
:设置JVM最大可用堆内存大小。例如,-Xmx1024m
。 -
-Xmn
:设置JVM新生代内存大小。例如,-Xmn1024m
。 -
-Xss
:设置每个线程的堆栈大小。例如,-Xss256k
。 -
-XX:NewSize
:设置新生代的默认初始大小。例如,-XX:NewSize=256m
。 -
-XX:MaxNewSize
:设置新生代的最大大小。例如,-XX:MaxNewSize=512m
。 -
-XX:PermSize
:设置永久代(PermGen space)的初始大小。例如,-XX:PermSize=128m
。 -
-XX:MaxPermSize
:设置永久代的最大大小。例如,-XX:MaxPermSize=256m
。 -
-XX:+UseG1GC
:启用G1垃圾收集器。 -
-XX:MaxGCPauseMillis
:设置G1收集器的最大暂停时间。例如,-XX:MaxGCPauseMillis=200
。 -
-XX:+PrintGCDetails
:打印详细的GC日志信息。-
PrintGCTimeStamps打印GC时间
-
PrintGCCauses打印GC产生的原因
-
-
-XX:-DoEscapeAnalysis 去掉逃逸分析
-
-XX:-UseTLAB 去掉TLAB
-
-XX:MaxTenuringThreshold 指定次数YGC,达到这个次数进入老年代,
-
Parallel Scavenge 默认15 ;CMS默认6 ;G1默认15
-
X是分表参数;m是meomory;s是最小值;x是最大值;n是new;s是stack;
:后面+是用;后面-禁用
5.6、TLAB
线程本地分配(Thread Local Allocation Buffer):占用eden 默认1%,这块空间线程独有;
优先栈上分配,分配不了在进行本地分配;
5.7、逃逸分析
在Java中每一个对象都有一定的作用域,理论上,一个对象在一块代码中构造,那么也应该在这块代码中被回收,但是实际上,我们经常会让一个对象存活更长的时间,超过定义它的代码块,这就好比一个人逃出了生他养他的地方,我们将这种现象称为逃逸。
三种逃逸现象
栈上分配:
众所周知,Java中对象时分配在堆上的,在初始化时,会在堆上分配一块空间,当这个对象不再使用时,会在之后发生垃圾回收时被回收,这是一个Java对象正常的生命周期。但是当能够明确对象不会发生逃逸时,就可以对这个对象做一个优化,不将其分配到堆上,而是直接分配到栈上,这样在方法结束时,这个对象就会随着方法的出栈而销毁,这样就可以将少垃圾回收的压力。
同步消除
在多线程中,对于一个变量操作进行同步操作是效率很低的,当我们确定一个对象不会发生逃逸时,那么就没有必要对这个对象进行同步操作,所以如果代码中有对这种变量操作的同步操作,JVM将会取消同步,从而提升性能。
标量替换
标量指的是没有办法再分解为更小的数据的类型,即Java中的基本类型,我们平时定义的类都属于聚合量。标量替换即是将一个聚合量拆成多个标量来替换,即用一些基本类型来代替一个对象。如果明确对象不会发生逃逸,并且可以进行标量替换的话,那么就可以不创建这个对象,而是直接使用基本类型来代替,这样也就可以节省创建和销毁对象的开销。
虽然基于逃逸技术的优化能够提升程序运行时的性能,但是在实际生产中,对象逃逸的分析默认是不开启的。这是因为分析一个对象是否会发生逃逸消耗比较大,所以,开启逃逸分析并进行这些优化之后得到的效果,并不一定就比不进行优化更好。如果确定开启逃逸分析效率更好,那么可以使用参数-XX:+DoEscapeAnalysis来开启逃逸分析
6.0、引用类型-强、软、弱、虚
强引用
强引用 是最常见的引用, 首先用 new 关键字创建对象的时候。这个对象就是一个强引用也就是默认的引用类型。 只要强引用的对象是可触及的, 那么他就不会被回收!如果强引用对象超过了他的作用范围或者被设置为 null 那就可以被回收了。只要有强引用在, 当内存不足的时候jvm就算抛出OOM也不会回收掉它!
如果对应的引用被使用了,且没有被设置为null,不会回收;
public class NormalReferenceTest {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize");
}
public static void main(String[] args) throws IOException {
NormalReferenceTest n = new NormalReferenceTest();
System.out.println(n);
n=null;
System.out.println(n);
System.gc(); //垃圾回收
System.in.read(); //模拟堵塞,因为回收异步的,不然看不到效果
}
}
执行结果:
reference.M@1540e19d
null
finalize
软引用
当一个对象被一个软引用所指向的时候,只有系统内存不够用的时候,才会回收它。
应用场景:缓存
public class SoftReferenceTest {
public static void main(String[] args) throws InterruptedException {
SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*30]);
System.out.println(m.get());
System.gc();
Thread.sleep(500);
System.out.println(m.get());
//启动参数中固定heap参数最大值为50m,这里模拟内存不够时 垃圾回收软引用的空间
byte[] b = new byte[1024*1024*30];
System.out.println(b);
System.out.println(m.get());
}
}
打印结果:
[B@1540e19d
[GC (System.gc()) [PSYoungGen: 2048K->592K(14848K)] 32768K->31320K(49152K), 0.0013733 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 592K->0K(14848K)] [ParOldGen: 30728K->31107K(34304K)] 31320K->31107K(49152K), [Metaspace: 3151K->3151K(1056768K)], 0.0048637 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[B@1540e19d
[GC (Allocation Failure) [PSYoungGen: 256K->0K(14848K)] 31363K->31107K(49152K), 0.0028011 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 0K->0K(14848K)] [ParOldGen: 31107K->31101K(34304K)] 31107K->31101K(49152K), [Metaspace: 3152K->3152K(1056768K)], 0.0086161 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [PSYoungGen: 0K->0K(14848K)] 31101K->31101K(49152K), 0.0016592 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(14848K)] [ParOldGen: 31101K->363K(34304K)] 31101K->363K(49152K), [Metaspace: 3152K->3152K(1056768K)], 0.0060233 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[B@677327b6
null
Heap
PSYoungGen total 14848K, used 640K [0x00000007bef80000, 0x00000007c0000000, 0x00000007c0000000)
eden space 12800K, 5% used [0x00000007bef80000,0x00000007bf0201d8,0x00000007bfc00000)
from space 2048K, 0% used [0x00000007bfc00000,0x00000007bfc00000,0x00000007bfe00000)
to space 2048K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007c0000000)
ParOldGen total 34304K, used 31083K [0x00000007bce00000, 0x00000007bef80000, 0x00000007bef80000)
object space 34304K, 90% used [0x00000007bce00000,0x00000007bec5ae88,0x00000007bef80000)
Metaspace used 3158K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 348K, capacity 388K, committed 512K, reserved 1048576K
对应的配置:
弱引用
弱引用,只要遭到gc就会被回收,如果有强引用指向了弱引用,只要强引用消失,弱引用就会被回收
public class WeakReferenceTest {
public static void main(String[] args) {
WeakReferenceTest test = new WeakReferenceTest();
WeakReference<WeakReferenceTest> m = new WeakReference<>(test);
System.out.println(m.get());
test = null; //只要这个强引用消失,弱引用就会被回收
System.gc();
System.out.println(m.get());
}
}
打印结果:
reference.WeakReferenceTest@1540e19d
null
这里ThreadLocal就是基于弱引用实现的:
可以看出ThreadLocal是由当前线程的thread和Map(threadLocals)实现的
当thread强引用消失了,map中的key的指向也被回收了,但有可能key指向了null,这个时候Map就会用用存在,造成内存泄漏。所以使用ThreadLocal时务必使用remove不然容易内存泄漏;
虚引用
管理堆外内存,构造方法有两个参数第二个参数必须是队列,给写JVM的人用的;如堆外内存的回收处理。
队列就是用来处理回收堆外内存逻辑的
public class PhantomReferenceTest {
private static final List<Object> list = new LinkedList<>();
private static final ReferenceQueue<PhantomReferenceTest> queue = new ReferenceQueue<>();
public static void main(String[] args) {
PhantomReference<PhantomReferenceTest> m = new PhantomReference<>(new PhantomReferenceTest(),queue);
new Thread(()->{
while (true){
list.add(new byte[1024*1024]);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(m.get());
}
}).start();
new Thread(()->{
while (true){
Reference<? extends PhantomReferenceTest> poll = queue.poll();
if(null != poll){
System.out.println("虚引用对象被jvm回收了 " + poll);
}
}
}).start();
}
}
7.0、调优
cpu飙高处理
1、找出哪个进程cpu飙高
top
2、该进程哪个线程cpu高
top -Hp ‘pid’
3、导出该线程的堆栈
jstack
4、对dump日志进行分析
jconsole、jvisualvm、jprofiler
阿里Arthas在线分析