目录
一. 什么是字节码?
编译器将源码编译为字节码,在虚拟机里运行
由单字节(byte)的指令组成,理论上最多支持 256 个操作码(opcode)。
实际上 Java 只使用了200左右的操作码, 还有一些操作码则保留给调试操作
根据指令的性质,主要分为四个大类:
1. 栈操作指令,包括与局部变量交互的指令
2. 程序流程控制指令
3. 对象操作指令,包括方法调用指令
4. 算术运算以及类型转换指令
JVM 是一台基于栈的计算机器。
每个线程都有一个独属于自己的线程栈(JVM Stack),用于存储
栈帧(Frame)。
每一次方法调用,JVM 都会自动创建一个栈帧。
栈帧由操作数栈, 局部变量数组以及一个 Class 引用组成。
Class 引用 指向当前方法在运行时常量池中对应的 Class
二. JVM 类加载器
(1). 类的生命周期
1. 加载(Loading):找 Class 文件
2. 验证(Verification):验证格式、依赖
3. 准备(Preparation):静态字段、方法表
4. 解析(Resolution):符号解析为引用
5. 初始化(Initialization):构造器、静态变量赋值、静态代码块
6. 使用(Using)
7. 卸载(Unloading)
(2). 类的加载时机
1. 当虚拟机启动时,初始化用户指定的主类,就是启动执行的 main 方法所在的类;
2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类,就是 new一个类的时候要初始化;
3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
5. 子类的初始化会触发父类的初始化;
6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
7. 使用反射 API 对某个类进行反射调用时,初始化这个类,其实跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化;
8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
(3). 不会初始化(可能会加载)
1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
2. 定义对象数组,不会触发该类的初始化。
3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
4. 通过类名获取 Class 对象,不会触发类的初始化,Hello.class 不会让 Hello 类初始化。
5. 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。Class.forName(“jvm.Hello”)默认会加载 Hello 类。
6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作(加载了,但是不初始化)
(4). 三类加载器
1. 启动类加载器(BootstrapClassLoader)
2. 扩展类加载器(ExtClassLoader)
3. 应用类加载器(AppClassLoader)
加载器特点:
1. 双亲委托
2. 负责依赖
3. 缓存加载
三. JVM 内存模型
整体结构
方法中使用的原生数据类型和对象引用地址在栈上存储;对象、对象成员与类定义、静态变量在堆上。
堆内存又称为“共享堆”,堆中的所有对象,可以被所有线程访问, 只要他们能拿到对象的引用地址。如果一个线程可以访问某个对象时,也就可以访问该对象的成员变量。如果两个线程同时调用某个对象的
同一方法,则它们都可以访问到这个对象的成员变量,但每个线程的局部变量副本是独立的。
(1).栈
每启动一个线程,JVM 就会在栈空间栈分配对应的 线程栈, 比如 1MB 的空间(-Xss1m)。
线程栈也叫做 Java 方法栈。 如果使用了JNI 方法,则会分配一个单独的本地方法栈(Native Stack)。
线程执行过程中,一般会有多个方法组成调用栈(Stack Trace), 比如 A 调用 B,B调用 C。。。每执行到一个方法,就会创建对应的 栈帧(Frame)。
栈帧是一个逻辑上的概念,具体的大小在一个方法编写完成后基本上就能确定。
比如返回值 需要有一个空间存放吧,每个局部变量都需要对应的地址空间,此外还有给指令使用的 操作数栈,以及 class 指针(标识这个栈帧对应的是哪个类的方法,指向非堆里面的 Class 对象)。
如图
(2). 堆
堆内存是所有线程共用的内存空间,JVM 将Heap 内存分为年轻代(Young generation)和老年代(Old generation, 也叫 Tenured)两部分。
年轻代还划分为 3 个内存池,新生代(Edenspace)和存活区(Survivor space), 在大部分GC 算法中有 2 个存活区(S0, S1),在我们可以观察到的任何时刻,S0 和 S1 总有一个是空的,但一般较小,也不浪费多少空间。Non-Heap 本质上还是 Heap,只是一般不归 GC管理,里面划分为 3 个内存池。Metaspace, 以前叫持久代(永久代, Permanentgeneration), Java8 换了个名字叫 Metaspace.CCS, Compressed Class Space, 存放 class 信息的,和 Metaspace 有交叉。Code Cache, 存放 JIT 编译器编译后的本地机器代码。
运行原理
程序运行时 , class 和元数据 存入 元数据区 , 执行时 如果是大对象 (默认是堆内存的 1/2000)直接进老年代, 年轻代 分为 eden区 s from 和 s to 区,
垃圾回收时 ,to区 是空的, Eden区和 from 区的 存活对象 都移入 to区 然后 清空 eden 和from 区 , 对象 年龄加1,当年龄 大于 15 时 就移入 老年代
四. jvm 重要参数
(1).GC相关
-Xmx, 指定最大堆内存。 如 -Xmx4g. 这只是限制了 Heap 部分的最大值为4g。这个内存不包括栈内存,也不包括堆外使用的内存。
-Xms, 指定堆内存空间的初始大小。 如 -Xms4g。 而且指定的内存大小,并不是操作系统实际分配的初始值,而是GC先规划好,用到才分配。 专用服务器上需要保持 –Xms 和 –Xmx 一致,否则应用刚启动可能就有好几个 FullGC。当两者配置不一致时,堆内存扩容可能会导致性能抖动。
-Xmn, 等价于 -XX:NewSize,使用 G1 垃圾收集器 不应该 设置该选项,在其他的某些业务场景下可以设置。官方建议设置为 -Xmx 的 1/2 ~ 1/4.
-XX:MaxPermSize=size, 这是 JDK1.7 之前使用的。Java8 默认允许的Meta空间无限大,此参数无效。
-XX:MaxMetaspaceSize=size, Java8 默认不限制 Meta 空间, 一般不允许设置该选项。
-XX:MaxDirectMemorySize=size,系统可以使用的最大堆外内存,这个参数跟 -Dsun.nio.MaxDirectMemorySize 效果相同。
-Xss, 设置每个线程栈的字节数,影响栈的深度。 例如 -Xss1m 指定线程栈为1MB,与-XX:ThreadStackSize=1m 等价
(2).启动参数
五. jdk命令行工具
工具 | 简介 |
jps/jinfo | 查看 java 进程 |
jstat | 查看 JVM 内部 gc 相关信息 |
jmap | 查看 heap 或类占用空间统计 |
jstack | 查看线程信息 |
jcmd | 执行 JVM 相关分析命令(整合命令) |
jrunscript/jjs | 执行 js 命令 |
jstat 命令
-class 类加载(Class loader)信息统计.
-compiler JIT 即时编译器相关的统计信息。
-gc GC 相关的堆内存信息. 用法: jstat -gc -h 10 -t 864 1s 20
-gccapacity 各个内存池分代空间的容量。
-gccause 看上次 GC, 本次 GC(如果正在 GC中)的原因, 其他输出和 -gcutil 选项一致。
-gcnew 年轻代的统计信息. (New = Young = Eden + S0 +S1)
-gcnewcapacity 年轻代空间大小统计.
-gcold 老年代和元数据区的行为统计。
-gcoldcapacity old 空间大小统计.
-gcmetacapacity meta 区大小统计.
-gcutil GC 相关区域的使用率(utilization)统计。
-printcompilation 打印 JVM 编译统计信息。
比如 jstat -gcutil pid 1000 1000
jmap 命令
-heap 打印堆内存(/内存池)的配置和使用信息。
-histo 看哪些类占用的空间最多, 直方图
-dump:format=b,file=xxxx.hprof Dump堆内存。
jstack 命令
-F 强制执行 thread dump. 可在 Java 进程卡死(hung 住)时使用, 此选项可能需要系统权限。
-m 混合模式(mixed mode),将 Java 帧和 native帧一起输出, 此选项可能需要系统权限。
-l 长列表模式. 将线程相关的 locks 信息一起输出,比如持有的锁,等待的锁。
jcmd命令
综合了前面的几个命令
六. 内置图形化工具
jconsole
jvisualvm
idea的工具 ----VisualGC
jmc
七.GC相关
(1). 算法
标记算法
遍历所有可达对象,标记不可达对象
清除算法
清除所有不可达对象
整理-压缩算法
一般 年轻代 都是 标记-复制算法 ,老年代是 标记-整理算法
因为 年轻代的对象 大部分都是临时对象 , 所以 为了 不产生碎片 , 就使用 复制算法
(2).收集器
1.串行gc(Serial GC)
-XX:+UseSerialGC 配置串行 GC
串行 GC 对年轻代使用 mark-copy(标记-复制) 算法,对老年代使用 mark-sweep-compact(标记-清除-整理)算法。
两者都是单线程的垃圾收集器,不能进行并行处理,所以都会触发全线暂停(STW),停止所有的应用线程。
因此这种 GC 算法不能充分利用多核 CPU。不管有多少 CPU 内核,JVM 在垃圾收集时都只能使用单个核心。
CPU 利用率高,暂停时间长。简单粗暴,就像老式的电脑,动不动就卡死。
该选项只适合几百 MB 堆内存的 JVM,而且是单核 CPU 时比较有用。
2. 并行gc(Parallel GC)
-XX:+UseParallelGC
-XX:+UseParallelOldGC
年轻代和老年代的垃圾回收都会触发 STW 事件。
在年轻代使用 标记-复制(mark-copy)算法,在老年代使用 标记-清除-整理(mark-sweep-compact)算法。
-XX:ParallelGCThreads=N 来指定 GC 线程数, 其默认值为 CPU 核心数。
并行垃圾收集器适用于多核服务器,主要目标是增加吞吐量。因为对系统资源的有效使用,能达到更高的吞吐量:
• 在 GC 期间,所有 CPU 内核都在并行清理垃圾,所以总暂停时间更短;
• 在两次 GC 周期的间隔期,没有 GC 线程在运行,不会消耗任何系统资源。
3.CMS GC(并发gc)
-XX:+UseConcMarkSweepGC
其对年轻代采用并行 STW 方式的 mark-copy (标记-复制)算法,对老年代主要使用并发mark-sweep (标记-清除)算法。
CMS GC 的设计目标是避免在老年代垃圾收集时出现长时间的卡顿,主要通过两种手段来达成此目标:
1. 不对老年代进行整理,而是使用空闲列表(free-lists)来管理内存空间的回收。
2. 在 mark-and-sweep (标记-清除) 阶段的大部分工作和应用线程一起并发执行。
也就是说,在这些阶段并没有明显的应用线程暂停。但值得注意的是,它仍然和应用线程争抢CPU 时间。默认情况下,CMS 使用的并发线程数等于 CPU 核心数的 1/4。
如果服务器是多核 CPU,并且主要调优目标是降低 GC 停顿导致的系统延迟,那么使用 CMS是个很明智的选择。进行老年代的并发回收时,可能会伴随着多次年轻代的 minor GC。
步骤:
阶段 1: Initial Mark(初始标记)
这个阶段伴随着 STW 暂停 。初始标记的目标是标记所有的根对象,包括根对象直接引用的对象,以及被年轻代中所有存活对象所引用的对象(老年代单独回收)
阶段 2: Concurrent Mark(并发标记)
在此阶段, CMS GC 遍历老年代,标记所有的存活对象,从前一阶段 “ “ Initial Mark” ” 找到的根对象开始算起。“ “ 并发标记” ” 阶段,就是与应用程序同时运行,不用暂停的阶段。
阶段 3: Concurrent Preclean(并发预清理)
此阶段同样是与应用线程并发执行的,不需要停止应用线程。 因为前一阶段【并发标记】与程序并发运行,可能有一些引用关系已经发生了改变。如果在并发标记过程中引用关系发生了变化, JVM 会通过“ “ Card (卡片)” ” 的方式将发生了改变的区域标记为“ “ 脏” ” 区,这就是所谓的 卡片标记
阶段 4: Final Remark(最终标记)
最终标记阶段是此次 GC 事件中的第二次(也是最后一次) STW 停顿 。本阶段的目标是完成老年代中所有存活
对象的标记. 因为之前的预清理阶段是并发执行的,有可能 GC 线程跟不上应用程序的修改速度。所以需要一次STW 暂停来处理各种复杂的情况。
通常 CMS 会尝试在年轻代尽可能空的情况下执行 FinalRemark 阶段,以免连续触发多次 STW 事件
阶段 5: Concurrent Sweep(并发清除)
阶段 6: Concurrent Reset(并发重置)
此阶段与应用程序并发执行,重置 CMS 算法相关的内部数据,为下一次 GC 准备
4 G1 gc
-XX:+UseG1GC -XX:MaxGCPauseMillis=50
G1 的全称是 Garbage-First,意为垃圾优先,哪一块的垃圾最多就优先清理它。
G1 GC 最主要的设计目标是:将 STW 停顿的时间和分布,变成可预期且可配置的。
事实上,G1 GC 是一款软实时垃圾收集器,可以为其设置某项特定的性能指标。为了达成可预期停顿时间的指标,G1 GC 有一些独特的实现。
首先,堆不再分成年轻代和老年代,而是划分为多个(通常是 2048 个)可以存放对象的 小块堆区域(smaller heap regions)。每个小块,可能一会被定义成 Eden 区,一会被指定为 Survivor区或者Old 区。在逻辑上,所有的 Eden 区和 Survivor区合起来就是年轻代,所有的 Old 区拼在一起那就是老年代
这样划分之后,使得 G1 不必每次都去收集整个堆空间,而是以增量的方式来进行处理: 每次只处理一部分内存块,称为此次 GC 的回收集(collection set)。每次 GC 暂停都会收集所有年轻代的内存块,但一般只包含部分老年代的内存块。G1 的另一项创新是,在并发阶段估算每个小堆块存活对象的总数。构建回收集的原则是:垃圾最多的小块会被优先收集。这也是 G1 名称的由来。
步骤:
阶段 1: Initial Mark ( 初始标记 )
此阶段标记所有从 GC 根对象直接可达的对象。
阶段 2: Root Region Scan ( Root 区扫描 )
此阶段标记所有从 "根区域" 可达的存活对象。根区域包括:非空的区域,以及在标记过程中不得不收集的区域。
阶段 3: Concurrent Mark ( 并发标记 )
此阶段和 CMS 的并发标记阶段非常类似:只遍历对象图,并在一个特殊的位图中标记能访问到的对象。
阶段 4: Remark ( 再次标记 )
和 CMS 类似,这是一次 STW 停顿(因为不是并发的阶段),以完成标记过程。 G1 收集器会短暂地停止应用线程,停止并发更新信息的写入,处理其中的少量信息,并标记所有在并发标记开始时未被标记的存活对象。
阶段 5: Cleanup ( 清理 )
最后这个清理阶段为即将到来的转移阶段做准备,统计小堆块中所有存活的对象,并将小堆块进行排序,以提升GC 的效率,维护并发标记的内部状态。 所有不包含存活对象的小堆块在此阶段都被回收了。有一部分任务是并发的:例如空堆区的回收,还有大部分的存活率计算。此阶段也需要一个短暂的 STW 暂停。
G1 GC-- 配置参数
-XX:+UseG1GC:启用 G1 GC;
-XX:G1NewSizePercent:初始年轻代占整个 Java Heap 的大小,默认值为 5%;
-XX:G1MaxNewSizePercent:最大年轻代占整个 Java Heap 的大小,默认值为 60%;
-XX:G1HeapRegionSize:设置每个 Region 的大小,单位 MB,需要为 1,2,4,8,16,32 中的某个值,默认是堆内存的 1/2000。如果这个值设置比较大,那么大对象就可以进入 Region 了。
-XX:ConcGCThreads:与 Java 应用一起执行的 GC 线程数量,默认是 Java 线程的 1/4,减少这个参数的数值可能会提升并行回收的效率,提高系统内部吞吐量。如果这个数值过低,参与回收垃圾的线程不足,也会导致并行回收机制耗时加长。
-XX:+InitiatingHeapOccupancyPercent(简称 IHOP):G1 内部并行回收循环启动的阈值,默认为 Java Heap的 45%。这个可以理解为老年代使用大于等于 45% 的时候,JVM 会启动垃圾回收。这个值非常重要,它决定了在什么时间启动老年代的并行回收。
-XX:G1HeapWastePercent:G1停止回收的最小内存大小,默认是堆大小的 5%。GC 会收集所有的 Region 中的对象,但是如果下降到了 5%,就会停下来不再收集了。就是说,不必每次回收就把所有的垃圾都处理完,可以遗留少量的下次处理,这样也降低了单次消耗的时间。
-XX:G1MixedGCCountTarget:设置并行循环之后需要有多少个混合 GC 启动,默认值是 8 个。老年代Regions的回收时间通常比年轻代的收集时间要长一些。所以如果混合收集器比较多,可以允许 G1 延长老年代的收集时间。
-XX:+G1PrintRegionLivenessInfo:这个参数需要和 -XX:+UnlockDiagnosticVMOptions 配合启动,打印 JVM 的调试信息,每个 Region 里的对象存活信息。
-XX:G1ReservePercent:G1 为了保留一些空间用于年代之间的提升,默认值是堆空间的 10%。因为大量执行回收的地方在年轻代(存活时间较短),所以如果你的应用里面有比较大的堆内存空间、比较多的大对象存活,这里需要保留一些内存。
-XX:+G1SummarizeRSetStats:这也是一个 VM 的调试信息。如果启用,会在 VM 退出的时候打印出 Rsets 的详细总结信息。如果启用 -XX:G1SummaryRSetStatsPeriod 参数,就会阶段性地打印 Rsets 信息。
-XX:+G1TraceConcRefinement:这个也是一个 VM 的调试信息,如果启用,并行回收阶段的日志就会被详细打印出来。
-XX:+GCTimeRatio:这个参数就是计算花在 Java 应用线程上和花在 GC 线程上的时间比率,默认是 9,跟新生代内存的分配比例一致。这个参数主要的目的是让用户可以控制花在应用上的时间,G1 的计算公式是 100/(1+GCTimeRatio)。这样如果参数设置为 9,则最多 10% 的时间会花在 GC 工作上面。Parallel GC 的默认值是 99,表示 1% 的时间被用在 GC 上面,这是因为 Parallel GC 贯穿整个 GC,而 G1 则根据 Region 来进行划分,不需要全局性扫描整个内存堆。
-XX:+UseStringDeduplication:手动开启 Java String 对象的去重工作,这个是 JDK8u20 版本之后新增的参数,主要用于相同 String 避免重复申请内存,节约 Region 的使用。
-XX:MaxGCPauseMills:预期 G1 每次执行 GC 操作的暂停时间,单位是毫秒,默认值是 200 毫秒,G1 会尽量保证控制在这个范围内。
1、年轻代模式转移暂停(Evacuation Pause)
G1 GC 会通过前面一段时间的运行情况来不断的调整自己的回收策略和行为,以此来比较稳定地控制暂停时间。在应用程序刚启动时,G1 还没有采集到什么足够的信息,这时候就处于初始的 fully-young 模式。当年轻代空间用满后,应用线程会被暂停,年轻代内存块中的存活对象被拷贝到存活区。如果还没有存活区,则任意选择一部分空闲的内存块作为存活区。拷贝的过程称为转移(Evacuation),这和前面介绍的其他年轻代收集器是一样的工作原理
各GC对比