JVM 学习总结
PC寄存器
- 每个线程拥有一个PC寄存器
- 在线程创建时被创建
- 指定下一条指令的地址
- 执行本地方法时,PC的值是undefined
方法区
- 保存装载的类信息
- 类型的常量池
- 字段,方法信息
- 方法字节码
【注意】:
JDK6:String等常量信息存在于方法区
JDK7:String等常量信息存在于堆内存
- 通常和永久区(Perm)关联在一起(永久区通常保存一些相对稳定相对静止的信息;通常是由Java虚拟机维护)
Java堆内存
- 和程序开发密切相关
- 应用系统对象都保存在Java堆中(new Object)
- 所有线程共享Java堆
- 对分代GC来说,堆也是分代的
- GC的主要工作区间
【注意】:堆内存每次分配后需要手动清理空间
Java栈内存(数据结构:先进后出)
- 线程私有
- 栈由一系列帧组成(因此Java栈也叫做帧栈)
- 帧保存一个方法的局部变量,操作数栈,常量池指针
- 每一次方法调用创建一个帧,并压栈
【注意】:栈内存每次分配后无需手动清理空间,函数调用完成自动清理
Java栈 - 局部变量表 【包含参数和局部变量】
Java栈 - 栈上分配
- 一般小对象(几十个bytes),在没有逃逸(线程共用的对象)的情况下,可以直接分配在栈上
- 一般一个栈的空间大概是几百KB到1M左右(尽量分配给小对象)
- 直接分配在栈上,可以自动回收,减轻GC压力
- 大对象或者逃逸对象无法栈上分配
堆,栈,方法区交互
- 局部变量实例化后是存入堆中,栈中会存放该对象的引用
- 类本身的信息,方法的字节码是在方法区当中存放
内存模型
- 每一个线程有一个工作内存和主存独立
- 工作内存存放主存中变量的值的拷贝
当数据从主内存复制到工作内存时,必须出现两个动作
第一:由主内存执行的读(read)操作
第二:由工作内存执行的相应的load操作
当数据从工作内存拷贝到主内存时,也出现两个操作
第一:由工作内存执行的存储(store)操作
第二:由主内存执行的相应的写(write)操作
每一个操作都是原子的,即执行期间不会被中断
对于普通变量,一个线程中更新的值,不能麻烦反应在其他变量中
如果需要在其他线程中立即可见,需要使用volatile关键字
可见性:
一个线程修改了变量,其他线程可以立即知道
保证可见性的方法
- 使用volatile关键字
- synchronized(unlock之前,写变量值回主存)
- final(一旦初始化完成,其他线程就可见)
有序性:
- 在本线程内,操作都是有序的
- 在多线程的情况下,操作都是无序的(指令重排或主内存同步延时)
指令重排:
- 线程内串行语义
- 写后读 a=1;b=a; 写一个变量之后,再读这个位置。
- 写后写 a=1;a=2; 写一个变量之后,再写这个变量。
- 读后写 a=b;b=1; 读一个变量之后,再写这个变量。
- 以上语句【不可】重排
- 编译器不考虑多线程间的语义
- 可重排:a=1;b=2;
指令重排的基本原则
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile先写,后读
- 锁规则:解锁(unlock)必然发生在随后的加锁(lock)之前
- 传递性:指令A先于指令B,指令B先于指令C,那么指令A必然先于指令C
- 线程的start方法限于它的每一个动作
- 线程的所有操作先于线程的中介(Thread.join())
- 线程的中断(interrupt())先于被中断的线程的代码
- 对象的构造函数执行结束先于finalize()方法
解释运行
- 解释执行以解释方式运行字节码
- 解释执行的意思:读一句执行一句
编译执行(JIT: Just In Time)
- 将字节码编译成机器码
- 直接执行机器码
- 运行时编译
- 编译后性能有数量级的提升
【注意】:解释运行和编译运行之间的性能差10倍左右
JVM常用参数配置
- Trace跟踪参数
1. -verbose:gc
2. -XX:+PrintGC(发生GC时则会打印相关的GC信息)
示例:- [GC 4790k->374k(15872k), 0.0001606 secs]
解释:GC回收前是占用4790k的空间,回收之后只占用374k,总大小为15872k
3. -XX:+PrintGCDetails(打印GC详细信息)
示例1:- [GC[DefNew: 4416k->0k(4928k),0.0001897 secs] 4790k->374k(15872k),0.0002232 secs]
[Times: user=0.00 sys=0.00 real=0.00 secs]
示例2:-XX:+PrintGCDetails在程序执行结束后打印的堆的信息
- Heap
- def new generation total 13824k used 11223k [0x27e80000,0x28d80000,0x28d80000]
e.x: 新生代共有13M可用,已用11M [低边界,当前边界(所使用,所分配到的位置),最大边界]
计算:(0x28d80000-0x27e80000)/1024/1024=15M
也就是说新生代被分配了15M
正好就是EdenSpace(12288)+FromSpace(1536)+ToSpace(1536)=15M
EdenSpace(12288)+FromSpace(1536)=NewGeneration(13824)
- eden space 12288k 91% used [0x27e8000,0x28975f20,0x28a80000]
e.x: 伊甸园公有12M可用,已用91%
- from space 1536k 0% used [0x28a80000,0x28a80000,0x28c00000]
e.x: 幸存代
- to space 1536k 0% used [0x28c00000,0x28c00000,0x28d80000]
e.x: 幸存代
- tenured generation total 5120k used 0k [0x28d80000,0x29280000,0x34680000]
e.x: 老年代共有5M可用,已用0k
- the space 5120k 0% used [0x28d80000,0x28d80000,0x28d80200,0x29280000]
- compacting perm gen total 12288k used 142k [0x34680000,0x35280000,0x38680000]
e.x: 永久区共有12M可用,已用142k
- the space 12288k 1% used [0x34680000,0x346a3a90,0x346a3c00,0x35280000]
- ro space 10240k 44% used [0x38680000,0x38af73f0,0x38af7400,0x39080000]
e.x: 只读共享区
- rw space 12288k 52% used [0x3980000,0x396cdd28,0x396cde00,0x39c80000]
e.x: 可读可写区
4. -XX:+PrintGCTimeStamps(打印GC发生的时间戳)
5. -Xloggc:log/gc.log(重定向GC信息到日志文件)
6. -XX:+PrintHeapAtGC(每一次GC前后,都打印【堆】信息)
7. -XX:+TraceClassLoading(监控系统中每个类的加载)
8. -XX:+PrintClassHistogram(按下Ctrl+Break后,打印类的信息)
示例:
num #instances #bytes class name
序号 实例数量 总大小 类型
- 堆的分配参数
1. -Xmx -Xms
指定最大堆内存和最小堆内存
示例:-Xmx20m -Xms5m
使用:Xmx = Runtime.getRuntime().maxMemory()/1024/1024
FreeMemory = Runtime.getRuntime().freeMemory()/1024/1024
TotalMemory = Runtime.getRuntime().totalMemory()/1024/1024
结果:
堆内存初始值
Xmx=18.0M
FreeMemory=6.200019836425781M
TotalMemory=7.0M
分配了1M空间给byte数组
Xmx=18.0M
FreeMemory=5.200004577636719M
TotalMemory=7.0M
分配了4M空间给数组
Xmx=18.0M
FreeMemory=5.699989318847656M
TotalMemory=11.5M
回收内存
Xmx=18.0M
FreeMemory=6.994834899902344M
TotalMemory=11.5M
观察Xmx、FreeMemory、TotalMemory变化情况
2. -Xmn
设置新生代大小(EdenSpace+FromSpace+ToSpace的总和)
3. -XX:NewRatio
新生代(eden + 2*s)和老年代(不包含永久区)的比值
4表示 新生代:老年代=1:4
即:年轻代占堆内存的1/5
4. -XX:SurvivorRatio
设置两个Survivor区和Eden的占比
8表示 两个Servivor:Eden = 2:8
即:一个Servivor占年轻代的1/10
【注意】:理论情况下,发生GC的次数越多,对系统性能的损耗越大
Eden区调大一点,幸存代调小一点,有利于减少GC次数
5. -XX:+HeapDumpOnOutOfMemoryError
OOM(Out Of Memory)时导出堆到文件
6. -XX:HeapDumpPath
导出OOm的路径
示例:-Xmx20M -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump
【总结】:
- 根据实际情况调整新生代和幸存代的大小
- 官方推荐新生代占堆内存的3/8
- 幸存代占新生代的1/10
- 在OOM时,记得Dump出堆信息,确保可以排查现场问题
7. -XX:PermSize -XX:MaxPermSize
设置永久区的初始空间和最大空间
他们表示一个系统可以容纳多少个类型(一般系统几十兆或者几百兆就够用了)
- 栈的分配参数
-Xss
- 通常只有几百KB
- 决定了函数调用的深度
- 每个线程都有独立的栈空间
- 局部变量、参数分配在栈上
【注意】:若想让系统多跑一些线程,应该把栈空间尽量减少,而不是增大
若系统中有“递归”调用,栈空间不宜太小,若太小,可能会造成OOM
若出现java.lang.StackOverflowError,说明函数调用的深度太深,需要调把栈空间调大一点
若想让函数尽可能被多调用的方式:减少局部变量(栈帧里面的局部变量表包含参数和函数当中的局部变量)
若能减少局部变量的数量,就能够减少每次函数调用所消耗的空间,这样能够让函数多被调用几次
【GC算法】
标记-清除算法
标记-清除算法是现代垃圾回收算法的思想基础。
标记-清除算法是将垃圾回收分为两个阶段
1.标记阶段
2.清除节点
一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。
因此未被标记的对象就是未被引用的垃圾对象。然后,在清除节点,清除所有未被标记的对象。
标记-压缩算法
标记-压缩算法适合用于存活对象较多的场合,如老年代。
它在标记-清除算法的基础上做了一些优化。
和标记-清除算法一样,标记-压缩算法首先需要从根节点开始,对所有可达对象做一次标记
之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端。
最后清理边界外所有的空间。
【注意】:标记-清除算法执行后,内存可能会产生一些碎片
标记-压缩算法执行后,内存空间是连续的,没有碎片
复制算法(对内存空间有一定的浪费)
与标记-清除算法相比,复制算法是一种相对高效的回收方法
不适用于存活对象较多的场合 如老年代
将原有的内存空间分为两块(两块大小完全相同),每次只使用其中一跨,在垃圾回收时
将正在使用的内存中的存活对象复制到未使用的内存块中
之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收
分代思想
依据对象的从怒火周期进行分类,短命对象归为新生代,长命对象归为老年代
根据不通代的特点,选取合适的收集算法
- 少量对象存活,适合复制算法
- 大量对象存活,适合标记-清理或者标记-压缩算法
GC算法总结
引用计数(没有被Java采用)【缺点】:没有办法处理循环引用
标记-清除 老年代使用
标记-压缩 老年代使用
复制算法 新生代使用
对象产生在Eden区,使用标记-清除算法
From区和To区空间大小一样,使用复制算法
垃圾对象
可触及性
- 从根节点可以触及到这个对象
什么是根节点
- 节点可以认为是栈中引用的对象
- 方法区中静态成员或者常量引用的对象(全局对象)
- JNI方法栈中引用的对象
可复活的
- 一旦所有引用被释放,就是可复活状态
- 因为在finalize()中可能复活该对象
不可触及的
- 在finalize()后,可能会进入不可触及状态
- 不可触及的对象不可能复活
- 可以回收
串行回收器
- 最古老,最稳定
- 效率高
- 可能会产生较长的停顿
- 在多核计算机中可能发挥不了最大的作用(原因:单线程)
- -XX:+UseSerialGC
- 新生代,老年代使用串行回收器
- 新生代复制算法
- 老年代标记-压缩
并行收集器ParNew(同样会产生Stop-The-World,即:停止用户应用程序的线程)
- -XX:+UserParNewGC(New:代表新生代)
- 新生代并行
- 老年代串行
- Serial收集器新生代的并行版本
- 复制算法
- 多线程,需要多核支持
- -XX:ParallelGCThreads 限制线程数量
并行收集器Parallel(同样会产生Stop-The-World,即:停止用户应用程序的线程)
- 类似ParNew
- 新生代复制算法
- 老年代标记-压缩
- 更加关注吞吐量
- -XX:+UseParallelGC
使用Parallel收集器(新生代并行+老年代串行)
-XX:+UserParallelOldGC
使用Parallel收集器(新生代并行+老年代并行)
-XX:MaxGCPauseMills
- 最大停顿时间,单位毫秒
- GC尽力保证回收时间不超过设定值
-XX:GCTimeRatio
- 0-100的取值范围
- 垃圾收集时间占总时间的比值
- 默认99,即:最大允许1%时间做GC
【注意】:以上两个参数是矛盾的。因为停顿时间和吞吐量不可能同时调优
CMS收集器(和应用程序的线程一起执行)
- Concurrent Mark Sweep 并发标记-清除
- 标记-清除算法
- 与标记-压缩算法相比
- 并发节点会降低吞吐量
- 单纯老年代收集器(新生代使用ParNew)
- -XX:+UseConcMarkSweepGC
CMS运行过程比较复杂,着重实现了标记的过程,可分为
- 初始标记(会产生全局停顿)
1.根可以直接关联到的对象
2.速度快
- 并发标记(和用户应用程序一起)
主要标记过程,标记全部对象
- 重新标记(会产生全局停顿)
1.由于并发标记时,用户线程依然运行,因此在正式清理前,再做修正
- 并发清除(和用户应用程序一起)
1.采用标记-清除算法
原因:因为CMS在执行时,是和应用程序并发执行,
而因为标记-压缩算法是需要将存活的对象移动到堆内存的一端
此时,应用程序有可能找不到存活的对象
而标记-清除算法只是清除不可达的对象,没有对他们进行移动
2.基于标记结果,直接清理对象
CMS的特点
- 尽可能降低停顿
- 会影响系统整体吞吐量和性能
比如:用户线程运行过程中,分一半CPU去做GC,系统性能在GC阶段,反应速度就下降一半
- 清理不彻底
因为在清理节点,用户线程还在运行,会产生新的垃圾,无法清理
- 因为和用户线程一起运行,不能在空间快满时再清理
- -XX:CMSInitiatingOccupancyFraction设置出发GC的阈值
- 如果不幸内存预留空间不足,就会引起Concurrent mode failure这个错误!!!
【经验】:若遇到Concurrent mode failure错误,使用串行收集器作为后备
因此,应用程序可能会产生一段时间的停顿
- -XX:+UseCMSCompactAtFullCollection Full GC之后,进行一次整理
整理过程是独占的(不与其他线程并发),会引起停顿时间变长
- -XX:+CMSFullGCsBeforeCompaction
设置进行几次Full GC之后,进行一次碎片整理
- -XX:ParallelCMSThreads
设置CMS的线程数量(一般情况下,约等于CPU的可用线程数量,不宜设置的太大)
【注意】:为了减轻GC压力,我们需要注意些什么?
1.软件如何设计架构
2.代码如何写
3.堆空间如何分配
总结GC参数
1. -XX:+UseSerialGC 在新生代和老年代使用串行收集器
2. -XX:SurvivorRatio 设置Eden区和Survivor区大小的比例
3. -XX:NewRatio 新生代和老年代的比例
4. -XX:+UseParNewGC 在新生代使用并行收集器
5. -XX:+UseParallelGC 新生代使用并行回收收集器
6. -XX:+UseParallelOldGC 老年代使用并行回收收集器
7. -XX:ParallelGCThreads=4 设置用于垃圾回收的线程数
8. -XX:+UseConcMarkSweepGC 新生代使用并行收集器,老年代使用CMS+串行收集器
9. -XX:ParallelCMSThreads 设置CMS的线程数量
10. -XX:CMSInitiatingOccupancyFraction=50 设置CMS收集器在老年代空间被使用多少后触发
11. -XX:+UseCMSCompactAtFullCollection 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的处理
12. -XX:CMSFullGCsBeforeCompaction=2 设置进行多少次CMS垃圾回收后,进行一次内存压缩
13. -XX:+CMSClassUnloadingEnabled 允许对类元数据进行回收
14. -XX:CMSInitiatingPermOccupancyFraction 当永久区占用率达到此值设置的百分比时,启动CMS回收
15. -XX:UseCMSInitiationOccupancyOnly 表示只在到达阈值的时候,才进行CMS回收
16. -XX:+DisableExplicitGC 表示禁用代码中System.gc()
17. -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。
如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。
对于年老代比较多的应用,可以提高效率。
如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,
这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
18. -XX:+PrintTenuringDistribution 打印新生代年龄分布
19. -XX:-ReduceInitialCardMarks 解决放入大对象导致JVM Crash
20. -XX:+CMSParallelRemarkEnabled 并行标记
21. -XX:+UseCompressedOops JVM压缩普通对象指针
jvisualVM监控配置参数:
-Dcom.sun.management.jmxremote=true (开启jvisualVM监控)
-Djava.rmi.server.hostname=xxx.xxx.xxx.xxx (主机名)
-Dcom.sun.management.jmxremote.port=xxxxx (端口)
-Dcom.sun.management.jmxremote.ssl=false (是否使用ssl)
-Dcom.sun.management.jmxremote.authenticate=false (是否需要用户名密码进行验证)
并且在jdk中需要修改java.policy, 在最后一行追加permission java.security.AllPermission;
并启动jdk中的jstatd这个服务。就可以使用jVisualVM监控JVM的各项参数变化了。
欢迎访问我的个人Github