视频参考
定义: java程序的运行环境(java二进制字节码的运行环境)
好处:一次编写,到处运行;自动内存管理,垃圾回收功能;(数组下标越界检查;多态)
内存结构

1.程序计数器
1.1作用
记住下一条JVM指令的执行地址。
Java源码经过编译形成二进制字节码(JVM指令),JVM指令经过解释器生成机器码,机器码在CPU中执行。
程序计数器通过寄存器实现。
1.2特点
- 线程私有:每个线程有自己的程序计数器
- 不会存在内存溢出
2.虚拟机栈
栈:线程运行需要的内存空间
栈帧:每个方法运行时需要的内存(参数、局部变量、返回地址)
2.1定义
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
问题辨析
- 垃圾回收是否涉及栈内存(否。每次方法调用结束后,都会自动弹出栈,自动回收掉)
- 栈内存分配越大越好吗?(否。栈内存越大,线程数越少,因为物理内存是固定的)
- 方法内的局部变量是否线程安全?(如果方法内局部变量没有逃离方法作用范围,那么是线程安全的;如果是局部变量引用了对象并逃离方法的作用范围,需要考虑线程安全问题)
2.2栈内存溢出
2.3线程运行诊断
案例1:CPU占用过多
定位:
- 用top命令定位哪个进程对CPU的占用过高
- ps H -eo pid,tid,%cpu | grep 进程id(用ps命令进一步定位是哪个线程)
- jstack 进程id(可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号)
案例2:程序运行很长时间没有结果
jstack 进程id
3.本地方法栈
给本地方法提供一个内存的空间。很多本地方法都是用c语言等编写的。
4.堆
4.1定义
Heap堆
特点
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制
4.2堆内存溢出
4.3堆内存诊断
- jps工具
查看当前系统中有哪些java进程
- jmap工具
查看堆内存占用情况(jmap -heap 进程id)
- jconsole工具
图形界面的,多功能的监测工具,可以连续监测
案例
- 垃圾回收后,内存占用仍然较高
jmap, jconsole=>jvitsualvm
5.方法区
5.1定义
存储类、类加载器、常量池等类的信息。
5.2组成
1.6中,永久代作为方法区的实现,永久代存储类、类加载器、运行时常量池。
1.8中,方法区使用元空间实现,不占用堆内存,不由JVM管理内存,移出到本地内存。
5.3方法区内存溢出
- 1.8以前会导致永久代内存溢出(-XX:MaxPermSize=8m)
- 1.8之后会导致元空间内存溢出(-XX:MaxMetaspaceSize=8m)
5.4运行时常量池
5.5StringTable特性
- 常量池中的字符串仅是符号,第一次用到时才变为对象
- 利用串池的机制,来避免重复创建字符串对象
- 字符串变量拼接的原理是StringBuilder
- 字符串常量拼接的原理是编译器优化
- 可以使用intern方法,主动将串池中还没有的字符串对象放入串池
- 1.8将这个字符串对象放入串池,如果有则不会放入,
如果没有则放入串池,会把串池中的对象返回
- 1.6将这个字符串对象放入串池,如果有则不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回
5.6StringTable位置

5.7StringTable垃圾回收
5.8StringTable性能调优
底层是HashTable。
桶的数量越大,冲突越少,运行用时越少。
调整 -XX:StringTableSize=桶个数
6.直接内存
6.1定义
- 常见于NIO操作时,用于数据缓冲区
- 分配回收成本较高,但读写性能高
- 不受JVM内存回收管理
6.2分配和回收原理
- 使用了unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,name就会由ReferenceHandler线程通过Cleaner的clean方法调用clean方法调用freeMemory来释放直接内存
垃圾回收
1.如何判断对象可以回收
1.1引用计数法
引用一个对象,计数加1;释放引用,计数减1。存在循环引用问题,使得引用无法减为0。
1.2可达性分析算法
1.3四种引用
- 强引用:只有当所有引用该对象的连接都断开时才会被回收
- 软引用:只有软引用连接,没有其他强引用引用它,当垃圾回收时内存不够会将其回收
- 弱引用:当没有其他强引用引用时,只要发生垃圾回收,都会将弱引用引用的对象回收
- 虚引用:必须配合引用队列使用,虚引用(eg:垃圾回收时Cleaner放入引用队列,其中有一个直接内存地址,其不归JVM管理,调用Unsafe.freeMemory方法进行释放)
- 终结器引用:当没有强引用引用这个对象时,有虚拟机创建对应的终结器引用,当这个对象被垃圾回收时,会将终结器引用加入到引用队列。再由一个优先级很低的线程finallize查看引用队列中是否有终结器引用,发现后会找到这个对象,调用finallize方法。调用完后下次垃圾回收就可以回收这个对象占用的内存。
2.垃圾回收算法
2.1标记清除算法
先标记,再清除。清除只需要将起始和结束地址做一个记录即可,速度快。但是容易产生内存碎片。
2.2标记整理算法
整理过程中将对象前移,整理回收的空间。没有内存碎片,但是整理设计对象移动,因此效率较低。
2.3复制算法
两块区域(From和To),先标记垃圾,接着将对象复制到To区,最后将From区的垃圾全部回收,并将From和To交换区域。优点是不产生内存碎片,缺点是占用双倍内存空间。
3.分代回收
新生代:伊甸园、幸存区From、幸存区To
老年代
创建一个新的对象首先存放在伊甸园,当伊甸园满时,触发一次Minor GC。采用复制算法将剩下的对象放在幸存区To,并将幸存对象寿命加1,伊甸园剩下的对象就被回收掉了。交换幸存区From和To的位置。
再次向伊甸园存放对象,等到满时再次触发Minor GC。这时将伊甸园里幸存的对象放入幸存区To,寿命值加1;将幸存区From存活的对象放入幸存区To,寿命值加1,此时为2。将剩下的垃圾清除掉。再次交换From和To的位置。幸存区中寿命(最大寿命是15)值超过阈值,就将其晋升到老年代。
Minor GC会引发stop the world
老年代内存不足,会先尝试Minor GC,若空间仍不足会触发Full GC。如果还是不足会报异常:out of memory。
3.1相关VM参数

4.垃圾回收器
4.1串行
开启命令:
-XX:+UserSerialGC = Serial + SerialOld
前者工作在新生代,使用复制算法
后者工作在老年代,使用标记整理算法
4.2吞吐量优先
- 多线程
- 堆内存较大,多核CPU
- 让单位时间内,STW的时间最短
-XX:+UserParallelGC -XX:+UserParallelOldGC(开启一个自动开启另一个)
-XX:UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio
-XX:MaxGCPauseMillis=ms
-XX:ParallelGCThreads=n
4.3响应时间优先
- 多线程
- 堆内存较大,多核CPU
- 尽可能让STW单次时间最短
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC SerialOld
-XX:ParallelGCThreads=n -XX:ConcGCThreads = threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark
4.4G1
适用场景
- 同时注重吞吐量和低延迟,默认暂停目标是200ms
- 超大堆内存,会将堆划分为多个大小相等的Region
- 整体上是标记整理算法,两个区域之间是复制算法
相关JVM参数
- -XX:+UseG1GC
- -XX:G1HeapRegionSize=size
- -XX:MaxGCPauseMillis=time
1)G1垃圾回收阶段
新生代回收-新生代回收+并发标记-混合回收
2)Young Collection
3)Young Collection+CM
- 在Young GC时会进行GC Root的初始标记
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定。
-XX:InitiatingHeapOccupancyPercent=percent
4)Mixed Collection
会对E、S、O进行全面垃圾回收
-XX:MaxGCPauseMillis=ms
5)Full GC
- SerialGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- ParallelGC
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足发生的垃圾收集 - full gc
- CMS
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足,并发收集失败才会full gc
- G1
- 新生代内存不足发生的垃圾收集 - minor gc
- 老年代内存不足,老年代占用内存达到阈值,触发并发标记和混合收集。垃圾产生速度大于回收速度->full gc
6)Young Collection跨代引用
将老年代分为card,被新生代引用的设为dirty card
7)Remark
5.垃圾回收调优
- 掌握GC相关的VM参数,会基本的空间调整
- 掌握相关工具
5.1调优领域
5.2确定目标
- 低延迟还是高吞吐量,选择合适的回收器
- CMS,G1,ZGG
- ParallelGC
5.3最快的GC是不发生GC
5.4新生代调优
- 新生代的特点
- 所有的new操作的内存分配非常廉价
- 死亡对象的回收代价是0
- 大部分对象用过即死
- MInor GC的时间远远低于Full GC
新生代内存并非越大越好,如果过大则老年代会相应减少,这时如果内存不足会触发full gc
5.5老年代调优
以CMS为例
- CMS的老年代内存越大越好
- 先尝试不做调优,如果没有Full GC那么已经…,否则先尝试调优新生代
- 观察发生Full GC时老年代内存占用,将老年代内存预设调大1/4~1/3
类加载与字节码技术
内存模型
1.java内存模型
JMM定义了一套在多线程读写共享数据时(成员变量、数组),对数据的可见性、有序性和原子性的规则和保障。
1.1原子性
i++;
i–;
1.2问题分析
以上命令在java中并不是原子操作。
1.3解决方法
synchronize
2.可见性
2.1退不出的循环
变量发生变化,但是程序却仍从高速缓存中读取数据,使得循环无法退出。
2.2解决方法
volatile(易变关键字):可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。
2.3可见性
保证在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况。
3.有序性
volatile修饰的变量,可以禁用指令重排。
4.CAS与原子类
4.1CAS
底层:CAS底层依赖于一个Unsafe类来直接调用操作系统底层的CAS指令。
4.2乐观锁与悲观锁
- CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量。
- synchronized是基于悲观锁的思想:最悲观的估计,得防着其他线程来修改共享变量。
5.synchronized优化
5.1轻量锁
轻量级锁与偏向锁不同的是:
- 轻量锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁
- 每次进入退出同步块都需要CAS更新对象头
- 争夺轻量级锁失败时,自旋尝试抢占锁
加锁过程
- 线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为displaced mark word。然后线程尝试使用CAS将对象头的mark word替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁,则轻量级锁会膨胀成重量级锁。
5.2锁膨胀
- 当竞争线程尝试占用轻量级锁失败多次之后,轻量级锁就会膨胀为重量级锁,重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒它。
5.3重量锁
重量锁在JVM中又叫监视器(Monitor),它很像C中的Mutex,出了具备其互斥的功能,还实现了信号量的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列,前者负责做互斥,后者用于线程同步。
5.4偏向锁