自动内存管理机制
内存分配 & 垃圾回收
运行时数据区域
程序计数器
记录正在运行的虚拟机字节码指令的地址,当前线程所执行的字节码的行号指示器。本地方法由于调用的是操作系统的方法,无虚拟机字节码指令,故程序计数器为
null
。- 程序计数器无
OutOfMemoryError
- 程序计数器无
Java虚拟机栈
- 栈帧:Java虚拟机栈的栈元素是栈帧。当Java方法被调用时,代表这个方法的栈帧入栈;当Java方法返回时,栈帧出栈
- 局部变量表
- 操作数栈
- 动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接 - 方法出口
- …
- 局部变量表:存储方法的局部变量以及方法参数,不能直接使用,必须通过相关指令将其压入操作数栈中再使用。局部变量表的空间单位是
slot
,对于 32 位之内的数据,用一个 slot 来存放,如 int,short,float 等;对于 64 位的数据用连续的两个 slot 来存放,如 long,double 等- 基本数据类型
- 对象引用
- 方法参数
- 操作数栈:Java 虚拟机指令由操作码和操作数组成。操作码是代表某种特定操作含义的数字,操作数是操作码的参数。Java 方法的操作码存储在操作码栈中,操作数栈可理解为 Java 虚拟机栈中的一个用于计算的临时数据存储区
- StackOverflowError:若单个线程请求的栈深度大于虚拟机允许的深度,则会抛出StackOverflowError
- OutOfMemoryError:当整个虚拟机栈内存耗尽,并且无法再申请到新的内存时抛出的异常
- 内存溢出:系统已经不能再分配你所需要的空间,比如你需要100M的空间,系统只剩90M了,这就叫内存溢出
- 内存泄露:意思就是你用资源的时候为他开辟了一段空间,当你用完时忘记释放资源了,这时内存还被占用着,一次没关系,但是内存泄漏次数多了就会导致内存溢出
- 栈帧:Java虚拟机栈的栈元素是栈帧。当Java方法被调用时,代表这个方法的栈帧入栈;当Java方法返回时,栈帧出栈
本地方法栈
Java 可以通过 JNI 来调用其他语言的程序,在Java里用
native
修饰符表示本地方法。本地方法如果是以字节码实现的话,可以将 Java 虚拟机栈和本地方法栈合并,hotspot
虚拟机把虚拟机栈和本地方法栈合二为一。本地方法可以通过本地方法接口来访问虚拟机的运行时数据区堆(GC堆)
对象实例
数组:数组也是对象
方法区
- 已被加载的类信息
- 常量
- 静态变量
- 即时编译器编译的代码
- …
方法信息中包含方法字节码
运行时常量池
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域
垃圾回收机制
堆对象回收
引用计数算法
给对象添加引用计数器,当有一个地方引用时,计数器+1;当引用失效时,计数器-1;计数器为0的对象可以被回收优点:简单高效
缺点:循环引用
1
2
3
4
5
6
7
8
9
10
11public static void main(String[] args) {
MyObject A = new MyObject();
MyObject B = new MyObject();
A.instance = B;
B.instance = A;
A = null;
B = null;
// 使用引用计数算法不会被回收
System.gc();
}
可达性分析算法
以一系列称为
GC Root
的对象为起始点向下搜索,搜索所走过的路径称为“引用链”,没有任何引用链相连的对象可以被回收可作为
GC Root
的对象:- Java 虚拟机栈的局部变量表中引用的对象
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中类常量引用的对象
引用(引用强度依次递减)
- 强引用
存在强引用,垃圾回收器不会回收 - 软引用
有用但并非必要的对象,当系统即将内存溢出时,将会把这些对象列入回收范围进行二次回收 - 弱引用
存活到下次垃圾回收之前 - 虚引用
为一个对象设置虚引用的目的是在这个对象被回收时会收到系统通知,虚引用不会对对象生命周期构成影响
方法区回收
常量回收
和堆中对象类似,没有被引用可被回收
类的卸载条件很多,需要满足以下三个条件,并且满足了条件也不一定会被卸载:
- 该类所有的实例都已经被回收,此时堆中不存在该类的任何实例
- 加载该类的 ClassLoader 已经被回收
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法
在 Hotspot 虚拟机中使用永久代实现方法区,由于方法区的回收率特别低。在 jdk1.7 后将常量池和静态变量存储到 Java 堆中,jdk1.8 后完全移除方法区,并将类信息存储于元空间中,元空间在本地内存中
垃圾收集算法(方法论)
新生代:复制
老年代:标记-清除、标记-整理
垃圾收集器(具体实现)
串行:垃圾回收和程序运行代码串行
并行:垃圾回收和程序代码并发执行
Serial 收集器
复制算法;串行、单线程;适用场景:运行在client 模式下的虚拟机
ParNew 收集器
复制算法;串行、多线程;适用场景:多核CPU
Parallel Scavenge 收集器
复制算法;串行、吞吐量优先、自适应调节策略;适用场景:注重高吞吐量
Serial Old 收集器
串行,标记-整理算法Parallel Old 收集器
c串行,标记-整理算法CMS 收集器
- 初始标记
- 并发标记
- 重新标记
- 并发清除
标记-清除算法;并行,以获取最短回收停顿时间为目标;适用场景:注重响应速度
G1 收集器:并行
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
垃圾收集器使用场景
- Serial:适用于 Client 模式
- ParNew:适用于 Server 模式,与 CMS 搭配
- Parallel Scavenge:适用于后台计算,不需要太多交互的批处理系统
- Serial Old:适用于 Client 模式
- Parallel Old:适用于 Server 模式
- CMS:以最短垃圾回收时间为目的,适用于响应速度要求高的交互式系统
- G1:适用于服务端,在多 CPU 和大内存的场景下有很好的性能
内存分配
minor GC
&full GC
- minor GC :回收新生代
- major GC:回收老年代
full GC:回收老年代,伴随着新生代的回收
内存分配
- 对象优先分配在 Eden 区
- 大对象直接进入老年代
- 长期存活对象进入老年代:为对象设置年龄
- 动态对象年龄判断
- 空间分配担保
对象分配位置
- 堆
- Java 虚拟机栈
- 逃逸分析:判断对象的作用域是否有可能逃逸出函数体
- 标量替换:
- Thread Local Allocation Buffer (TLAB)
对象的访问定位
- 句柄
- 直接指针:
hotspot
使用直接指针
class 文件结构
类加载机制
类初始化情况
- 遇到
new
getstatic
putstatic
invokestatic
这四个指令时,如果未初始化会触发初始化- 编译期常量不依赖类,不会触发类的初始化;运行期常量依赖类,会触发类的初始化
- 使用 Java 反射机制对类反射调用时,如果未初始化会触发初始化
- 当初始化一个类时,父类未初始化会触发初始化
- 虚拟机启动需要用户指定一个要执行的主类,会触发主类初始化
- jdk 1.7 ,使用
java.lang.invoke.MethodHandle
实例解析方法句柄所对应的类未初始化会触发初始化
类加载过程
加载:通过类加载器 ClassLoader 进行加载
- 通过类的完全限定名称获取定义该类的二进制字节流
- 将该字节流表示的静态存储结构转换为方法区的运行时存储结构
- 在内存中生成一个代表该类的 Class 对象,作为方法区中该类各种数据的访问入口
加载阶段完成后二进制字节流就按照虚拟机所需的格式存储在方区去中
验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备:为类变量分配内存并设计系统初始值
解析
初始化
类加载器
四种类加载器
Bootstrap
类加载器加载核心库,
rt.jar
中的JDK类文件Extension
类加载器
加载扩展库,从jre/lib/ext
目录下或者java.ext.dirs
系统属性定义的目录下加载类System
类加载器从
classpath
环境变量中加载某些应用相关的类Custom 类加载器
自定义的类加载器
三个机制:
- 委托机制
System
->Extension
->Bootstrap
,保证 java 核心库的安全性 - 可见性
子类加载器可以看到父类加载器加载的类,而反之则不行 - 单一性
父加载器加载过的类不能被子加载器加载第二次
自定义类加载器
自定义类加载器的核心在于对字节码文件的获取。最好不要重写loadClass方法,因为这样容易破坏双亲委托模式
1 | public class FileSystemClassLoader extends ClassLoader { |
java 内存模型与线程
硬件并发
java 内存模型
java 内存模型试图屏蔽硬件和操作系统的内存访问差异,以实现 java 在各个平台都有相同的内存访问效果。java 内存模型的主要目标是定义程序中变量的访问规则,这里的变量指的是堆和方法区中的变量,而不包括 java 虚拟机栈和本地方法栈中的局部变量。
内存间交互操作
主内存和工作内存具体的交互协议,Java 内存模型定义了8个基本操作:
- lock:作用于主内存,把变量标记为一个线程独占
- unlock:作用于主内存,把处于锁定状态的变量释放
volatile
Java 虚拟机提供的轻量级同步机制,被定义为 volatile
的变量具有两个特性
可见性:保证此变量对所有线程可见,当其中一个线程修改该变量时,其他线程能够立刻得知;可见性不能保证并发安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyVolatile {
private static volatile int val = 0;
public static void incr() {
val++; // 不具备原子性
}
public static int getVal() {
return val;
}
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
CountDownLatch countDownLatch = new CountDownLatch(20);
for(int i = 0; i < 20; i++) {
executorService.execute(()->{
for(int j = 0; j < 1000; j++) {
incr();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(getVal());
}
}volatile
具有可见性,但无法保证代码执行的原子性。此例中,自增操作由4个字节码指令构成:getstatic
、iconst_1
、iadd
、putstatic
。volatile
可以保证getstatic
时是正确的,但是无法保证入栈操作和累加操作时,val
值是最新的。禁止指令重排序优化
内存模型三大特性
- 原子性
- 可见性
- 有序性
先行发生原则
线程安全与锁优化
当多个线程同时访问共享对象时,取得这个对象的结果始终是正确的
Java 线程安全
在 Java 语言中,操作共享数据可以分为以下5类:
不可变
不可变的对象一定是线程安全的,final
、String
绝对线程安全
不管调用环境如何,调用者都不需要额外的同步措施相对线程安全
对象的单独操作是线程安全的
线程兼容
对象本身不是线程安全的,需要调用者使用正确的同步手段来保证线程安全线程对立
无论调用者是否采取同步手段,都无法保证线程安全
线程安全的实现方法
锁类型
- 可重入锁:当线程执行某方法获得一个锁后,再次尝试获得该锁不会被阻塞
- 不可重入锁:当线程执行某方法获得一个锁后,再次尝试获得该锁会被阻塞
- 可中断锁:当线程在等待锁的过程中,可以中断转而去执行其他的任务
- 公平锁:多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
方法:
- 互斥同步
- synchronized:可重入锁,非公平锁,需要指定一个引用参数作为锁对象。若不指定参数,实例方法的参数是
this
,静态方法的参数是Class
- ReentrantLock:可重入锁,默认非公平锁。高级特性:等待可中断、公平锁、绑定多个条件
- synchronized:可重入锁,非公平锁,需要指定一个引用参数作为锁对象。若不指定参数,实例方法的参数是
- 非阻塞同步
- CAS:处理器指令,是原子操作。需要3个操作数:内存地址、旧的预期值、新值。存在ABA问题,通过版本号解决ABA问题
- 无同步方案
- 线程私有变量无需同步
- 线程本地存储
锁优化
自旋锁:为了让线程等待,只需让线程执行一个忙循环(自旋)
锁消除:虚拟机即时编译器在运行时,对一些在代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除
锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。锁粗化通过扩大锁作用域的方式将多个锁转化成一个锁轻量级锁
在无竞争的情况下,使用CAS操作去消除同步使用的互斥量偏向锁
在无竞争的情况下,去掉同步操作
偏向锁 / 轻量级锁 / 重量级锁
锁的状态:
- 无锁状态
- 偏向锁状态
- 轻量级锁状态
- 重量级锁状态
锁的状态是通过对象监视器在对象头中的字段来表明的。四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级。这四种状态都不是Java语言中的锁,而是Jvm为了提高锁的获取与释放效率而做的优化(使用synchronized时)
偏向锁偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价
轻量级轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能
重量级锁重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低
jvm 参数设置
-Xms10m
:jvm 堆初始化内存-Xmx10m
:jvm 堆最大内存-Xmn10m
:jvm 新生代初始化内存-XX:NewSize
:jvm 新生代初始化内存-XX:MaxNewSize
:jvm 新生代最大内存-XX:PermSize=16M
:永久代初始化内存-XX:MaxPermSize=64M
:永久代最大内存-XX:Xss=128K
:栈内存大小-XX:SurvivorRatio
:设置Eden和其中一个Survivor的比值-XX:InitialTenuringThreshol
:晋升到老年代年龄的最小值-XX:MaxTenuringThreshold
:晋升到老年代年龄的最大值
jvm 调优
jvm 调优并不是一成不变的,而是根据不同的系统来进行分析。可以在运行系统前进行初始化调优,也可以根据运行情况来监测系统从而发现问题解决问题
jvm 监测
可以从 CPU 、内存和磁盘等角度对程序进行分析
- CPU
top
:找到占用 CPU 高的进程top -p pid -H
:找到占用 CPU 高的线程jstack -l pid >temp.log
:打印堆栈日志
- jvm 内存
jmap -heap pid
:查看堆的分布情况jstat -gcuitl pid 2000 10
:每隔 2s 输出 GC 次数,输出 10 次
高性能硬件程序部署策略
对于 64 位大内存的操作系统来说,如果想充分利用硬件资源,可以有两种部署方式:
- 通过 64 位 JDK 使用大内存:如果堆内存分配太大,需要考虑
Full GC
的频率 - 使用若干 32 位虚拟机部署虚拟集群
堆外内存溢出
- 栈溢出:使用
-Xss
调整大小 - 直接内存溢出:使用
-XX:MaxDirectMemorySize
调整大小
远程调用
对于服务处理时间不均等,一快一慢的远程调用,使用异步的方式进行处理,比如消息队列
内存调优
- 堆大小的调优:一般来说堆越大越好。可以降低 GC 的频率,但会提高单次 GC 的时间。可以增加堆内存并设置定时任务,在深夜触发 GC
- 新生代调优:增加新生代的大小会减少 Minor GC 的频率,但这并不意味着会增加单次 Minor GC 的时间。新生代调优的目的是尽可能的待在新生代,减少晋升老年代的对象。但长期存活的对象在新生代频繁复制也会造成不必要的开销,所以需要权衡新生代晋升老年代的年龄的阀值
- 老年代调优:尽可能的调优新生代;对 CMS 垃圾回收器手动进行 Full GC
dump 文件
获取进程 pid:ps -aux | grep java
获取线程ID:top -p pid -H
获取 dump
jmap
:Java 自带工具,jmap -heap pid
kill
:Linux 命令,kill -3 pid
jstack
:sun JDK 工具,jstack pid