虚拟机构成
JVM优化
程序计数器(Program Counter Register)(pc寄存器)
正在执行的是java方法,pc寄存器存的则是正在执行的编译好的字节码的指令的地址;
每个线程都有一个独立的pc寄存器,各线程之间pc寄存器互不影响,独立存储,pc寄存器是“线程私有”的内存。
既然是存放的指令的地址,指令有两种类型:一个是程序员手写的java方法,另一个是native方法;
正在执行的是native方法,pc寄存器为空。
仅从功能上来说,jvm中的pc寄存器和通用计算机中的pc寄存器含义一致:用于存放下一条指令所在的地址。
当前线程所执行的字节码的行号指示器。通过改变pc寄存器的值,可以选择下一条需要执行的字节码指令。
占用空间较小,与程序员编码无关.格式内容固定.所以是唯一一个不会OOM的区域。
虚拟机栈(VM Stack)
JVM优化
每个方法执行时都会创建一个栈帧(Stack Frame),用于保存局部变量表,操作数栈,动态链接,返回地址等。
每一个方法从调用直至执行完成的过程,对应着一个栈帧在vm stack中入栈到出栈的过程
遇到递归,则当前未执行完的方法入栈,并将递归方法中的方法再次入栈,直到递归结束。
栈都是有深度限制的,若栈帧数或者操作数的数量超过了其栈的允许值,那么就会报StackOverflowError.
而如果虚拟机栈的数量限制可以动态扩展(大部分虚拟机都支持动态扩展),而在扩展时无法申请到足够的内存,则会抛出OutOfMemoryError。
局部变量表
局部变量表就是用于存放编译期间(注意是编译期间)各种变量的值(基本类型变量)和引用对象地址还有returnAddress的空间。
局部变量表的容量以变量槽(Slot)为最小单位,32位虚拟机中一个Slot可以存放一个32位以内的数据类型(boolean、byte、char、short、int、float、reference和returnAddress八种)。
引用类型(String,对象)是保存了一个该引用的地址,而对象的实际数据是在堆(Heap)中
Slot对对象的引用会影响GC(要是被引用,将不会被回收)。
系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。
操作数栈
操作数栈中保存的也是数据,但不同的是,操作数栈的作用是实际完成函数中的具体操作
javap -c ClassName
JVM优化
先是两个操作数入栈(a和b),然后出栈进行运算,运算结果再入栈,最后结果保存到局部变量表的变量c中。
虚拟机栈也是线程私有的,其生命周期和线程相同。
通过标准的栈操作—压栈和出栈—来访问的。
虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如int、long、float、double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前,也会被转换为int。
JVM优化
JVM优化
动态链接
动态链接即表明该栈帧属于哪一个函数。
函数本身不在虚拟机栈中,因为函数是可以多次被调用的.实际上函数是保存在方法区中的运行时常量池中的
方法返回地址
就是该方法返回时返回到先前调用该方法的地址的地方,以便程序能接着执行。
还有一个就是returnAddress。returnAddress类型是为字节码指令jsr、jsr_w和ret服务的,它指向了一条字节码指令的地址。
本地方法栈(Native method stack)
和虚拟机栈类似,不过该部针对的不是java方法,而是native方法。
堆(Heap)
几乎所有的对象实例都在堆上分配,即堆空间的唯一目的就是用于存放对象。
The heap is the runtime data area from which memory for all instances and arrays is allocated.
堆是垃圾回收作用的空间。
堆的大小都是可动态扩展的(通过-Xmx和-Xms来控制),如果在堆中没有内存完成实例分配,并且堆无法再扩展时,会抛出OutOfMemoryError。
线程共享
方法区(Method Area)
线程共享的区域
方法区是保存类的信息。
类加载器(ClassLoader)首先定位到这个类的.class文件,然后读取这个文件(IO操作),jvm提取类信息,其存到方法区中。那么下一次在需要创建该类的对象时,就从方法区中取该类的信息。
方法区的大小可以可以动态调整的,当无法再扩展时就抛出OutOfMemoryError.
当方法区中的某各类不在被需要(不可达时),该类的信息就被卸载,即被垃圾回收。
方法区保存的类的结构信息: 运行时常量,类的类型(Class or Interface),访问修饰符(public private等)等。
运行时常量池(Runtime Constant Pool),用于存放字段、方法信息、静态变量等。
Jdk1.6及之前:常量池分配在永久代 。
Jdk1.7:有,但已经逐步“去永久代” 。
Jdk1.8及之后:无永久代,改用元空间(MetaSpace)代替(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。
对象加载
对象创建内存分配方式
指针碰撞
当内存中所有被使用的内存在内存的某一部分,未使用的放在另一部分,那么分配新内存时只需要移动一下未分配的内存和已分配内存的的临界指针。
单纯的移动指针方法无疑会浪费内存,因此还需要额外的压缩整理功能
空闲列表
维护一个表,表上记录那些区域是空闲的,这意味着,可能存在一个对象被分配两个不连续的内存空间的情况。
分配内存解决线程安全问题
线程同步
本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)
预先为每个线程在java堆中分配一小块内存,这块内存是线程私有的
生成对象所需的空间就由TLAB提供。只有当TLAB用完时,再加上同步锁从堆上划分。
初始化
对象的内存被分配完成后,虚拟机需要将分配到的内存空间都初始化为零值
这保证了对象的实例字段可以在不赋初始值就直接使用。
紧接着,jvm就会执行对应的初始化方法(构造方法)
对象内存结构
对象真正存储的可以被程序员操作的字段数据。(成员变量)
对象起始位必须是8字节整数倍.不够需要填充
JVM优化
对象头(Header)
JVM优化
对象头中包含MarkWord和ClassMetadataAddress.如果是数组,还会包含第三部分,数组长度
实例数据(Instance Data)
填充(Padding)
Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构
JVM优化
锁状态 25bit 4bit 1bit是否是偏向锁 2bit 锁标志位
无锁状态 对象HashCode 对象分代年龄 0 01
由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:
JVM优化

垃圾回收
原则
堆里存放着java几乎所有的对象实例,而对象只有不再被使用时,才会被回收
判定垃圾方式
引用计数法
给每个对象添加一个引用计数器,当该对象被引用一次时,该对象的计数器就加一
引用失效时就减一当为0时,意味着不可能再被赋予其他引用了
该方法最大的问题在于无法解决相互循环引用。
可达性分析
将对象被引用看做一个图,从堆中GCROOT出发,看能不能通过图的遍历找到一个变量
使用的遍历方法可能是深度优先遍历或者广度优先遍历
若无法找到一个变量引用则说明该对象是不可用的
GC Roots:
虚拟机栈(一般来说应该是局部变量表)中引用的对象
方法区中类静态属性或常量引用的对象
本地方法区(native)引用的对象
引用的4种类型
强引用(Strong Reference):强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
软引用(Soft Reference) :如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用(Weak Reference) :弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象. 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
虚引用(Phantom Reference) :“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
回收作用域
程序计数器、虚拟机栈、本地方法栈这三个区域随线程而生,随线程而灭,因此不需要回收
方法区,需要回收的主要是两部分内容:废弃常量和无用的类。
该类所有的实例都被回收
加载该类的ClassLoader已经被回收
该类对应的java.lang.Class对象(反射)没有在任何地方被引用,且无法在任何地方通过反射访问该类的方法。
是否对类进行回收,是通过jvm的参数控制的,比如-Xnoclassgc等参数控制,因此理、CGLib等框架、动态生成JSP时需要jvm开启类卸载的功能,以防止溢出。
堆内存回收
该对象不可达,则被第一次标记,如果该对象没有覆盖finalize()方法或者该方法已被执行过,则该对象就等待被回收
该对象执行finalize()方法后又被引用了,则逃脱被回收,否则被再次标记,下一次被就会被回收。
finalize()方法只被执行一次,且在System.gc()方法时执行
垃圾回收策略(算法)
与标记清除相比,第一阶段仍是标记,不同之处在于第二阶段,不是直接对对象进行回收,而是让所有存活的对象都向一端移动。
标记-清除Mark-Sweep
分为两个阶段:即“标记”和“清除”阶段。首先标记处需要回收的对象(使用可达性分析),标记完一回收。
效率低,尤其是清除阶段,需要针对每个标记逐一清除
空间不连续,产生内存碎片,导致分配较大对象时找不到合适的空间 ,执行full GC
复制算法
将内存空间分为两块,其中一块用于存放内存,另一块空着
分配对象空间时,按序分配,当其中一块用完时,触动垃圾回收,标记出需要回收的对象和不需要回收的对象
将满的那一块上仍存活对象按序移到另一块空的上,满的那一块则腾出来了。
问题是可用内存缩小了一半
解决思路:将内存分为三块,一块较大的eden空间和两块survivor空间,每次分配时使用eden和一块survivor,当回收时,将eden和survivor上的存活对象移到另一块survivor上,然后清理掉eden和一块survivor上的空间。如果遇到survivor上放不下的情况,则这些对象将被迁移到老年代
标记-整理Mark-Compact
分代收集算法Generational Collection
其主旨是将对象进行分代管理,分为“新生代”和“老年代”,分代的依据是对象存活的周期次数。
对象经过多次垃圾回收后仍存活,则将其移到老年代。同时,新生代和老年代采用不同的垃圾收集算法。
新生代大部分对象都是朝生夕死,因此新生代采取的垃圾回收算法是复制算法.同时老年代也需要进行垃圾回收,由于老年代的对象存活率高,因此,常采用标记-整理或标记清除算法。
OopMap
可达性分析选择GCROOT:不就是全局性的引用(例如常量或者类静态属性),栈帧中的局部变量表吗?然后实际情况是,方法区就有数百兆,如何逐一检查每个引用,必然会消耗很多时间。
一种较好的方法是使用一个表来记录这些GC Roots。 jvm使用一组称为OopMap的数据结构来直接得知哪些方法存放着对象的引用。
程序运行期间,引用的变化在不断发生,如果每一条指令都声称OopMap,那占用空间就太大了
在进行可达性分析时,我们需要注意到其他的java线程仍是在执行的,这会导致可达性分析的误差,于是不得不暂停所有的java线程(一般称为Stop The World). STW
所以有了安全点(Safe Point)。只在安全点进行GC停顿,只要保证引用变化的记录完成于GC停顿之前就可以
抢先式中断:GC发生时,中断所有线程,如果发现有线程不再安全点上,就恢复线程让它运行到安全点上。现在几乎不用这种方案。

主动式中断:设需要中断线程,此时不是直接对线程操作,而是设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真,就自己主动挂起.HotSpot使用主动式中断。

垃圾回收器种类
JVM优化
Serial
单线程,即只会使用一个CPU或一条收集线程去完成垃圾收集的工作
效率高(无需线程交互).但无法发挥多核优势
采用复制算法
垃圾回收时,必须暂停其他所有线程的工作线程,即所谓的“Stop The World”
使用-XX:+UseSerialGC打开。
jvm在Client模式下,默认的新生代收集器仍然是Serial收集器
Serial Old
实际上Serial Old收集器是Serial的老年代版本
单线程,即只会使用一个CPU或一条收集线程去完成垃圾收集的工作
采用标记-整理法
Serial Old可以与其他三个新生代的垃圾回收器协同作用,故当老年代使用CMS收集器出现故障时(Concurrent Mode Failure),可以作为CMS的后备选择。
ParNew
其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾回收之外,其他甚至包括控制参数(-XX:SurvivorRatio)都与Serial一样,即也需要停止所有用户线程,采用复制算法等。
除了Serial,只有ParNew能与CMS协同作用,因此在jvm的Server模式下,ParNew是老年代的首选垃圾收集器。
Parallel Scavenge
和ParNew相比Parallel Scavenge可控制吞吐量(Throughput)—吐吞量最大化,其他完全一样.包括采用复制算法
最大垃圾回收暂停:指定垃圾回收时的最大暂停时间,通过-XX:MaxGCPauseMillis=指定。为毫秒数,如果指定了这个值的话,堆大小和垃圾回收相关参数会进行调整以达到指定值。此值可能会减少应用的吞吐量。
吞吐量:吞吐量为垃圾回收时间与非垃圾回收时间的比值。通过-XX:GCTimeRatio=来设定,公式为1/(1+N),例如,-XX:GCTimeRatio=19时,表示5%的时间用于垃圾回收,默认情况为99,既1%的时间用于垃圾回收。
使用-XX:ParallelGCThreads=设置并行垃圾回收的线程数,此值可以设置与机器处理数量相等。
Parallel Old
Parallel Scavenge的老年代版本收集器,只能和Parallel Scavenge配合使用,使用“标记-整理”算法。
该收集器就是针对Parallel Scavenge的,因为Parallel Scavenge除了Parallel Old外,就只能和Serial Old配合使用
这个组合常用于注重吞吐量以及CPU资源敏感的场合。
CMS (Concurrent Mark Sweep)
最多占用n/4个线程
-XX:+UseConcMarkSweepGC
从名字就可以看出是并发和采用标记-清除算法的垃圾收集器
分4阶段进行工作
JVM优化
初始标记(CMS initial mark) 这一阶段仍然需要Stop The World,该阶段的任务仅仅是标记一下GC Roots能直接关联到的对象。 我们知道GC Roots是使用OopMap来获取的,这说明这一阶段(初始标记)的速度是很快的。
并发标记(CMS concurrent mark) 这一阶段是GC Roots后的延伸,即找出GC Roots能关联到的对象,即GC Roots Tracing的过程。 该阶段是和用户线程并发执行的
重新标记(CMS remark) 由于上一阶段并发标记是并发的,这意味着在进行GC Roots Tracing时,用户进程仍会改变已标记的对象的状态。故该阶段重新标记也是Stop The World,是为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分,该阶段的时间比初始标记略长,但比并发标记时间短。
并发清除(CMS concurrent sweep) 和用户程序一起运行。清除上述标记的垃圾。
CMS仍有如下缺点
对CPU资源非常敏感 => 垃圾收集线程和用户线程竞争资源,当CPU数较高时,才建议使用CMS.
无法有效处理浮动垃圾 => 堆上需要预留空间给用户线程使用,预留的空间无法满足程序的需要,会出现“Concurrent Mode Failure”.此时CMS垃圾收集器失效,将会启用Serial Old。XX:CMSinitiatingOccupancyFraction=指定还有多少剩余堆时开始执行并发收集。
垃圾碎片 =>太多的空间碎片会导致内存分布太散,无法容纳大对象,当无法容纳大对象时,就会触发一次Full GC,会在Full GC发生前启用一次内存整理合并。
G1 (Garbage-First)
-XX:+UseG1GC -XX:MaxGCPauseMillis -Xmx3550m
G1既可以作用于新生代又可以作用于老年代
对于G1来说,java堆被划分为多个大小相等的独立区域(Region,默认2048个),并跟踪每个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收时所需要的经验值),在后台维护一个优先列表,并根据允许的收集时间,优先回收价值最大的Region。
G1的垃圾回收步骤
JVM优化
初始标记(Initial Marking) 和CMS的第一阶段一样,仅仅是标记GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一阶段的用户在并发执行时,可以正确的创建对象(这也说明Region内是采用标记-整理算法)。同样是需要Stop The World
并发标记(Concurrent Marking) 同CMS的并发标记,耗时较长,但可以与用户线程并发执行。
最终标记(Final Marking) 同样是为了修正在并发标记期间引起的变动。不同的是,jvm将对象变化记录在线程Remembered Set Logs里面,该阶段需要把Remembered Set Logs的数据合并到Remembered Set中,该阶段是需要Stop The World的,但垃圾回收线程却可以并行执行。
筛选回收(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据用户所希望的GC停顿时间来执行回收计划。
G1的特点
并行与并发
分代收集
空间整合
可配置停顿
附录,切换垃圾回收器命令
-XX:+UseSerialGC
-XX:+UseParallel
-XX:+UseParallelOldGC
-XX:+UseParNewGC
-XX:+UseParalledlOldGC
-XX:+UseConcMackSweepGC
-XX:+UseG1GC
内存分配与回收
大多数情况下,对象优先在新生代的Eden上分配,当Eden区没有足够空间时,jvm发动一次Minor GC。如果Survivor无法存放存活的对象,则将对象移入老年代。
其中Minor GC是指新生代GC,与之相对的,Full GC是指老年代GC.
大对象直接进老年代jvm提供一个设置-XX:PretenureSizeThreshold ,令占用内存大于这个值的对象直接进入老年代,以防新生代GC时的复制移动。
长存活对象进入老年代 jvm另提供设置-XX:MaxTenuringThreshold,默认值是15 来使得存活过多次的对象进入老年代,每一次Minor GC后,存活的对象计数加1,超过阈值后,则进入老年代。
另外jvm提供动态判定的机制:如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以进入老年代
空间分配担保 在发生Minor GC时,只要老年代的连续空间大于新生代对象总大小或者历次晋升到老年代的平均大小,那么就进行Minor GC,否则进行Full GC。
JVM工具

JPS
java process status
本地虚拟机唯一ID
类似于Linux中的ps
-l 可以看到类详情
-m 可查看启动参数
-v 可查看vm参数
Jstat
类装载,内存,垃圾收集,jit编译信息
根据jps查看进程号指定
-gcutil 垃圾回收信息
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
jinfo
jinfo -flag UseG1GC PID : 查看是否使用指定垃圾回收器
jinfo -sysprops 查看系统属性(未必支持)
jmap
jmap -dump:format=b,file=d:\a.hprof 生成快照
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\gc.hprof 同上,当OOM时到处快照到指定位置
eclipse memory analyzer查看
jhat
Jvm堆分析工具
功能同eclipse memory analyzer
jhat 文件名
jstack
栈(线程)快照查看
-f 强制打印
-m 打印java与本地方法
-l 添加监听,打印更多信息,包括锁信息
可在代码中调用:Thread.getAllStackTraces()达到同样效果
JConsole
可视化监控工具
监控内存
监控线程
监控死锁
visualvm(第三方,已经整合到java/bin目录)
https://visualvm.github.io/download.html
jvisualvm
javap
javap -c class文件路径 可对class进行反汇编,查看JVM指令
虚拟机调优
打印GC日志
-XX:+PrintGCDetailis -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log
堆设置
-Xmx3550m:设置JVM最大堆内存为3550M。
-Xms3550m:设置JVM初始堆内存为3550M。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-XX:MetaspaceSize和-XX:MaxMetaspaceSize=xxm 调整元空间大小.对应的-XX:PermSize及-XX:MaxPermSize 1.8之后(持久带被元空间代替)被废弃.
-Xss128k:设置每个线程的栈大小。JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K。应当根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
-Xmn2g:设置堆内存年轻代大小为2G。整个堆内存大小=年轻代大小+年老代大小+持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-XX:MaxNewSize=size:新生成的对象能占用内存的最大值。
-XX:NewRatio=4:设置堆内存年轻代(包括Eden和两个Survivor区)与堆内存年老代的比值(除去持久代)。设置为4,则年轻代所占与年老代所占的比值为1:4。
-XX:SurvivorRatio=4:设置堆内存年轻代中Eden区与Survivor区大小的比值。设置为4,则两个Survivor区(JVM堆内存年轻代中默认有2个Survivor区)与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6。
-XX:MaxTenuringThreshold=7:表示一个对象如果在救助空间(Survivor区)移动7次还没有被回收就放入年老代。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代,对于年老代比较多的应用,这样做可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代存活时间,增加对象在年轻代即被回收的概率。

OutOfMemoryError

java.lang.OutOfMemoryError: Java heap space… => 创建了过多对象
java.lang.OutOfMemoryError: unable to create new native thread => 创建了过多线程
java.lang.OutOfMemoryError: PermGen space => 常量池溢出(JDK1.6)|类信息溢出(JDK1.7)
java.lang.OutOfMemoryError: Java heap space => jdk1.8

相关文章:

  • 2022-12-23
  • 2021-05-16
  • 2021-11-03
  • 2021-04-03
猜你喜欢
  • 2022-01-22
  • 2021-05-03
相关资源
相似解决方案