混乱的jvm

Posted 聪明鱼聪明故事

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了混乱的jvm相关的知识,希望对你有一定的参考价值。

这篇是关于jvm的知识点,但是jvm知识点实在太多太杂,我尝试整理了一些重要知识点,如果有问题欢迎指出,本篇仅作为个人笔记


前置知识:

java方法和本地方法的区别:java方法是由由java语言编写,编译成字节码,存储在class文件中的。java方法是与平台无关的。本地方法是由其他语言(如C、C++ 或其他汇编语言)编写,编译成和处理器相关的代码。本地方法保存在动态连接库中,格式是各个平台专用的,运行中的java程序调用本地方法时,虚拟机装载包含这个本地方法的动态库,并调用这个方法。

jvm,jdk,jre

区别:jvm是java虚拟机,jre是java类库api中的javase子集+jvm,jdk是java语言+jvm+java类库

所以jdk包含jre,jre包含jvm

jvm的总体结构

jvm由四部分组成

类加载器运行时数据区执行引擎垃圾收集

jvm的中不论在哪个部分都遵循栈管运行,堆管储存

类的生命周期


其中解析并不是必须在初始化之前执行,这是为了支持java语言运行时的动态绑定特性

类加载时机

有且仅有以下六种情况下必须立即对类进行初始化

1.遇到以下字节码指令:new,getstatic(获取静态字段),putstatic(设置静态字段),invokestatic(调用静态方法)

2.对未初始化的类反射

3.初始化类时没有初始化其父类时要初始化其父类(这里加载的是父类,仅在父类加载这一时刻上如果子类是在引用父类的静态字段的话,子类是不会同时被初始化的!!)

4.jvm启动时初始化主类

5.jdk7的动态语言支持(四种方法句柄)

6.jdk8中的default关键字修饰的接口方法的实现类如果被初始化,则这个接口要在其之前初始化

另外注意,一个数组被new的时候初始化的不是这个数组的类型,而是数组类本身,比如new int[],初始化的对象是[int类型而不是int类型

如果遇到某个类型的静态字段被引用,则不会初始化这个类,因为静态字段本质上是被统一存放在NotInitialization的常量池中的

接口初始化时不要求其父类初始化,只有其父接口被使用时才会初始化

类加载过程(加载->验证->准备->解析->初始化)

1.加载:需要完成三件事

1.通过一个类的全限定名(包名 + 类型名)来获取定义此类的二进制字节流2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口注意:二进制字节流不需要必须从某个class文件中获取数组本身不通过类加载器创建,是由java虚拟机直接在内存中动态构造的,但是元素类型还是要靠加载器完成加载,并且数组的可访问性和它的元素可访问性一致

2.验证:确保class文件的字节流中包含的信息是否符合java虚拟机规范

1.文件格式验证:验证字节流是否符合class文件规范2.元数据验证:验证字节码描述的信息是否符合java语言规范,就是验证有没有语法错误3.字节码验证:代码逻辑验证,但是由于代码逻辑这东西很难验证,所以字节码验证通过了也不代表没问题,一般采用StackMapTable存放当前方法体内的所有基本块的本地变量表和操作栈状态,这样可以只检查StackMapTable中的属性是否合法4.符号引用验证:验证该类是否缺少或不能访问它所依赖的外部方法,类,字段

3.准备

该阶段为静态变量和类变量分配内存并初始化,需要注意初始化的时候只包括类变量不包括实例变量,并且初始值是默认值,而不是赋值,比如

public static int value = 123;

这里的value实际上在准备阶段的初始值是0,不是123

4.解析

将常量池内的符号引用换成直接引用的过程

注意:对同一个符号引用的解析允许重复多次,但是第一次解析会将结果缓存,也就是说,如果第一次解析成功的话那么之后的解析都将成功,如果第一次解析失败的话之后的解析都将失败,哪怕之后某一次解析实际上成功了也没用

解析的同时也会对字段和方法的可访问性进行检查

以上几点在对invokedynamic指令解析的时候不成立,因为该指令本身就是用于动态语言支持的,也就是说该指令必须要等待程序实际运行的时候才进行解析,这就是之前提到的解析不一定在初始化之前执行的情况了

解析一般包括:

1.对类或接口的解析2.对字段的解析3.对方法的解析4.对接口方法的解析

5.初始化

该阶段将类中的java程序代码主权从虚拟机交给应用程序

该阶段真正执行赋值,等同于执行类构造器<clinit>()方法的过程

关于<clinit>():

1.由javac编译器自动生成2.和类的构造函数不同,它保证子类的<clinit>()在父类的<clinit>()后执行(接口除外),该方法主要是对静态语句块和赋值行为进行处理3.该方法线程安全,加锁同步

类加载器

1.类加载器是用于判断两个类是否相等的前提,只有两个相同的类由相同的类加载器加载,两个类相等

2.类加载器的类型:

类加载器全部继承自抽象类java.lang.ClassLoader

1.启动类加载器:由c++实现,是虚拟机的一部分,加载 lib目录下或被参数-Xbootclasspath指定的目录下的类库 2.其他类型加载器:由java实现,独立在虚拟机之外扩展类加载器:ExtClassLoader实现,负责加载 libext目录中被java.ext.dirs所指定的路径中的所有类库 应用程序类加载器:AppclassLoader实现,,是ClassLoader类中getSystemClassLoader()方法的返回值,负责加载用户路径(classpath)上的所有类库,可以在代码中直接使用,如果应用程序中没有定义过,则这了就是程序中默认的类加载器

3.双亲委派模型

混乱的jvm

双亲委派模型每次在类加载器接到任务的时候都会想父类传送,如果父类处理不了才自己查找后加载,这样的好处就是可以保证类加载器的一致性,即相同的类总是会被相同的类加载器加载

4.破坏双亲委派模型

1.第一次被破坏是因为jdk1.2之前没有双亲委派模型,所以很多自定义类加载器的代码没办法保证类加载器的一致性,解决方法就是在ClassLoader中添加一个findClass方法,让用户自己编写类加载逻辑2.第二次破坏是由于双亲委派模型不能让父加载器加载子加载器才能访问的类库,所以使用了线程上下文类来解决,类似允许双亲委派模型逆向使用类加载器3.第三次破坏是为了能够让代码热替换,使用了OSGi提案实现代码热替换,将类加载器模型变成网状结构,破坏了双亲委派模型的结构

运行时数据区域

混乱的jvm


程序计数器

1.用作表示当前线程所执行的字节码的行号指示器

2.程序控制流的指示器,控制循环,分支,异常处理,线程恢复等

3.每个线程的计数器独立

4。如果当前线程执行的是本地方法,则计数器直接为空

5.此区域没有OutOfMemoryError错误

虚拟机栈

1.线程私有,为java方法(字节码)服务

2.用于存放栈帧

3.关于栈帧

栈帧由局部变量表,操作数栈,动态链接,方法出口入口(参数)等信息组成局部变量表包含(注意是包含,不是存储)了编译期可知的各种java虚拟机基本数据类型(int,boolean这些东西,注意不包含String)和对象引用类型和热土让你Address类型(指向字节码指令的指针)局部变量表中的存储空间单位为slot,运行期间空间大小不会改变,但是slot本身的大小不一定(例如long为64bit,如果slot=32bit则long占两个slot,slot=64bit则long只占一个slot)局部变量表被定义为一个数组,主要用于存储方法参数(形参)和定义在方法体内的局部变量栈帧的本质可以理解成一个方法从调用到调用完毕的全过程

4.如果线程请求的栈深度大于虚拟机允许的深度,抛出StackOverflowError,如果栈容量允许扩展并且内存不足会抛出OutOfMemoryError错误

5.本地方法栈和虚拟机栈不是一个东西,但是性质是一样的,只不过为本地方法服务

java堆

1.只能存放对象实例和数组

2.被垃圾收集器管理,也叫GC堆

3.线程共享,但是可以划分出多个线程私有的分配缓冲区(TLAB)

4.逻辑空间连续,物理空间可以不连续,但是大多数虚拟机实现的时候要求物理空间也连续

5.通过Xmx和Xms设置大小,如果内存不足又无法扩展大小,抛出OutOfMemoryError

方法区

1.本质上也是堆,但是和java堆是两码事,线程共享,存储被虚拟机加载的类型信息,常量,静态变量,编译后的代码

2.方法区存放的数据一般是永久的,所以很早以前方法区和永久代相同,这样做使得方法区无法扩展,容易发生内存溢出,jdk8之后永久代被抛弃,,所以现在永久代和方法区不是一回事

3.物理内存可以不连续,还可以选择不实现垃圾收集,因为方法区存放的数据生命周期大多是永久的

4.会报OutOfMemoryError

5.运行时常量池:方法区的一部分,存放编译期(注意是编译期)中产生的各种字面量和符号引用,因为java不要求常量在编译期才能产生,所以和class文件常量池是不同的

6.直接内存:方法区的一部分,简单解释就是由于NIO的存在,使得本地方法可以直接在本地内存中存放,使用直接内存可以对这块本地内存进行引用,避免使用本地方法时本地堆和java堆之间来回复制数据

对象

对象创建

1.new的时候jvm会先检查这个指令参数是否能在常量池里找到相同的类的引用

2.如果有的话检查一下是否被加载,解析和初始化

3.如果没有初始化,那么先执行相应类的加载

4.分配内存(从堆里划内存块),分配内存有两种方式

1.指针碰撞:物理内存连续的话,在使用过的没用过的内存中间放个指针就可以了2.空闲列表:如果物理内存不连续,就使用一个空闲列表记录内存空闲单位然后分配3.使用标记移动算法的回收器时用指针碰撞,使用标记清理算法的回收器时用空闲列表4.指针碰撞在高并发下线程不安全,可以使用CAS或TLAB缓冲区解决线程安全问题

TLAB

1.TLAB的本质是一块很小的内存,从eden区薅来的(关于eden见GC部分)

3.每次TLAB用完再去找eden要就行,而TLAB本身又是原子操作,不存在线程安全问题

4.相当与以前是new的时候对new出来的对象的要分配内存加锁,现在是直接取一块内存出来,取的时候加锁,这样即保证线程安全,又可以减少加锁次数提高效率

对象布局

1.对象头(markword):保存了对象自身数据(hashcode,锁标志位啥的)和指向对象类型本身的指针,jvm通过这个指针来判断对象是什么类型的,如果是数组的话还会记录一发数组长度

2.实例数据:保存了字段真正的内容

3.对齐填充:人如其名,就是对齐用的无用数据,不用管它

对象访问定位

1.java通过栈上的引用数据来操作堆上的数据

2.一般这个所谓的引用的底层实现有两种,句柄or指针

3.句柄:java堆中一部分内存作为句柄池,一个句柄包含对象实例指针和对象类型指针,通过两个指针在实例池里找实例,在方法区里找类型

4.指针:指针的话直接在java堆里存放数据,一个指针包含对象实例的数据和类型指针,相比句柄由于实例已经存在堆里了所以少一次指针定位,访问开销小

内存屏障

用于同步两个线程,控制特定条件下的重排序和内存可见性问题,Java编译器也会根据内存屏障的规则禁止处理器重排序,java中有四种内存屏障

1.LoadLoad屏障:举例语句是Load1; LoadLoad; Load2(这句里面的LoadLoad里面的第一个Load对应Load1加载代码,然后LoadLoad里面的第二个Load对应Load2加载代码),此时的意思就是在Load2加载代码在要读取的数据之前,保证Load1加载代码要从主内存里面读取的数据读取完毕。

2.StoreStore屏障:举例语句是 Store1; StoreStore; Store2(这句里面的StoreStore里面的第一个Store对应Store1存储代码,然后StoreStore里面的第二个Store对应Store2存储代码)。此时的意思就是在Store2存储代码进行写入操作执行前,保证Store1的写入操作已经把数据写入到主内存里面,确认Store1的写入操作对其它处理器可见。

3.LoadStore屏障:举例语句是 Load1; LoadStore; Store2(这句里面的LoadStore里面的Load对应Load1加载代码,然后LoadStore里面的Store对应Store2存储代码),此时的意思就是在Store2存储代码进行写入操作执行前,保证Load1加载代码要从主内存里面读取的数据读取完毕。

4.StoreLoad屏障:举例语句是Store1; StoreLoad; Load2(这句里面的StoreLoad里面的Store对应Store1存储代码,然后StoreLoad里面的Load对应Load2加载代码),在Load2加载代码在从主内存里面读取的数据之前,保证Store1的写入操作已经把数据写入到主内存里面,确认Store1的写入操作对其它处理器可见。

三种常见的垃圾回收机制

1.标记清除:先标记,标记后统一清除 缺点:执行效率不稳定(回收对象少和多的效率区别巨大),内存空间容易碎片化

2.标记复制:将内存分成两半(比例不一定是1比1),一半用完了就开始回收,将没挂的放到另一半上 缺点:内存只能用一半了 优点:内存空间连续了 改进:eden+两个survivor就是这么来的(appel算法)

3.标记整理:每次kill完之后让所有存活对象都移动到内存空间的一端 缺点:移动的时候会停掉其他用户线程(所谓的STW Stop the World)

垃圾回收算法

引用计数法

1.每个对象维护一个引用计数器

2.被引用一次计数器+1,引用失效计数器-1

3.计数器归零后对象失效

优点:高效

缺点:需要大量额外空间,无法解决两个对象相互引用的问题(a引用b,b再引用a,这样ab的引用计数器永远至少是大于0的,永远不会被回收)

可达性分析算法

1.假设有一个没有失效的对象,叫它GC Roots,从该点进行图遍历,被遍历到的对象就是被引用的对象,那么他们肯定也没有失效

2.没有被遍历到的对象会被标记一次或者直接失效

3.GC Roots的选取:

1.虚拟机栈或本地方法栈中被引用的对象(肯定还活着)2.方法区中的静态属性和常量属性引用的对象(永生)3.虚拟机内部的引用,比如基本数据类型对应的class,常驻的一场对象啥的(永生)4.synchronized所持有的对象(阻塞)5.本地代码缓存啥的总结的话就是:永生的,肯定还活着的,阻塞的另外一些临时性对象也可以加入,还有不同内存区域内的对象也可以,上面五个是肯定可以,而不是只能是五个

4.oopMap

oopmap的本质就是一个记录了栈上本地变量到堆上对象的引用关系其作用是:垃圾收集时,收集线程会对栈上的内存进行扫描,看看哪些位置存储了 Reference 类型。如果发现某个位置确实存的是 Reference 类型,就意味着它所引用的对象这一次不能被回收oopmap可以不必每次GC的时候都遍历整个GC roots,同时线程除了维护自己的oopmap以外还会将自己的oopmap传给其他线程,这样的作法本质就是以时间换空间

GC中的常见概念

四种引用:

1.强引用:被引用的对象绝不会被回收2.软引用:在内存溢出前不会被回收。如果内存溢出则回收,如果回收后内存还溢出就抛异常吧3.弱引用:垃圾收集时直接被回收4.幻影引用:不光会被直接回收,而且还不能通过引用取得对象实例,这种引用的存在就是在被回收的时候发个通知

三色标记

白色:未被访问过的对象,如果可达性分析结束还是白色,则该对象不可达(挂掉)黑色:被访问过并且存活灰色:被访问过,但是其指向的引用至少还有一个没被扫描过,是黑色和白色之间的交汇

引用关系破坏问题

假设对象a引用对象b,对象b引用对象c,那么根据可达性分析算法,abc都为黑色标记假设GC和用户线程并发执行现在a被访问后,在访问b的时候如果用户线程删除了b到c的引用,同时让a引用c,那么c会变成白色,并且c本不该是白色,造成误删解决该问题的方法就是使用写屏障(原始快照,增量更新)

安全点

1.oopmap不能对每条指令进行记录,这样消耗太大,所以只有线程在到达安全点的时候才更新oopmap,到达安全点的线程会挂起2.安全点的选取一般是执行时间较长的代码块,比如循环之类的3.安全点的线程中断分为两种1.被动中断:安全点是固定的,每次回收的时候停掉所有在安全点的用户线程,让不在安全点的线程跑到安全点再停掉2.主动中断:安全点固定,每次回收的时候设置一个标志,不中断任何线程,线程不断轮询标志,如果标志为true,则线程找到离自己最近的安全点再断掉

安全区域

如果用户线程sleep之类的,则没办法自己跑到安全点or轮询,这个时候就用到了安全区,安全区代码要求代码内的引用关系不发生变化,线程执行到安全区代码的时候先标识一下,虚拟机回收垃圾的时候会直接略过安全去代码,出安全区的时候要看根节点枚举是否完成,完成了就直接出,没完成则需要阻塞等待完成才能出

Stop The World

1.单线程下GC和用户线程只能同时执行一个,所以这时候需要STW2.并发下为了保证GC中对象的引用关系的一致性,所以需要在GC的时候启动STW,防止出现引用破坏问题3.stw的实现原理是利用安全点实现的,因为安全点和安全区域可以保证引用关系的一致性,就是所谓的冻结快照,这个时候执行stw是安全的

对象的死亡过程

1.被标记后再进行二次筛选,标准是是否执行了finalize()方法2.如果执行了finalize()方法则系统会像赛跑一样开始追这个对象,这个对象如果在内存回收前执行完finalize()方法则会逃生成功,如果执行的太慢or执行的优先度太低就死掉了3.finalize()方法只能执行一次,执行第二次就不管用了,对象会直接死掉4.finalize()这个方法已经不建议使用了,可以使用try+finally代替

垃圾回收器

垃圾回收器分为老年代和新生代两种,一个GC的过程中可以使用不同的两个垃圾回收器作为组合回收老年代的新生代的内存区域

概念

1.Partial GC:部分收集,仅收集堆中部分区域的垃圾,包含了Young GC(Minor GC),Old GC(Major GC)和Mixed GC 2.Full GC:对整个java堆和方法区回收

分类

回收器 新生代 老年代 并发 标记清除 标记复制 标记整理 STW
Serial O X X X O X O
Serial Old X O X X X O O
ParNew O X O X O X O
Parallel Scavenge O X O X O X O
Parallel Old X O O X X O O
CMS X O O O X X O
G1 O O O X O X O

Parallel Scavenge:

1.目标是控制吞吐量,所以多用于客户端

2.控制吞吐量指的是:单次回收的速度可以慢,只要运行期间总的回收速度快就可以

CMS

1.CMS的运行过程

1.初始标记:STW,标记GC Roots能直接关联的对象,速度快2.并发标记:从GC Roots的直接关联对象开始并发遍历,not STW,速度慢3.重新标记:修正并发标记期间用户线程运行导致变化的对象关系,STW,速度快,采用卡表+写屏障(增量更新)解决跨代引用和引用关系破坏问题4.并发清除:kill掉要挂掉的对象,速度快,not STW

2.跨代引用

跨代引用指的是老年代引用年轻代or年轻代引用老年代的问题假设老年代引用年轻代,则执行Young GC的时候不光要扫描年轻代,还要将老年代全部扫描一次才能确保引用关系不出错,这样消耗太大可以使用记忆集来解决,将跨代引用记录到一个集合中跨代引用本质上和引用关系破坏可以放在一起说,因为跨代引用会出问题的部分一般都是老年代引用年轻代,但是老年代出生早与年轻代,所以老年代引用年轻代等同于给黑色对象添加了一个指向白色对象的引用

3.记忆集和卡表

CARD_TABLE [this address >> 9] = 0;

通过卡表记录跨代引用,将对应的card位置dirty,就可以不必扫描整个老年代,只扫描dirty找出跨代引用,同时由于跨代引用下的老年代必然不会被回收,所以可以作为GC Roots,使用卡表可以快速定位老年代中的GC Roots


4.写屏障

卡表的dirty则由写屏障触发,写屏障的本质就是在写操作前or写操作后挂上一段逻辑代码通过并发,如果GC回收时引用关系进行了变化,那么写屏障就会更新卡表,防止出现引用关系破坏问题

5.增量更新和原始快照

这两个方法都是用于解决引用关系破坏问题的,而写屏障则是这两种方法的实现手段,使用写屏障可以在并发环境下实时记录引用关系的变化,并在重新标记阶段再次扫描增量更新用于CMS中,而原始快照用于G1增量更新:只要有黑色对象新加了指向白色对象的引用关系,把这个新插入的引用记录下来,等并发扫描结束后,在将这个黑色对象为根重新扫描(本质就是把黑色对象变成灰色对象)原始快照:当灰色对象删除指向白色对象的引用关系时,就记录这个将要删除的引用,并发扫描结束后,在以白色对象为根重新扫描一次(本质就是把白色对象变成灰色对象)

G1

G1比其他GC器都要特殊,它同时拥有老年代和新生代的GC,有自己特殊的Mixed GC模式,并且老年代和新生代不再进行物理隔离,而是逻辑上区分

1.G1的数据结构

G1不再将对象作为回收单位,而是使用region作为回收单位一个region是一块内存,region中保存了多个card,card指的就是卡表里那个card,由于card也是一块内存,所以一个card未必仅指一个对象region也被进行了分代,分为新生代和老年代,其中新生代又被分为Eden,Survivor(from)和Survivor(to),以下简称e,sf和st,他们的比例为8:1:1,老年代除了老年代数据以外,还有Humongous,它用于存储大对象,即容量大于0.5个region的对象region的数量默认为2048个,单个region的大小取值范围为1-32mb



2.G1的分代模型

1.G1中的Young GC被分为e,st和sf三部分2.第一次GC的时候会将所有region视为e,被回收掉的e就不管它了,生存下来的e会被标记复制算法放入st中3.接下来把st中的对象用标记整理算法一定移动到sf中,在sf中的对象内存是连续的,每熬过一次GC就会年长一岁,G1中默认为15岁的对象会被放入老年代,如果sf区域满了,那么同样4.接下来每次GC循环上面的过程,每次GC后保证e和st为空即可5.这样做的理由:Survivor的存在是用于解决老年代回收过于频繁的问题,如果没有Survivor,每次幸存对象都会进入老年代,而老年代由于使用标记整理,所以回收速度很慢,而年轻代则快很多使用sf专门对幸存对象进行整理可以避免内存碎片问题(为什么使用两个Survivor)

3.G1的回收逻辑

G1除了传统的Young GC和Old GC之外还有自己独立MixedGCMixed GC=YoungGC+部分Old GC,Young GC因为耗费时间短所以肯定会全部执行具体做法是,对region进行价值排序,回收最大的那些region,至于回收的数量由用户制定一个-XX:MaxGCPauseMills参数,该参数为用户希望的停顿时间,根据衰减均值作为理论基础实现对回收步骤总时间的计算,以此推导出在不超过用户期望时间的情况下最多能回收的region个数

3.rset,cset

4.TAMS

TAMS两个指针,用于标记并发回收和并发清除前,region当前最后一个card所在的位置(这个被称top指针)这样在并发过程中,如果产生新对象,G1要求新对象被分配在tams指向的位置之后,通过tams可以直接得到在并发中产生的新对象,在G1中,并发时产生的新对象被认为是需要存活的

5.G1的Mixed GC运行过程

1.触发条件:eden快满了就触发GC,同时创建cset2.初始标记:STW,标记GC Roots能直接关联的对象,速度快,同时这一步要修改TAMS指针方便之后的并发标记3.并发标记:从GC Roots的直接关联对象开始并发遍历,not STW,速度慢,同时这一步启动原始快照记录引用关系变化4.最终标记:修正并发标记期间用户线程运行导致变化的对象关系,STW,速度快,仅仅对写屏障(原始快照)进行遍历就可以解决引用关系破坏和跨代引用问题5.筛选标记:根据-XX:MaxGCPauseMills最大化收益,筛选出要回收的region(部分old GC)

6.G1的full GC触发条件:一般当回收速度赶不上内存分配的速度的时候会出发full GC,会导致长时间的stw,cms触发full GC的条件也是如此,比如年轻代晋升老年代失败(G1),比如由于内存碎片产生浮动垃圾导致Concurrent Mode Failure(cms)

一篇关于jvm GC的问答文章:https://www.cnblogs.com/yescode/p/13961190.html

空间担保

1.空间担保指的是老年代进行空间分配担保,本质是通过对剩余空间的检查自动判断本次GC是Younbg GC还是Full GC

2.在发生Young GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果小于,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败

3.如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小,如果大于,则尝试进行一次Young GC,但这次Young GC依然是有风险的

4.如果小于或者HandlePromotionFailure=false,则改为进行一次Full GC

下面这张图是包含了空间担保的GC时机和过程


年轻代大小选择

1.响应时间优先的场景:尽可能将年轻代设大,让应用响应时间尽可能接近系统的最低响应时间限制,这样年轻代GC发生的频率最小

2.吞吐量优先的场景:尽可能设置大,因为对响应时间没有要求,GC可以并行进行,适合8核cpu以上的应用

老年代大小的选择:

1.响应时间优先的场景:老年代使用并发GC,设置要考虑并发会话率和会话持续时间等参数,堆太大回收慢,堆太小内存碎片多,一般参考以下原则优化

1.并发垃圾收集信息2.持久代并发收集次数3.传统GC信息4.花费在年轻代和老年代GC上的时间比例一般减少年轻代和老年代花费时间会提高应用效率

2.吞吐量优先的场景:老年代设置小,年轻代设置大,这样可以尽可能回收掉大部分短期对象,老年代存放的都是需要长期存活的对象

OOM解决办法:

先检查内存是否泄漏,如果泄漏,找到GC Roots泄漏的路径解决设置-Xms -Xmx加大两个堆检查堆中是否有对象实例一直没有释放让-Xms=Xmx,减少内存扩展的开销

堆空间不足的原因

吞吐量增加程序无意中保存了对象引用,无法被GC回收过度使用finalizer解决方法:-xmx增加堆大小or修复程序中的内存泄漏

GC开销超过限制

--Xmx

使用-XX:-UseGVOverheadLimit取消GC开销限制修复程序中的内存泄漏

请求数组大小超过虚拟机限制

Xmx(万物皆可Xmx)修复程序中分配巨大数组的bug

永久代空间不足

XX:MaxPermsize增加永久代大小重启jvm

Metaspace不足

设置-XXMaxMetaSpaceSize增加metaspace大小为服务器分配更多内存修复可能的程序bug

无法新建本机线程

一般是内存不足造成的

为机器分配更多内存减少java堆空间修复内存泄漏增加操作系统级别的限制

误杀进程或子进程

一般也是因为内存不足

将进程迁移到其他机器上给机器增加内存


以上是关于混乱的jvm的主要内容,如果未能解决你的问题,请参考以下文章

从JVM的角度看JAVA代码--代码优化

常用Javascript代码片段集锦

如何使用事件侦听器来加载动画片段的循环

分析定位占用CPU资源高的JVM线程

片段之间的共享数据(父列表视图和子列表视图)

详解Jvm内存结构