一、JVM 概述
熟悉JVM架构与GC垃圾回收机制以及相应的堆参调优,有过在linux进行系统调优的经验
JVM是运行在操作系统之上的,他与硬件没有直接的交互。
二、JVM体系结构概览
运行数据区包括方法区、堆、本地方法栈、java 栈、程序计数器这五个部分。其中,(1)方法区用于保存类的元结构信息(保存所有定义的方法的信息,以及类中定义的静态变量、常量、运行时常量池等)、(2)堆内存中存放实例变量,(3)本地方法栈用于处理标记为 native 的代码,(4)程序计数器作为一个指针(指向方法区中的方法字节码,用于指示下一个要执行的指令代码是什么,告诉执行引擎下一条要执行的指令是什么),(5)栈内存是各线程私有的,生命周期为:在线程创建时创建,线程结束时释放。基本类型的变量、引用类型变量、实例方法都是在栈内存中存放的。
垃圾回收:主要针对方法区和堆内存(有些垃圾回收器不回收方法区内存),两者存在 OOM问题(OutOfMemoryError)
java 栈和本地方法栈不存在垃圾回收,但是存在 StackOverFlow(栈内存溢出问题)
-
Class Loader类加载器
负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,值与他是否可以允许,则由Execution Engine决定
-
Execution Engine执行引擎 负责解释命令,提交操作系统执行
-
Native Interface 本地接口
Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。
-
Native Method Stack 本地方法栈
java在内存中专门开辟了一块区域处理标记为native的代码,他的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。
-
Runtime Data Area 运行数据区
-
Method Area方法区
方法区是被所有线程共享,所有字段和方法字节码、以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,==此区属于共享区间==。用来保存装载的类的元结构信息。
==静态变量+常量+类信息+运行时常量池存放在方法区==
==实例变量存在堆内存中==
-
PC Register 程序计数器
每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码(下一个将要执行的指令代码),有执行引擎读取下一条指令,是一个非常小的内存空间,可以忽略不记
==栈管运行,堆管存储==
-
Java Stack 栈
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。基本类型的变量、实例方法、引用类型变量都是在函数的栈内存中分配
栈管运行,堆管存储
三、栈(Stak)
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。基本类型的变量、实例方法、引用类型变量都是在函数的栈内存中分配
3.1 栈存储什么
先进后出,后进先出即为栈
栈帧中主要保存3类数据
- 本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
- 栈操作(Operand Stack):记录出栈、入栈的操作;
- 栈帧数据(Frame Data):包括类文件、方法等。
3.2 栈运行原理
栈中的数据以栈帧的格式存在,每执行一个方法就创建一个栈帧,将该栈帧压入栈的顶部(栈遵循后入先出,先入后出的原则),当方法执行结束之后就将方法从栈的顶部弹出
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存去块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,
当一个方法A被调用时就产生一个栈帧F1,并被压入到栈中,
A方法调用了B方法,于是产生栈帧F2也被压入到栈,
B方法调用了C方法,于是产生栈帧F3也被压入到栈。。。
执行完毕后,先弹出F3,再弹出F2,再弹出F1.。。。
遵循“先进后出/后进先出”的原则。
图示在一个栈中有两个栈:
栈2是最先被调用的方法,先入栈,
然后方法2调用了方法1,栈帧1处于栈顶的位置,
栈帧2处于栈底,执行完毕后,依次弹出栈帧1和栈帧2,
线程结束,栈释放。
3.3 判断JVM优化是哪里
主要是优化堆(对堆空间进行优化,如对 InitialHeapSize 和 MaxHeapSize 等参数进行调整,当执行 java -XX:PrintCommandLineFlags 时会打印出 JVM 调参常用参数,其中就包含和堆内存大小相关的参数)
3.4 三种JVM
- Sun公司的HotSpot
- BEA公司的JRockit
- IBM公司的 J9 VM
四、堆(Heap)
新生区分为伊甸区和幸存区,所有的类在被创建出来时位于伊甸区,当伊甸区的空间耗尽时,程序又需要创建新对象的话,JVM 的垃圾回收机制就会对伊甸区进行垃圾回收,其中不再被任何 GC Roots 所引用的对象作为垃圾被销毁,而幸存的其他对象则被移到幸存0区;当0区也满了的时候,则再对0区进行垃圾回收,幸存对象移到幸存1区;进而0区也满了的时候,再移动到养老区。最终如果养老区也满了,对养老区执行完垃圾回收后发现仍然无法创建新的对象,这时候就会产生 OOM 异常。
产生 OOM 异常的原因有两点:(1)java 虚拟机对堆内存的设置太小,可以对-Xms 和-Xmx 参数进行设置,从而调整堆初始内存大小(默认为物理内存的16分之1)、和堆最大内存大小(默认为物理内存的64分之1) (2)代码中创建了过多的大对象,并且由于这些对象存在被引用,长时间无法被垃圾回收器清理
永久存储区(实际上为方法区,永久区逻辑上属于堆内存空间)存储 JDK 自带的类、接口的元数据,即存储运行环境所必须的类信息,只有在关闭 JVM 时才会释放此区域所占用的内存。如果出现永久区内存 OOM 的话,一般原因是JVM 要加载过多的第三方 jar 包。
4.1 堆内存示意图
4.2 新生区
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生去又分为两部分:伊甸区(Eden Space)和幸存者区(Survivor Space),所有的类都是再伊甸区被new出来。幸存区有两个:0区和1区。当伊甸园的空间用完是,程序有需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园区中的生于对象移动到幸存0区,若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。如果1区也满了,再移动到养老区。若养老区也满了,那么这时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了FullGC后发现依然无法进行对象保存,就会产生OOM异常(OutOfMemoryError)。
如果出现
java.lang.OutOfMemoryError:Java heap space
异常,说明java虚拟机的堆内存不够。原因有二:
Java虚拟机的对内存设置不够,可以通过参数-Xms、-Xmx来调整
默认最大内存是机器的四分之一大小
代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
==JDK1.8之后,永久代取消了,由元空间取代==
4.3 养老区
养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃。
对象池技术基本原理的核心有两点:缓存和共享,即对于那些被频繁使用的对象,在使用完后,不立即将它们释放,而是将它们缓存起来,以供后续的应用程序重复使用,从而减少创建对象和释放对象的次数,进而改善应用程序的性能。事实上,由于对象池技术将对象限制在一定的数量,也有效地减少了应用程序内存上的开销。
common-pool 提供了一套对象池组件,实现的对象池的步骤如下:
- 实现自己的池化工厂,继承BasePoolableObjectFactory,再其中覆写makeObject 方法(创建池子中的相应对象)、passivateObject方法(其中可能调用对象的一些set方法等,当用完对象还回池子中时,调用这个方法还原对象状态,如修改对象的某些成员变量)
- 在主程序中:(1)创建 ObjectPool (对象池对象),传入自定义池化工厂对象(ObjectPool 接口定义了对对象池操作的方法,如borrowObject方法从自定义池子中取出一个对象) (2)调用对象池对象的borrowObject方法得到一个对象 (3)用得到的对象进行一系列对象方法调用 (4)调用池子的returnObject 方法归还对象
4.4 永久区
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
- 如果出现
java.lang.OutOfMemoryError:PermGen space
,说明是Java虚拟机对永久区Perm内存设置不够,一般出现这种情况,都是程序启动需要加载大量的第三方jar包。例如在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被沾满。- Jdk1.6之前:有永久代,常量值1.6在方法区
- Jdk1.7:有永久代,但已经逐步“去永久代”,常量池1.7在堆
- Jdk1.8之后:无永久代,常量池1.8在元空间
4.5 小总结
逻辑上堆由新生代、养老代、元空间构成、实际上堆只有新生和养老代;方法区就是永久代,永久代是方法区的实现
- 方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的类信息、普通常量、静态常量、编译器编译后的代码等,虽然JVM规范将方法去描述为堆的一个逻辑部分,但他却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
- 对于HotSpot虚拟机,很多开发者习惯将方法区成为“永久代”,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于一个接口Interface)的一个实现,JDK1.7的版本中,已经将原本放在永久代的字符串常量池移走。
- 常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放
常量池
可以分为静态常量池和运行时常量池,静态常量池指的是 class 字节码文件中(主要存放字面量和引用符号,其中字面量包括 :final常量值和文本字符串,引用符号则包含三种:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符),运行时常量池是静态常量池中的类或接口常量的运行时表示形式,会在 JVM 对 class 文件进行加载时,对class 文件中的常量信息进行加载(并根据静态常量池中的对类和接口符号引用,将相应类的 class 文件加载到方法区内存空间中,并将原本静态常量池中的符号引用替换为直接引用),每一个 class 文件都对应拥有一个自己的运行时常量池。
常量池:
Java 中静态/运行时常量池并非特指保存 final 常量,它还保存诸如字面量、类和接口全限定名、字段、方法名称、修饰符等永恒不变的东西。
一个 Java 程序启动时加载了众多的类,有JDK的,也有我们自己定义的,那么我们怎么在程序运行的时候准确定位到类的位置呢?比如 String str = new String("xxx"),我们怎么在虚拟机内存中找到 String 这个类的定义(或者说类的字节码)呢?
答案就在常量池的符号引用中。在未加载到JVM的时候,在 .class 文件的静态常量池中我们可以找到这么一项 CONSTANT_Class(常量表类型),当然这一项仅仅只是符号引用,我们只知道有 java.lang.String 这么一个类。只有等 JVM 启动,并判断程序用到 java.lang.String 的时候才会加载 String 的 .class 文件到内存中(准确地说是方法区),之后,我们就可以在运行时常量池中将原本的符号引用替换为直接引用了。也就是说实际上我们的定位是依靠运行时常量池的,这也就是为什么运行时常量池对于动态加载非常重要的原因。
字符串池:
在 JDK 1.6 以及以前的版本中,字符串池是放在 Perm 区(Permanent Generation,永久代)。Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,容量是固定的,默认在 32 M 到 96 M 间,我们可以通过 -XX:MaxPermSize = N 来配置永久代的大小,但是在运行过程中它仍然还是固定大小的。也有说 Perm 区实际上就是 HotSpot 下的方法区,HotSpot 的开发人员更愿意将方法区称为 Permanent Generation,这里我们不做过多的探讨
在 JDK 1.7 的版本中,字符串池移到Java Heap。在 JDK 1.8 中永久代的说法被废弃,元空间成为方法区的替代品。(本文 5.1 章节补充关于为什么永久代被废弃).
字符串池的实现——StringTable,String 类中提及缓存/池的概念只有intern() 这个方法(可以将字符串加入字符串池,若字符串池已经存在,则将原引用指向池中的字符串),字符串池(String pool)实际上是一个 HashTable
native方法(intern是 native 方法)
native方法,本身并不是由 Java 语言实现的,而是通过 jni (Java Native Interface)调用了其他语言(如C/C++)实现的一些外部方法,StringTable 的 intern() 方法跟 Java 中的 HashMap 的实现是差不多的。
五、JVM垃圾收集(Java Garbage Collection)
5.1 堆内存调优简介
-Xms | 设置初始分配大小,默认为物理内存的“1/64” |
---|---|
-Xmx | 最大分配内存,默认为物理内存的“1/4” |
-XX:+PrintGCDetails | 输出详细的GC处理日志 |
七、GC三大算法
GC 三大算法为:复制算法、标记清除算法、标记整理算法。
当新对象创建时,如果新生代空间不足,则会触发普通 GC(MinorGC),其回收策略称为复制;如果老年代空间不足(老年代空间一般比较大)则会触发全局 GC(FullGC),其回收策略成为整理压缩。频繁收集新生(young)区,较少收集老年(old)区, 基本不动 永久(Perm) 区
普通 GC:复制算法的垃圾回收机制主要包括复制、清空、互换三个部分,首先将新生代分为伊甸区和幸存区(幸存区分为两部分,只有一部分作为内存使用,始终保持另一部分为空内存),其中一块空的幸存区称为 to 区,另一块幸存区和伊甸区构成 From 区,新生代中在一次GC中存活下来的所有对象会被移动到to 区(对象的 age 设为1),然后原来的 From 区会被清空,这时两个幸存区交换身份,即原本为 from 区的幸存区转为 to 区,剩下的幸存区和伊甸区构成新的 From 区,当再次进行GC 的时候,复制算法重新执行上述操作,交换的对象的 age 每次增加1。当对象的 age 增加到15时,这些对象被移动到老年区(称为老年代)。
全局 GC:标记清除算法、标记整理算法的垃圾回收机制称为全局垃圾回收,均是一次扫描整个内存区域,对仍然存活的对象进行标记,然后对未被标记对象进行清除,但标记整理算法会对存活的引用进行整理,保证清理出来的空间是连续的。
复制算法和标记整理算法的内存整齐度更高(因为标记清除算法清理出来的空闲内存空间不是连续的),但复制算法的内存利用率低(因为浪费了一半的幸存区)
7.1 GC算法总体概述
JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。
Java中的GC回有两种回收:年轻带的MinorGC,老年代的FullGC;新对象创建时如果伊甸园空间不足会触发MinorGC,如果此时老年代的内存空间不足会触发FullGC,如果空间都不足抛出OutOfMemoryError。
因此GC按照回收的区域又分了两种类型,一种是普通GC(MinorGC),一种是全局GC(FullGC)
-
普通GC:只针对新生代区域的GC
-
全局GC:针对年老代的GC,偶尔伴随对新生代的GC以及对永久代的GC。
7.2 复制算法:MinorGC(普通GC)
新生代使用的MinorGC,这种GC算法采用的是复制算法(Copying),频繁使用
复制-->清空-->互换
7.2.1 原理
MinorGC会把Eden中的所有活着的对象都移到Survivor区域中,如果Survivor区中放不下,那么剩下的活的对象就被移到Old Generation中,也即一旦GC后,Eden区就变成空的了。
当对象在Eden(包括一个Survivor区域,这里假设是from区域)出生后,在经过一次MinorGC后,如果对象还存活,并且能够被另外一块Survivor区域所容纳(上面已经假设为from区域,这里应为to区域,即to区域有足够的内存空间来存储Eden和from区域中存活的对象),则使用复制算法将这些仍然还存活的对象复制到另外一块Survivor区域(即to区)中,然后清理所有使用过的Eden以及Survivor区域(即from区),并且讲这些对象的年龄设置为1,以后对象在Survivor区每熬过一次MinorGC,就将对象的年龄+1,当对象的年龄达到某个值时(默认15,通过-XX:MaxTenuringThreshold
来设定参数),这些对象就会成为老年代。
==-XX:MaxTenuringThreshold设置对象在新生代中存活的次数==
7.2.2 解释
HotSpot JVM把年轻代分为了三部分:1个Eden区和两个Survivor区,默认比例是8:1:1,一般情况下,新创建的对象都会被分配到Eden区,这些对象经过第一次的MinorGC后,如果仍然存活,将会被移到Survivor区。对象Survivor区中每熬过一次MinorGC,年龄就增加一岁,当他的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
==复制要交换,谁空谁是to==
7.3.3 劣势
复制算法弥补了标记清除算法中,内存布局混乱的缺点。
- 浪费了一半的内存,太要命了
- 如果对象的存活率很高,我们可以极端一点,假设是100%存活率,那么我们需要将所有对象都复制一遍,并将所有引用地址重置一遍。复制这一工作所花费的时间,在对象存活率达到一定程度是,将会变的不可忽视。所以从以上描述不难看出,复制算法想要使用,最起码对象的存活率要非常低才行,而且最重要的是,我们必须要客服50%的内存的浪费
7.3 标记清除/标记整理算法:FullGC又叫MajorGC(全局GC)
老年代一般是由标记清除或者是标记清除与标记整理的混合实现
7.3.1 标记清除(Mark-Sweep)
7.3.1.1 原理
-
标记(mark)
从根集合开始扫描,对存活的对象进行标记
-
清除(Sweep)
扫描整个内存空间,回收未被标记的对象,使用free-list记录可以区域。
7.3.1.2 劣势
- 效率低(递归与全堆对象遍历),而且在进行GC的时候,需要停止应用程序,这会导致用户体验非常差劲
- 清理出来的空闲内存不是连续的,我们的死亡对象都是随机的出现在内存的各个角落,限制把他们清除之后,内存的布局自然会乱七八糟,而为了应付这一点,JVM不得不维持一个内存的空闲列表,这又是一种开销,而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
7.3.2 标记整理(Mark-Compact)
7.3.2.1 原理
-
标记
与标记-清除一样
-
压缩整理
再次扫描,并往一段滑动存活对象
7.3.2.2 劣势
效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上说,效率要低于复制算法
7.4 小总结
- 内存效率:复制算法>标记清除算法>标记整理算法
- 内存整齐度:复制算法=标记整理算法>标记清除算法
- 内存利用率:标记整理算法=标记清除算法>复制算法
分代收集算法
引用计数法:
- 缺点:每次对对象赋值时均要维护引用计数器,且计数器本身也有一定的消耗
- 较难处理循环引用
二、JVM 参数
JVM的参数类型:
标配参数
- -version
java -version
- -help
X参数(了解)
- -Xint:解释执行
- -Xcomp:第一次使用就编译成本地代码
- -Xmixed:混合模式
XX参数(下一节)
JVM的XX参数之布尔类型
公式:-XX:+ 或者 - 某个属性值(+表示开启,-表示关闭)
如何查看一个正在运行中的java程序,它的某个jvm参数是否开启?具体值是多少?
jps -l //查看一个正在运行中的java程序,得到Java程序号。
jinfo -flag PrintGCDetails (Java程序号 ) //查看它的某个jvm参数(如PrintGCDetails )是否开启。
jinfo -flags (Java程序号 ) //查看它的所有jvm参数通过上述步骤,可以查看某个 正在运行的java程序的某个 JVM 参数的当前值是多少,如果不满意(需要调整),则可以在运行时修改 Edit Configuration 中的 VM Options ,在其中修改布尔类型或者设值类型的 JVM 参数(IDEA 环境下),修改完成之后再调用 jinfo 命令查看该参数,则会发现其值发生变化(如,可以将MetaSpaceSize 从大概22m,修改到128m)
是否打印GC收集细节
-XX:-PrintGCDetails
-XX:+PrintGCDetails
是否使用串行垃圾回收器-XX:-UseSerialGC
-XX:+UserSerialGC
JVM的XX参数之设值类型
公式:-XX:属性key=属性值value
Case
- -XX:MetaspaceSize=128m //方法区中的元数据空间大小
- -XX:MaxTenuringThreshold=15 //到多大的年龄, young区中的可以升级到 old 区
JVM的XX参数之XmsXmx坑题
两个经典XX参数:(JVM 在进行加载的时候,根据物理内存的大小进行值的变更)
- -Xms等价于-XX:InitialHeapSize,初始大小内存,默认物理内存1/64
- -Xmx等价于-XX:MaxHeapSize,最大分配内存,默认为物理内存1/4
设值方式:-Xms100m(没有+-=号,但是仍然是-XX 参数,只不过因为经常使用,起了一个别名)
JVM盘点家底查看初始默认值
在进行 JVM 调优的时候,应该利用该命令对 JVM 参数的初始值进行查看
查看初始默认的参数值 -XX:+PrintFlagsInitial 公式:java -XX:+PrintFlagsInitial
后面可以带上-version 也可以不加
查看修改更新后参数值 -XX:+PrintFlagsFinal 公式:java -XX:+PrintFlagsFinal 命令返回的结果中
=表示默认,:=表示修改过的(如,由于-Xms 和-Xmx 参数是在 JVM 加载时重新设值的,因此其形式为:=)。
C:\Users\abc>java -XX:+PrintFlagsFinal
...
size_t HeapBaseMinAddress = 2147483648 {pd product} {default}
bool HeapDumpAfterFullGC = false {manageable} {default}
bool HeapDumpBeforeFullGC = false {manageable} {default}
bool HeapDumpOnOutOfMemoryError = false {manageable} {default}
ccstr HeapDumpPath = {manageable} {default}
uintx HeapFirstMaximumCompactionCount = 3 {product} {default}
uintx HeapMaximumCompactionInterval = 20 {product} {default}
uintx HeapSearchSteps = 3 {product} {default}
size_t HeapSizePerGCThread = 43620760 {product} {default}
bool IgnoreEmptyClassPaths = false {product} {default}
bool IgnoreUnrecognizedVMOptions = false {product} {default}
uintx IncreaseFirstTierCompileThresholdAt = 50 {product} {default}
bool IncrementalInline = true {C2 product} {default}
size_t InitialBootClassLoaderMetaspaceSize = 4194304 {product} {default}
uintx InitialCodeCacheSize = 2555904 {pd product} {default}
size_t InitialHeapSize := 268435456 {product} {ergonomic}
...
JVM盘点家底查看修改变更值
运行java命令的同时打印出参数
java
-XX:+PrintFlagsFinal -XX:MetaspaceSize=512m
HelloWorld 会打印出修改后的JVM 参数(该命令行中对 JVM 参数的修改生效)以及程序返回值
打印命令行参数
-XX:+PrintCommandLineFlags 会打印出常见的JVM参数(如InitialHeapSize、MaxHeapSize、默认的垃圾回收器等)