引言
什么是JVM
- 定义
Java Virtual Machine -java程序的运行环境(java二进制字节码的运行环境)
- 好处
一次编译,导出运行
自动内存管理,垃圾回收功能
- 比较
jvm jre jdk
学习路线
一个类从java源代码编译为java的二进制字节码以后,必须经过ClassLoader(类加载器)才能被加载到JVM里去运行
类都是放在Method Area(方法区)的部分,类将来创建的实例对象是放在Heap(堆)的部分,而堆里面的这些对象调用方法时又会用到JVM Stacks(虚拟机栈),PC Register(程序计数器)及Native Method Stacks(本地方法栈)
方法执行时每行代码是由执行引擎中的Interpreter(解释器)逐行进行一个执行。方法里的热点代码,也就是频繁被调用的代码,会有一个JIT Compiler(即使编译器),它对热点代码要做编译,优化的执行
GC(垃圾回收)会对堆里面一些不再被引用的对象进行垃圾回收
还有一些java代码不方便实现的功能必须调用底层操作系统的功能,所以要跟操作系统的一些功能打交道就需要借助本地方法接口来调用操作系统提供的一些功能方法。
内存结构
程序计数器
Program Counter Register 程序计数器(寄存器)
记住下一条jvm指令的执行地址
特点
- 是线程私有的
- 不会存在内存溢出
虚拟机栈
定义
Java Virtual Machine Stacks(Java虚拟机栈)
-Xss 256K 设置栈内存大小为256k
每个线程运行时所需要的内存,成为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的哪个方法
没有垃圾回收机制方法执行完直接出栈
栈内存溢出
StackOverflowError 栈内存溢出
栈帧过多导致栈内存溢出
栈帧过大导致栈内存溢出
线程运行诊断
用top定位哪个进程对cpu的占用过高
ps -H eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位哪个线程引起的cpu占用过高)
jstack 进程id
- 可以根据线程id找到问题的线程,进一步定位到问题代码的源码行号
本地方法栈
给本地方法的运行提供内存的空间
堆
定义
Heap堆
通过new关键字,创建的对象都会使用堆内存
特点
- 线程共享,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
-Xmx[8m] 设置堆空间大小为8m
内存溢出
OutOfMemoryError: Java heap space 堆内存溢出
堆内存诊断
jps工具: 查看当前系统中有哪些java进程
jmap工具: 查看堆内存占用情况 jamp -heap 进程id
jconsole工具: 图形界面的,多功能的监控工具,可以连续监测
方法区
定义
是所有java虚拟机线程共享的区,它存储了跟类的结构相关的信息,有类的成员变量,方法数据以及成员方法和构造器方法代码,包括一些特殊方法(类的构造器)
方法区在虚拟机启动时被创建,逻辑上是堆的组成部分,但是每个厂家实现都不太相同,并不强制
方法区也会导致内存溢出的错误OutOfMemeoryError:Metaspace
组成
方法区内存溢出
1.8以前会导致永久代内存溢出
1.8以后会导致元空间内存溢出
- XX:MaxMetaspaceSize=8m 设置最大元空间大小为8m
运行时常量池
常量池:就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量(如helloworld的字符串或是一些基本的数据类型)等信息
运行时常量池: 常量池是*.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真是地址
StringTable
- 常量池与串池的关系
- 字符串变量拼接
StringBuilder,toString()相当于创建一个新的String对象new String(“ab”)
面试题
打印false,因为s3是在串池里面的,而s4是通过new出来的在堆里面所有地址值不一样String s3 = "ab"; String s5 = "a" + "b"; System.out.println(s3 == s5);
打印true,javac在编译期间的优化,结果已经在编译期确定为ab
- 特性
常量池中的字符串仅是符号,第一次用到时才变为对象
利用串池的机制,来避免重复创建字符串对象
字符串变量拼接的原理是StringBuilder(1.8)
字符串常量拼接的原理是编译器优化
可以使用Intern方法,主动将串池中还没有的字符串对象放入串池
- 1.8将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,串池中的常量与该字符串一样,会把串池中的对象返回
- 1.6将这个字符串对象尝试放入串池,如果有则不会放入,如果没有会把此对象复制一份放入串池,串池中的常量与该字符串不一样,会把串池中的对象返回
- 位置
- 垃圾回收
- 性能调优
- 调整 -XX:StringTableSize=桶个数
- 考虑将字符串对象是否入池
直接内存
定义
Direct Memory(属于操作系统的内存)
- 常用于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
- 基本使用
需要两份数据,一份在系统缓存区,然后再复制一份在java缓冲区,java程序才能访问到
使用directBuffer后
读到直接缓存区,java程序和系统都能访问,少了一次缓冲区的复制操作
内存溢出
直接内存不受jvm虚拟机内存管理
直接内存也会导致内存溢出: OutOfMemoryError: Direct buffer memory
分配和回收原理
使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
垃圾回收
如何判断对象可以回收
引用计数法
只要一个对象被其他变量所引用,那么就让这个对象它的一个计数加1,如果被引用了两次,那就让它的计数变成2,如果某一个变量不再引用它了,那么让它的计数减1, 当这个对象它的引用计数变为0时,意味着没有人再引用它了,那么它就可以作为一个垃圾进行一个回收
可达性分析算法
Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
扫描堆中的对象,看是否能够沿着GC Root对象为起点的引用链找到该对象,找不到,表示可以回收
哪些对象可以作为GC Root?
- d
在垃圾回收之前,首先会对堆中的所有对象进行一遍扫描,然后看看每一个对象是不是被刚才提到的根对象所直接引用或间接的引用,如果是那么这个对象就不能被回,反之如果一个对象没有被根对象直接或间接引用那么这个对象就是作为垃圾将来会被回收,比如葡萄
四种引用
实线表示强引用,虚线表示其他引用
- 强引用
只有所有GC Root对象都不通过【强引用】引用该对象,该对象才能被垃圾回收
- 软引用(SoftReference)
仅有软引用引用该对象,在垃圾回收后,内存仍不足时会再次发出垃圾回收,回收软引用对象
可以配合引用队列来释放软引用自身
在内存不足时,就会把软引用对应的这些byte数组对象的内存进行释放
软引用,配合引用队列
- 弱引用(WeakReference)
仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
可以配合引用队列来释放弱引用自身
- 虚引用(PhantomReference)
必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存
- 终结器引用(FinalReference)
无需动手编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时才能回收被引用对象
垃圾回收算法
标记清除
定义:Mark Sweep
速度较快
有内存碎片
标记整理
定义: Mark Compact
没有内存碎片
需要改变对象的内存地址,速度慢
复制
定义: copy>
交换from和to的位置
不会产生碎片
会占用双倍的内存空间
分代垃圾回收
- 对象首先分配在伊甸园区域
- 新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄加1并且交换from与to
- minor gc会引发 stop the world,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
- 当对象寿命超过阈值时,会晋升至老年代,最大寿命时15(4bit 1111)
- 当老年代空间不足,会先尝试触发minor gc,如果之后空间仍不足,那么触发full gc,stop the world 的时间更长
相关vm参数
GC分析
垃圾回收器
串行
- 单线程
- 堆内存较小,适合个人电脑
吞吐量优先
- 多线程
- 堆内存较大,多核cpu
- 让单位时间内,stop the world 的时间最短 0.2 0.2 = 0.4
响应时间优先
- 多线程
- 堆内存较大,多核cpu
- 尽可能让单次stop the world的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
G1
定义: GarbageFirst
- 2004 论文发布
- 2009 JDK 6u14 体验
- 2012 JDK 7u4 官方支持
- 2017 JDK 9 默认
适用场景:
- 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是200ms
- 超大堆内存,会将堆划分为多个大小相等的Regin(区域)
- 整体上是标记+整理算法,两个区域之间是复制算法
相关JVM参数:
- -XX:+USEG1GC (1.8不是默认需要开开)
- -XX:G1HeapRegionSize=size
- -XX:MaxGCPauseMillis=time
- G1垃圾回收阶段
- Young Collection
- 会 stop the world
- Yong Collection + CM
在Young GC 时会进行GC Root的初始化标记
老年代占用堆空间比例达到阈值时,进行并发标记(不会stop the world),由下面的JVM参数决定
-XX:InitiatingHeapOccupancyPercent=percent(默认45%)
- Mixed Collection
会对 E,S,O 进行全面垃圾回收
- 最终标记(Remark)会stop the world
- 拷贝存活(Evacuation)会stop the world
- -XX:MaxGCPauseMillis=ms (根据最大暂停时间有选择的去进行一个回收)
- 对于老年代的区域来讲优先要回收那些垃圾最多的区,为了达到暂停时间短的目标
- Full GC
SerialGC
- 新生代内存不足发生的垃圾收集-minor gc
- 老年代内存不足发生的垃圾收集-full gc
ParallelGC
- 新生代内存不足发生的垃圾收集-minor gc
- 老年代内存不足发生的垃圾收集-full gc
CMS
- 新生代内存不足发生的垃圾收集-minor gc
- 老年代内存不足
G1
- 新生代内存不足发生的垃圾收集-minor gc
- 老年代内存不足
- Young Collection跨代引用
- 新生代回收的跨代引用(老年代引用新生代)问题
- Remark
- pre-write barrier + satb_mark_queue
- JDK 8u20 字符串去重
- JDK 8u40 并发标记类卸载
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
-XX:+ClassUnloadingWithConcurrentMark 默认启用
- JDK 8u60 回收巨型对象
- 一个对象大于region的一半时,称之为巨型对象
- G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1会跟踪老年代所有incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
- JDK 9 并发标记起始时间的调整
并发标记必须在堆空间占满前完成,否则退回为FullGC
JDK 9 之前需要使用 -XX:IntiatingHeapOccupancyPercent
JDK 9 可以动态调整
- -XX:IntiatingHeapOccupancyPercent用来设置初始值
- 进行数据采样并动态调整
- 总会添加一个安全的空档空间
垃圾回收调优
新生代调优
新生代的特点
- 所有的new操作的内存分配非常廉价
– TLAB thread-local allocation buffer- 死亡对象的回收代价是零
- 大部分对象用过即死
- Minor GC的时间远远低于Full GC
类加载和字节码技术
类文件结构
根据JVM规范,类文件结构如下:
javap 工具
自己分析类文件结构太麻烦,Oracle提供了javap工具来反编译class文件
图解方式执行流程
- 原始java代码
- 编译后的字节码文件
- 常量池载入运行时常量池
- 方法字节码载入方法区
- main线程开始运行、分配栈帧内存
- 执行引擎开始执行字节码
iload1 iload2
iadd
iload3
字节码指令
byte,short, char 都会按int比较,因为操作数栈都是4字节
goto用来进行跳转到指定行号的字节码
多态的原理
当执行invokevirtual指令时
- 先通过栈帧中的对象引用找到对象
- 分析对象头,找到对象的实际Class
- Class结构中有vtable,它在类加载的链接阶段就已经根据方法的重写规则生成好了
- 查表得到方法的具体地址
- 执行方法的字节码
类加载阶段
加载
将类的字节码载入方法区中,内部采用C++的instanceKlass描述java类,它的重要field有:
- _java_mirror即java的类镜像,例如对String来说,就是String.class,作用是把klass暴露给java使用
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法表
如果这个类还有父类没有加载,先加载父类
加载和链接可能是交替运行的注意
- instanceKlass这样的【元数据】是存储在方法区(1.8后的元空间内),但_java_mirror是存储在堆中
链接
- 验证
验证类是否符合JVM规范,安全性检查
- 准备
为static变量分配空间,设置默认值
- static变量在jdk7前存储于instanceKlass末尾,从jdk7开始,存储于_java_mirror末尾
- static变量分配空间和赋值时两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果static变量是final的基本类型及字符串常量,那么编译阶段结束值就确定了,赋值在准备阶段完成
- 如果static变量是final,但属于引用类型,那么赋值也会在初始化阶段完成
- 解析
将常量池中的符号引用解析为直接引用
符号引用仅仅是个符号,它并不知道类或者是方法、属性在内存的哪个位置,但是经过了解析以后,变成了直接引用就能确切的知道这个类或者是方法、属性在内存中的位置了
初始化
<cinit>()v
方法
初始化即调用
<cinit>
()v,虚拟机会保证这个类的【构造方法】的线程安全
- 发生的时机
概括的说,类初始化是【懒惰的】
- main方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new会导致初始化
不会导致类初始化的情况
- 访问类的static final静态常量(基本类型和字符串)不会触发初始化
- 类对象.class不会触发初始化
- 创建该类的数组不会触发初始化
- 类加载器的loadClass方法
- Class.forName的参数2为false时
类加载器
- 以JDK8为例:
名称 | 加载哪的类 | 说明 |
---|---|---|
BootStrap ClassLoader | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader | JAVA_HOME/jre/lib/ext | 上级为Bootstrap,显示为null |
Application ClassLoader | classpath | 上级为Extension |
自定义类加载器 | 自定义 | 上级为Application |
- 双亲委派模式
所谓的双亲委派,就是指调用类加载器的loadClass方法时,找到类的规则
注意:
这里的双亲,翻译为上级似乎更为合适,因为它们并没有继承关系
内存模型
Java内存模型
Java Memory Model(JMM)的意思,简单的说,JMM定义了一套在多线程读写共享数据是成员变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。
- 原子性解决方法
synchronized(同步关键字)既能保证可见性也能保证原子性
可以把锁对象比作一个房间,t1进入了房间并且锁住了门,然后执行代码,这时t2也运行到了同步代码块,它发现们已经被锁住了,只能在门外等待,当t1执行完同步代码块里面的代码时,才会解开锁对象,从房间里走出来,此时t2才能进入,然后也锁住门,执行它的代码。需要注意的是,t1和t2必须使用同一个对象锁。
- 可见性解决方法
volatile(易变关键字)只能保证可见性
它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作内存中查找变量的值,必须到主内存中获取它的值,线程操作volatile变量都是直接操作主内存。
- 有序性解决方法
volatile修饰的变量,可以禁止指令重排
CAS与原子类
- CAS
即Compare and Swap,它体现的是一种乐观锁的思想,比如多个线程要对一个共享的整体变量执行+1操作:
获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰,结合CAS和volatile可以实现无锁并发,适用于竞争不激烈,多核CPU的场景下。
- 相较于synchronized线程不会阻塞,性能会高一些,但是如果竞争激烈,会一直重试,反而效率会受影响
CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令。
- 原子类
juc中提供了原子类,可以提供线程安全的操作,例如:AtomicInteger,AtomicBoolean等,底层采用CAS技术+volatile来实现的。
Synchronized优化
Java HotSpot虚拟机中,每个对象都有对象头(包括class指针和Mark Word)。Mark Word平时存储这个对象的哈希码、分代年龄、当加锁时,这些信息就根据情况被替换为标记位、线程锁记录指针、重量级锁指针、线程ID等内容。