JVM,听到这个词是不是很熟悉,只要你是做过几年Java开发,估计也都被面试官用这个东东虐过,我把自己收集整理的笔记放在这里,希望能帮到你
一、概述
- JVM: Java Virtual Mathine, Java虚拟机
- 虚拟机:指通过软件模拟的具有完整硬件功能的, 运行在完全隔离环境中的完整计算机系统;如: VirtualBox, VMWare, JVM, 其中VirtualBox和VMWare使用软件模拟物理计算机的指令集; 而JVM模拟Java字节码指令集, 并且裁剪了部分寄存器的模拟;
二、Java发展历史:
- 1996 -> SUN JDK 1.0 Classic VM(纯解释执行, 使用外挂进行JIT)
- 1997 -> JDK 1.1
- AWT
- InnerClass
- JDBC
- RMI
- Reflect
- 1998 -> JDK 1.2 Solaris Exact VM
- JIT和解释器混合
- Accurate Memory Management, 数据类型敏感
- GC性能
- Swing Collections)
- Java 1.2 开始称为 Java 2 (J2SE, J2EE, J2ME)
- 2000 -> JDK 1.3 hotspot 作为默认虚拟机发布(假如JavaSound)
- HotSpot由Longview Technologies发布, 后被SUN公司收购,HotSpot是目前被广泛使用的JVM
- 2002 -> JDK 1.4 (Classic VM退出历史舞台)
- Assert
- Regex
- NIO
- IPV6
- Log API
- Encode Lib
- 2004 -> JDK 5
- Generic
- Annotation
- Fold Box
- Enum
- Params Dynamic Length
- Foreach
- 2006 -> JDK 1.6
- Script Support
- JDBC 4.0
- Java Compiler API
- 同一年, Java开源, 并建立OpenJDK
- HotSpot成为SUN JDK和OpenJDK 的默认虚拟机
- 2008 Oracle收购BEA公司, 得到了JRockit VM
- 2010 Oracle收购SUN公司, 得到HotSot VM
- 2011 -> JDK 7
- G1 Collector
- Dynamic Language Enhency
- NIO 2.0
- Compress Indicator in X64
- 2014 -> JDK 8
- Lambda
- Sytax Enhency
- Java Type Annotation
- Integrate The Two VMs, Transplant JRockit Advantages to HotSpot
- 2016 -> JDK 9
- Modularity
三、Java规范:
- Java语言规范
- 语法, 变量, 类型, 文法
- JVM规范
- Class文件类型, 运行时数据, 帧栈, 虚拟机的启动, 虚拟机的指令集
四、Java Vritual Machine的启动流程
- 使用java命令运行一个字节码文件
java <java-class-file-name-without-ext>
- 加载Java环境的配置文件
- 根据环境变量加载
<jre-home>/lib/amd64/jvm.cfg
; - 根据这个jvm.cfg文件中的配置加载
<jre-home>/bin/server/jvm.dll
,这个jvm.dll
就是在windows上JVM的实现,利用这个文件初始化JVM;
- 根据环境变量加载
- 初始化JVM完成后就会去获取JVM的
JNIEnv
接口,然后根据要运行的类查找main方法,找到后就开始执行main方法;
五、JVM内存模型
-
结构图
- JVM内存模型详解
-
数据区
- 方法区
- 保存类的元信息,如:类的描述信息、类中的静态变量,常量,JIT即时编译代码(动态生成的类等信息);
- 通常和永久区(Perm)关联在一起;
- 在JDK6的时候,字符串的常量是放置在常量区,但是JDK7之后就移到了堆中;
- Java堆
- 程序中动态创建的所有对象都是存放在Java堆中的;
- Java堆对所有的线程共享;
- 从分代GC的角度看,堆分为如下几个区:eden,s0,s1,tenured;
- 堆分区:
- 1.8以前JVM的内存模型:
- 新生代(属于堆): 使用复制回收算法管理
- eden: 堆中申请空间第一个考虑的位置;
- s0(from):
- s1(to):
- 老年代(Old Generation, 属于堆)
- 永久代(Survivor Space/Compacting Perm Generation, 属于方法区)
- 新生代(属于堆): 使用复制回收算法管理
- 1.8内存模型
- 新生代(New Generation,属于堆)
- eden
- s0(from)
- s1(to)
- 老年代(Tenured Generation,属于堆)
- meta Space: 用来取代永久代,1.8之前的永久代的大小是固定的, 可能溢出, 而meta space的大小是动态扩容的;
- 新生代(New Generation,属于堆)
- 1.8以前JVM的内存模型:
- 方法区
-
指令区
- 程序计数器: 指向当前线程正在执行的字节码指令的地址;
- 虚拟机栈
用来存储当前程序运行当前方法锁需要数据、指令以及返回地址;
虚拟机栈是线程私有的;
-
虚拟机栈由一系列的方法帧组成,每个方法帧中保存该方法创建的局部变量表,操作数栈,常量池指针;栈帧中包含:
- 局部变量表:
- 当前方法的局部变量,即8大基本数据类型和引用类型的指针的值会存储在这个地方;
- 实例方法和静态方法局部变量表的不同点是实例方法局部变量表中有一个this的引用;
- 操作数栈: 模拟真实机器的寄存器
- 各种运算的操作数, 即Java中的所有的运算符操作都是在这里进行的;
- 由于Java中没有寄存器, 所以参数的传递使用操作数栈实现;
- 动态链接: 运行的时候动态的绑定具体的类型, 即Java的三大特性之一: 多态, 就是在这里进行链接的;面相接口编程的实现;
- 出口: 即当前栈帧的方法运行的出口, 有可能是正常执行结束, 有可能return语句, 也有可能是抛异常;
- ...
- 局部变量表:
栈是线程私有的
1>虚拟机栈是用来保存方法的局部变量,操作数栈,常量池指针;
2>每一次调用方法的时候都会创建一个栈帧, 并压栈;-
栈上分配内存:
public class OnStackAlloc { public static void alloc() { byte[] b = new byte[2]; b[0] = 1; } public static void main(String[] args) { long begin = System.currentTimeMillis(); for (int index = 0; index < 999999999; index++) { alloc(); } long end = System.currentTimeMillis(); System.out.println(end - begin); } }
- 分别使用参数:
-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC
和-server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:+PrintGC
运行程序,两者执行结果分别为:// 前者执行结果 [GC (Allocation Failure) 2048K->1011K(9728K), 0.0035452 secs] [GC (Allocation Failure) 3059K->1253K(9728K), 0.0028998 secs] 12 // 后者执行结果 [GC (Allocation Failure) 2048K->1035K(9728K), 0.0017382 secs] [GC (Allocation Failure) 3083K->1287K(9728K), 0.0179023 secs] 。。。 [GC (Allocation Failure) 3344K->1296K(9728K), 0.0002343 secs] [GC (Allocation Failure) 3344K->1296K(9728K), 0.0003139 secs] 13202
- 前者是在栈上分配的,后者是在堆上分配的;
- 分别使用参数:
-
本地方法栈
- 主要用来记录调用C/C++代码时候的出栈入栈等操作和数据的
-
-
内存模型总结
六、堆内存分代
-
什么是堆内存分代
- 堆内存分代即把堆内存划分为几个区域,每个区域中存放不同生命周期长度的对象,是堆内存回收的一种有效的分而治之的思想的体现;
-
为什么要对堆内存进行分代
- 我们知道,堆是用来动态开辟内存空间的内存区域,不同于栈等其他的内存区域,堆是需要进行手动回收内存的,但是回收的操作不好控制,JVM就把这部分工作接管。每当JVM的GC被触发,就会进行一次垃圾回收,如果不分区,所有的不同生命周期的对象(业务类对象,方法局部变量对象等)都会被扫描一遍,这样的话,垃圾回收的效率极其低下,但是如果对堆内存进行分区管理,每个区域存放不同生命周期的对象,那么触发GC的时候,先去扫描释放生命周期较短的对象,如果释放的内存已经足够,就没有必要扫描生命周期更长的区域,如果内存还紧张,则按照存放对象的生命周期由短到长次序扫描即可,大大的提高了GC的效率;
-
堆内存是如何分代的
-
堆内存分代目前有两个版本,一个是JDK7及之前,一个是JDK8,相同部分如下:
- 在JDK8之前,堆内存中还有一个永久代(Permenent Generation)
- 年轻代中的各区比值为:Eden:S0 :S1 = 8:1:1;
- S0和S1默认情况下会动态的自动调整大小,可以使用-XX:-UseAdaptiveSizePolicy来关闭动态调整;
-
-
JVM垃圾回收算法
- 标记-清除:这是垃圾收集算法中最基础的,根据名字就可以知道,它的思想就是标记哪些要被回收的对象,然后统一回收。这种方法很简单,但是会有两个主要问题:1.效率不高,标记和清除的效率都很低;2.会产生大量不连续的内存碎片,导致以后程序在分配较大的对象时,由于没有充足的连续内存而提前触发一次GC动作。
- 复制算法:为了解决效率问题,复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清除完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一半的内存。 于是将该算法进行了改进,内存区域不再是按照1:1去划分,而是将内存划分为8:1:1三部分,较大那份内存称为Eden区,其余是两块较小的内存区叫Survivor区。每次都会优先使用Eden区,若Eden区满,就将对象复制到第二块内存区上,然后清除Eden区,如果此时存活的对象太多,以至于Survivor不够时,会将这些对象通过分配担保机制复制到老年代中。
- 标记-整理:该算法主要是为了解决标记-清除,产生大量内存碎片的问题;当对象存活率较高时,也解决了复制算法的效率问题。它的不同之处就是在清除对象的时候现将可回收对象移动到一端,然后清除掉端边界以外的对象,这样就不会产生内存碎片了。
- 分代收集:现在的虚拟机垃圾收集大多采用这种方式,它根据对象的生存周期,将堆分为新生代和老年代。在新生代中,由于对象生存期短,每次回收都会有大量对象死去,那么这时就采用复制算法。老年代里的对象存活率较高,没有额外的空间进行分配担保,所以可以使用标记-整理 或者 标记-清除。
-
JVM垃圾回收器
- Parallel Old:使用标记整理算法
- Serial Old:使用标记整理算法,是CMS的备用算法,当新生代的空间不够的时候需要老年代担保,而这个时候老年代也不够了就会触发担保失败(Concurrent Mode Failure),从而触发一次Full GC
- CMS:使用标记清除算法,为了进一步的减少STW时间,设计了四个阶段:
- 初始标记:根据GC Roots能直接关联到的对象,速度很快
- 并发标记:业务和GC标记一起运行,根据第一步的GC Roots的一级对象很快地进行Trace,标记处所有的能关联的对象
- 重新标记:这个时候才会STW,重新标记的目的在于重新关联在并发标记过程中状态变化的对象
- 并发清除:清除没有标记到的对象
- CMS缺点:GC敏感的,并发造成CPU对业务的短暂的吞吐量下降,因为使用标记清除算法,所以还有碎片的产生
- G1
- 安全点,安全区域
-
常见的JVM参数:
- 堆设置
-
-Xms<n>
:初始堆大小 -
-Xmx<n>
:最大堆大小 -
-XX:NewSize=<n>
:设置年轻代大小 -
-XX:NewRatio=<n>
:设置年老代和新生代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4 -
-XX:SurvivorRatio=<n>
:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5 -
-XX:MaxPermSize=<n>
:设置持久代大小
-
- 收集器设置
-
-XX:+UseSerialGC
:启用串行收集器 -
-XX:+UseParallelGC
:启用并行收集器 -
-XX:+UseParalledlOldGC
:启用并行年老代收集器 -
-XX:+UseConcMarkSweepGC
:启用并发收集器
-
- 垃圾回收统计信息
-
-XX:+PrintGC
: -
-XX:+PrintGCDetails
: -
-XX:+PrintGCTimeStamps
: -Xloggc:<log-file-path>
-
- 并行收集器设置
-
-XX:ParallelGCThreads=<n>
:设置并行收集器收集时使用的CPU数。并行收集线程数。 -
-XX:MaxGCPauseMillis=<n>
:设置并行收集最大暂停时间 -
-XX:GCTimeRatio=<n>
:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
-
- 并发收集器设置
-
-XX:+CMSIncrementalMode
:设置为增量模式。适用于单CPU情况。 -
-XX:ParallelGCThreads=<n>
:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
-
- 其他配置
-
-XX:PretenureSizeThreshold
:新生代分配对象大小的阈值,超过该值,对象就直接分配在老年代;
-
- 分代GC名称
- Minor GC:新生代GC
- Major GC:老年代GC
- Full GC:全GC,Minor GC + Major GC
- 堆设置
-
典型JVM参数配置:
- -Xmx3550m -Xms3550m -Xmn2g -Xss128k
- -XX:ParallelGCThreads=20
- -XX:+UseConcMarkSweepGC
- -XX:+UseParNewGC
- 解释
- -Xmx3550m:设置JVM最大堆内存为3550M, 堆大小=年轻代大小+年老代大小+持久代大小。
- -Xms3550m:设置JVM最小堆内存为3550m,即最大堆内存 == 最小堆内存 → 堆大小不变化,以避免每次垃圾回收完成后JVM重新分配内存,提高效率。
- -Xmn2g:设置年轻代大小为2G。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,官方推荐配置为整个堆的3/8。
- -Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
- 查看当前的垃圾回收器
- 使用指令: -XX:+PrintFlagsFinal 或者 -XX:+PrintCommandLineFlags
- 默认:PS Scavenge PS MarkSweep,可以通过代码获取:ManageFactory.getGarbageCollectorMXBeans()来获取
- 输出日志
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-Xloggc:/gc.log
-XX:+PrintHeapAtGC - 日志文件控制
-XX:-UseGCLogFileRotation
-XX:GCLogFileSize=8K
-
确定堆大小的方法
- 确定活动对象的占用内存的大小(一次Full GC后老年代的生存对象的磁盘占用大小)
- 根据活动对象大小估计各个分代的大小
- 堆总大小:活动大小的3到4倍
- 新生代大小:活动大小的1到1.5倍
- 老年代大小:活动大小的2到3倍
七、JVM中的引用
- 强引用:不可达的会被回收
- 软引用:内存空间不够了才会被回收
- 弱引用:GC就会被回收
- 虚引用:
附:
- GC日志解读
-
JVM内存结构图解