一、JVM整体架构
1、JVM(Java虚拟机):指以软件的方式模拟具有完整硬件系统功能、运行在一个完全隔离环境中的完整计算机系统,是物理机的软件实现。常用的虚拟机有VMWare、Virtual Box、Java Virtual Machine。
2、JVM由三个主要的子系统构成
- 类加载子系统 (即类加载器 / ClassLoader)
- 运行时数据区(即内存结构 / 内存模型 / JMM)
- 执行引擎(包含垃圾收集器)
通过类加载器将.class文件加载进内存中,再由执行引擎去运行。
堆和方法区是所有线程都共享的。
Java栈、本地方法栈和程序计数器是非线程共享的。
二、JVM内存结构
下面会用到的例子:定义一个Math类。
1、本地方法栈(线程私有):登记native本地方法(即native方法是存到本地方法栈),在执行引擎执行时加载本地方法库(C语言实现的库)。
举例:Thread类中的 start0() 方法底层实现是用C语言写的。
2、程序计数器(线程私有):就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即指向将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
举例:对.class文件进行 javap -c 反汇编,下图为 math() 方法反汇编后的结果:(iconst_1和istore_1两条指令代表 int a=1)
(注意:反汇编是得到汇编代码,反编译是得到高级语言代码)
3、方法区(线程共享):类的所有字段和方法字节码,以及一些特殊方法如构造函数、接口代码也定义在此(类加载器会将字节码文件加载到方法区中,分解成多个部分)。简单说,所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中,虽然JVM规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
4、Java栈(线程私有):Java线程执行方法的内存模型,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表、操作数栈、动态链接、方法出口等信息),不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。
JVM对该区域规范了两种异常:
① 线程请求的栈深度大于虚拟机栈所允许的深度时,将抛出StackOverFlowError异常。
② 若虚拟机栈可动态扩展,当无法申请到足够内存空间时将抛出OutOfMemoryError,通过JVM参数 -> Xss指定栈空间,空间大小决定函数调用的深度。
下图左边为Java栈的内存结构图:
动态链接:如Map map = new HashMap(),程序运行时map变量需要去找到运行时的HashMap实例对象(多态)的过程,就叫动态链接。
方法出口:如main()方法中的math()方法运行结束return返回的值用于main()方法中,return的这个过程就叫方法出口。
5、堆(线程共享): 虚拟机启动时创建,用于存放对象实例,几乎所有的对象(包含常量池)都放在堆上分配内存,当对象无法在该空间申请到内存时将抛出OutOfMemoryError异常。同时也是垃圾收集器管理的主要区域。可通过 -Xmx -Xms 参数来分别指定最大堆和最小堆。
下图为堆的内存结构图:
JVM调优的根本目的:尽量减少Full GC的次数,并且缩短每次Full GC的时间长度。(Full GC性能非常低,执行这个过程时会停止整个JVM,即STW(Stop The World),包括程序运行的那些线程,然后专门去做垃圾收集。现在新的GC会尽量缩短STW时间长度,尽量让垃圾收集和业务线程并发执行)
垃圾收集器(GC)存在的意义:回收堆中无引用的对象,释放空间。
堆中新生代(Young Generation)占1/3的堆空间,新生代包括伊甸园区(Eden Space)和幸存者区(Survivor Space);老年代(Old Generation)占2/3的堆空间。
JDK1.8以前是没有元数据区(MetaData Space)的,而是永久代,从1.8开始元空间取代了永久代,本质和永久代类似,都是对JVM规范中方法区的实现,区别在于元数据区并不在虚拟机中,不属于堆中的内存结构,而是直接使用本机物理内存,永久代在虚拟机中,逻辑结构上属于堆,但是物理上不属于堆,堆大小=新生代+老年代。元数据区也有可能发生OutOfMemory异常。
- JDK1.6及之前:有永久代,常量池在方法区
- JDK1.7:有永久代,但已逐步“去永久代”,常量池在堆
- JDK1.8及之后:无永久代,常量池在元空间
提问:为什么JDK1.8用元数据区取代了永久代?
官方解释:移除永久代是为融合HotSpot JVM与JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。
元数据区的动态扩展:默认-XX:MetaspaceSize值为21MB的高水位线。一旦触及则Full GC将被触发并卸载没有用的类(类对应的类加载器不再存活),然后高水位线将会充值。新的高水位线的值取决于GC后释放的元空间。如果释放的空间少,这个高水位线则上升。如果释放空间过多,则高水位线下降。
(JDK 8实际上是这两种虚拟机合并之后的产物,HotSpot JVM(Sun公司开发),JRockit VM(BEA公司开发)。猜测:HotSpot JVM输了,没错,开发出Java语言的Sun公司最后输了,就听JRockit VM的)
① new出来的对象会放在Eden区中,当Eden区占满时会做一次Minor GC(即轻GC),回收Eden区中无引用的对象,剩下的存活对象则会放到From区中。
② 后面又有新的new出来的对象不断存放在Eden区中,当Eden区占满时再做Minor GC,又有对象存放在From区中,当From区占满时也会做Minor GC,GC会回收Eden区和From区中无引用的对象,剩下的存活对象则会放到To区中,此时角色转变,From区变成To区,To区变成From区。
③ 接着,还会有新的对象存放到Eden区中,当Eden区占满时再做Minor GC,此时存活的对象放到新的From区中,当新的From区放满时又做Minor GC,把存活对象放入新的To区,依次轮循。
④ 如果一直有新的对象过来,不断的做Minor GC,当From区和To区占满时(或者即使没占满,当幸存者区的From区和To区轮循15次时(默认15),如果To区还有存活的对象),再做一次Minor GC则会把存活的对象移入老年代。
⑤ 如果有一天老年代也占满了,则会触发Full GC再次回收无引用对象(Full GC 是清理整个堆空间—包括年轻代和老年代)。
三、JVM执行引擎
JVM执行引擎:读取运行时数据区的Java字节码并逐个执行。
解释执行字节码的方式:① JIT编译器;② 字节码解释器;③ mixed mode(前两种的混合模式,JDK 8是采用这种,之前的我不知道)
JIT编译器(Just In Time,即时编译):一次性解释所有指令,第一次执行慢,后面执行快。(会缓存)
字节码解释器:逐行解释指令,每次执行需要重新解释,前几次可能比JIT编译器快,后面比它慢。(不缓存)