JVM 结构
JVM 总体结构图
类加载子系统与方法区:
类加载子系统负责从文件系统和网络中加载 Class 信息, 加载的类信息存放于一块称为方法区的内存空间.
除了类信息外, 方法区中还可能会存放运行时常量池信息, 包括字符串字面量和数字常量(这部分常量信息是 Class 文件中常量池部分的内存映射)
Java 堆:
java 堆在虚拟机启动时建立, 它是 java 程序最主要的内存工作区域.
几乎所有的 java 对象实例都存放在 java 堆中. 堆空间是所有线程共享的.
直接内存:
java 的 NIO 库允许使用直接内存.
直接内存是在 java 堆外的, 直接向系统申请的内存空间.
通常访问直接内存的速度会优于 java 堆.
出于对性能的考虑, 读写频繁的场合可能会考虑使用直接内存.
由于直接内存在 java 堆外, 因此它的大小不会受限于 Xmx 指定的最大堆大小, 但是系统内存是有限的, java 堆和直接内存的总和依然受限于操作系统能给出的最大内存.
垃圾回收系统:
垃圾回收器可以对方法区, java 堆和直接内存进行回收. 其中, java 堆是垃圾收集器的工作重点.
和 C/C++ 不同, java 中所有的对象空间释放都是隐式的, java 中没有类似 free()或者 delete()这样的函数释放指定的内存区域.
对于不再使用的垃圾对象, 垃圾回收系统会在后台查找, 标识并释放对象, 完成 java 堆, 方法区和直接内存中的全自动化管理.
java 栈:
每一个 java 虚拟机线程都有一个私有的 java 栈.
一个线程的 java 栈在线程创建的时候被创建.
java 栈中保存着帧信息, 保存着局部变量, 方法参数, 同时和 java 方法的调用, 返回密切相关.
本地方法栈:
和 java 栈非常类似.
java 栈用于方法的调用, 而本地方法栈则用于本地方法的调用.
作为对 java 虚拟机的重要扩展, java 虚拟机允许 java 直接调用本地方法(通常使用 C 编写)
PC(Program Counter):
PC 寄存器也是每一个线程私有的空间.
java 虚拟机会为每一个 java 线程创建 PC 寄存器.
在任意时刻, 一个 Java 线程总是在执行一个方法, 这个正在被执行的方法称为当前方法. 如果当前方法不是本地方法, PC 寄存器就会指向当前正在被执行的指令. 如果当前方法是本地方法, 那么 PC 寄存器的值就是 undefined.
执行引擎:
负责执行虚拟机的字节码.
现代虚拟机为了提高执行效率, 会使用即使编译 (just in time) 技术将方法编译成机器码后再执行.
JVM 堆结构及分代
堆内存是 java 虚拟机管理的内存中最大的一块, 也是垃圾回收最频繁的一块区域, 程序中所有的对象实例都存放在堆内存中.
给堆内存分代是为了提高对象内存分配和垃圾回收的效率.
如果堆内存没有区域划分, 所有的新创建的对象和生命周期很长的对象放在一起, 随着程序的执行, 堆内存需要频繁进行垃圾收集, 而每次回收都要遍历所有的对象, 所花费的时间代价是巨大的, 也会严重影响 GC 效率.
Java 虚拟机根据对象存活的周期不同, 把堆内存分为新生代, 老年代和永久代(于 JDK8 中把存放元数据中的永久内存从堆内存中移到了本地内存(native memory))
新创建的对象会在新生代中分配内存, 经过多次回收仍然存活下来的对象放在老年代.
新生代中的对象存活时间短, 需要频繁地进行垃圾回收以保证无用对象尽早被释放掉; 老年代中对象生命周期长, 内存回收的频率相对较低, 不需要频繁进行回收. 不同年代采用各自合适的垃圾回收算法, 可以大大提高回收效率.
新生代(Young Generation)
新生代主要存放新生成的对象, 内存大小相对会比较小, 垃圾回收会比较频繁. 且垃圾回收效率高, 通常进行一次垃圾收集一般可以回收 70% ~ 95% 的空间.
HotSpot JVM 把新生代代分为了三部分: 1 个 Eden 区和 2 个 Survivor 区 (分别叫 from 和 to), 默认比例是 8:1:1. 这样划分的目的是因为 HotSpot 采用复制算法来回收新生代.(复制算法的基本思想就是将内存分为两块, 每次只用其中一块, 当这一块内存用完, 就将还活着的对象复制到另外一块上面. 复制算法不会产生内存碎片.) 新生成的对象在 Eden 区分配(一些大对象除外), 这些对象经过第一次 Minor GC 后, 如果仍然存活, 将会被移到 Survivor 区. 当 Eden 区没有足够的空间进行分配时, 虚拟机将发起一次 Minor GC.
在 GC 开始的时候, 对象只会存在于 Eden 区和名为 "From" 的 Survivor0 区, Survivor1 区 "To" 是空的 (作为保留区域).GC 进行时, Eden 区中所有存活的对象都会被复制到 "To"Survivor1 区, 而在 "From"Surivor0 区中, 仍存活的对象会根据他们的年龄值来决定去向. 新生代中的对象每熬过一轮垃圾回收, 年龄值就加 1, 年龄值达到年龄阀值(默认为 15, 可以通过 - XX:MaxTenuringThreshold 来设置, GC 分代年龄存储在对象的 header 中) 的对象会被移到老年代中, 没有达到阀值的对象会被复制到 To Survivor1 区. 经过这次 GC 后, Eden 区和 "From"Surivor0 区已经被清空. 接着将 "To" 区与 "From" 区交换, 确保 "To" 区在一轮 GC 后是空的. Minor GC 会一直重复这样的过程, 直到 "To" 区被填满, 没有足够的空间存放上一次新生代收集下来的存活对象时, 会将所有的对象移入老年代.
老年代(Old Generation)
老年代主要存放老年代的空间用于存放长时间幸存的对象, 即在新生代中经历了多次 GC 后仍然存活下来的对象. 老年代中的对象生命周期较长, 存活率比较高, 在老年代中进行 GC 的频率相对而言较低, 回收的速度也比较慢.
通常当老年代内存被占满时进行一次 Major GC. 相较于 minor GC, Major GC 的执行次数要比 minor GC 要少很多, 同时, Major Gc 执行的时间较 Minor Gc 要长.
永久代(Permanent)
于 JDK8 中把存放元数据中的永久内存从堆内存中移到了本地内存(native memory).
JVM 垃圾回收算法及收集器
垃圾回收常见算法
引用计数(Reference Counting)
比较古老的回收算法, 原理是此对象有一个引用, 即增加一个计数, 删除一个引用则减少一个计数.
垃圾回收时, 只用收集计数为 0 的对象, 此算法最致命的是无法处理循环引用的问题.
复制(Copying)
此算法把内存空间划为两个相等的区域, 每次只使用其中一个区域.
垃圾回收时, 遍历当前使用区域, 把正在使用中的对象复制到另外一个区域中, 此算法每次只处理正在使用中的对象, 因此复制成本较小, 同时复制过去以后还能进行相应的内存管理, 不会出现 "碎片" 问题.
当然, 此算法的缺点也很明显, 就是需要两倍内存空间.
标记 - 清除(Mark - Sweep)
此算法执行分两阶段, 第一阶段从引用根结点开始标记所有被引用的对象, 第二阶段遍历整个堆, 把未标记的对象清除. 此算法需要暂停整个应用, 同时, 会产生内存碎片.
标记 - 整理(Mark-Compact)
此算法结合了 "标记 - 清除" 和 "复制" 两个算法的优点. 也是分两个阶段, 第一阶段从根结点开始标记所有被引用对象, 第二阶段遍历整个堆, 清除未标记对象并且把存活对象 "压缩" 到堆的其中一块, 按顺序排放. 此算法避免了 "标记 - 清除" 的碎片问题, 同时也避免了 "复制" 算法的空间问题.
jvm 中垃圾收集器
Scavenge Gc(次收集)和 Full GC(全收集)的区别
新生代 GC(Scavenge GC):Scavenge GC 指发生在新生代的 GC, 因为新生代的 java 对象生命周期短, 所以 Scavenge GC 非常频繁, 一般回收速度也比较快. 当 Eden 空间不足为对象分配内存时, 会触发 Scavenge GC.
一般情况下, 当新对象生成, 并且在 Eden 申请空间失败时, 就会触发 Scavenge GC, 对 Eden 区域进行 GC, 清除非存活对象, 并且把尚且存活的对象移动到 Survivor 区. 然后整理 Survivor 的两个区. 这种方式的 GC 是对年轻代 Eden 区进行, 不会影响到老年代. 因为大部分对象都是从 Eden 区开始的, 同时 Eden 区不会分配的很大, 所以 Eden 区的 GC 会频繁进行. 因而, 一般在这里需要使用速度快, 效率高的算法, 使 Eden 区能尽快空闲出来.
老年代 GC(Full GC/Major GC):Full GC 指发生在老年代的 GC, 出现了 Full GC 一般会伴随着至少一次的 Minor GC(老年代对象大部分是 Minor GC 过程中从新生代进入老年代), 比如分配担保失败. Full GC 的速度一般会比 Minor GC 慢 10 倍以上. 当老年代内存不足或者显式调用 System.gc()方法时, 会触发 Full Gc.
次收集
当年轻代堆空间紧张时会被触发
相对于全收集而言, 收集间隔较短
全收集
当老年代或者持久代堆空间满了, 会触发全收集操作
可以使用 System.gc()方法来显式的启动全收集
全收集一般根据堆大小的不同, 需要的时间不尽相同, 但一般会比较长.
分代垃圾回收器
新生代收集器
串行收集器(Serial)
Serial 收集器是 JAVA 虚拟机中最基本, 历史最悠久的收集器, 在 JDK 1.3.1 之前是 JAVA 虚拟机新生代收集的唯一选择. Serial 收集器是一个, 但它的 "单线程" 的意义并不仅仅是说明它只会使用一个 CPU 或一条收集线程去完成垃圾收集工作, 更重要的是在它进行垃圾收集时, 必须暂停其他所有的工作线程, 直到它收集结束.
Serial 收集器到 JDK1.7 为止, 它依然是 JAVA 虚拟机运行在 Client 模式下的默认新生代收集器. 它也有着优于其他收集器的地方: 简单而高效(与其他收集器的单线程比), 对于限定单个 CPU 的环境来说, Serial 收集器由于没有线程交互的开销, 专心做垃圾收集自然可以获得最高的单线程收集效率. 在用户的桌面应用场景中, 分配给虚拟机管理的内存一般来说不会很大, 收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存, 桌面应用基本上不会再大了), 停顿时间完全可以控制在几十毫秒最多一百多毫秒以内, 只要不是频繁发生, 这点停顿是可以接受的. 所以, Serial 收集器对于运行在来说是一个很好的选择.
并行收集器(ParNew)
ParNew 收集器是 JAVA 虚拟机中垃圾收集器的一种. 它是 Serial 收集器的多线程版本, 除了使用多条线程进行垃圾收集之外, 其余行为包括 Serial 收集器可用的所有控制参数(例如:-XX:SurvivorRatio, -XX:PretenureSizeThreshold,-XX:HandlePromotionFailure 等), 收集算法, Stop The World, 对象分配规则, 回收策略等都与 Serial 收集器一致.
ParNew 是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,, 除了 Serial 收集器外, 只有它能与 CMS 收集器配合工作.
Paraller Scavenge 收集器
Parallel 是采用复制算法的多线程新生代垃圾回收器, 似乎和 ParNew 收集器有很多的相似的地方. 但是 Parallel Scanvenge 收集器的一个特点是它所关注的目标是吞吐量(Throughput). 所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值, 即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间). 停顿时间越短就越适合需要与用户交互的程序, 良好的响应速度能够提升用户的体验; 而高吞吐量则可以最高效率地利用 CPU 时间, 尽快地完成程序的运算任务, 主要适合在后台运算而不需要太多交互的任务.
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本, 采用多线程和 "标记 - 整理" 算法. 这个收集器是在 jdk1.6 中才开始提供的, 在此之前, 新生代的 Parallel Scavenge 收集器一直处于比较尴尬的状态. 原因是如果新生代 Parallel Scavenge 收集器, 那么老年代除了 Serial Old(PS MarkSweep)收集器外别无选择. 由于单线程的老年代 Serial Old 收集器在服务端应用性能上的 "拖累", 即使使用了 Parallel Scavenge 收集器也未必能在整体应用上获得吞吐量最大化的效果, 又因为老年代收集中无法充分利用服务器多 CPU 的处理能力, 在老年代很大而且硬件比较高级的环境中, 这种组合的吞吐量甚至还不一定有 ParNew 加 CMS 的组合 "给力". 直到 Parallel Old 收集器出现后,"吞吐量优先" 收集器终于有了比较名副其实的应用祝贺, 在注重吞吐量及 CPU 资源敏感的场合, 都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器.
-UseParallelGC: 虚拟机运行在 Server 模式下的默认值, 打开此开关后, 使用 Parallel Scavenge + Serial Old 的收集器组合进行内存回收.-UseParallelOldGC: 打开此开关后, 使用 Parallel Scavenge + Parallel Old 的收集器组合进行垃圾回收
老年代收集器
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本, 它同样是一个单线程收集器, 使用 "标记 - 整理" 算法.
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 使用多线程和 "标记 - 整理" 算法.
CMS 收集器
CMS 收集器是基于 "标记 - 清除" 算法实现的, 它的运作过程相对于前面几种收集器来说要更复杂一些, 整个过程分为 6 个步骤, 包括:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
并发预清理(CMS-concurrent-preclean)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
并发重置(CMS-concurrent-reset)
其中初始标记, 重新标记这两个步骤仍然需要 "Stop The World". 初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象, 速度很快, 并发标记阶段就是进行 GC Roots Tracing 的过程, 而重新标记阶段则是为了修正并发标记期间, 因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录, 这个阶段的停顿时间一般会比初始标记阶段稍长一些, 但远比并发标记的时间短. 其他动作都是并发的.
分区收集 - G1 收集器
JVM 优化
JVM 小工具
java jdk 的 bin 目录下带有一些 jvm 小工具.
jps(Java Virtual Machine Process Status Tool)
列出正在运行的 java 进程, 并显示执行主类的名称及进程在本地 JVM 中的 ID.
使用方法:
jps [options][hostid] [options]:
-q: 只输出 LVMID
-m: 输出 JVM 启动时传给主类的方法
-l: 输出主类的全名, 如果是 Jar 则输出 jar 的路径
-v: 输出 JVM 启动参数
jstat(Java Virtual Machine Statistics Monitoring Tool)
JVM 统计信息监控工具.
监控 JVM 各种运行状态信息, 如虚拟机进程中的类装载, 内存, GC,JIT 编译等数据.
使用方法:
vmid 是虚拟机 ID, 在 Linux/Unix 系统上一般就是进程 ID.interval 是采样时间间隔. count 是采样数目.
S0C,S1C,S0U,S1U:Surivor 0/1 区容量 (Capacity) 和使用量(Used)
EC,EU:Eden 区容量和使用量
OC,OU: 年老代容量和使用量
MC,MU: 方法区容量和使用量
CCSC,CCSU: 压缩类容量和使用量
YGC,YGT: 年轻代 GC 次数和 GC 耗时
FGC,FGT:Full GC 次数和 Full GC 耗时
GCT:GC 耗时
jmap(Java Virtual Machine Memory Map for Java)
java 内存映像工具.
用于生成堆转储快照, 即 dump 文件可以查询 finalize 执行队列, Java 堆和永久代的详细信息(使用率, 当前用的 GC 等).
jstack(Java Virtual Machine Stack Trace for Java)
堆栈跟踪工具.
用于生成 JVM 当前的线程快照 (即当前 JVM 内每一个条线程正在执行的方法堆栈集合) 用于分析线程出现长时间停顿的原因.
javap
查看经 javac 之后产生的 JVM 字节码代码
jcmd
一个多功能工具, 可以用来导出堆, 查看 Java 进程, 导出线程信息, 执行 GC, 查看性能相关数据等.
jvisualvm
JDK 中最强大运行监视和故障处理工具
JVM 参数介绍
-Xms: 初始堆大小
-Xmx: 最大堆大小
-XX:NewSize=n 设置年轻代大小
-XX:NewRatio=n 设置年轻代和老年代的比值
-XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值
-XX:MaxPermSize=n 设置持久代大小
收集器设置
-XX:+UseSerialGC 设置串行收集器
-XX:+UseParallelGC 设置并行收集器
-XX:+UseParalledlOldGC 设置并行年老代收集器
-XX:+UseConcMarkSweepGC 设置并发收集器
垃圾回收统计信息
- -XX:+PrintGC
- -XX:+Printetails
- -XX:PrintGCTimeStamps
- -Xloggc:filename
来源: https://www.cnblogs.com/shangyang/p/10546942.html