堆、方法栈、程序计数器、方法区、本地方法区
堆:对象和数组
虚拟机栈:每个方法创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
程序计数器:当前操作字节码行号指示器
方法区(永久代):用于存储被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。java8后变为元数据区,使用直接内存。
本地方法区:jvm执行时用到的本地方法
publicclass Test{ publicstaticvoid main(String args[]){ int date = 9; Test test =new Test(); test.change(date); BirthDate d1=newBirthDate(7,7,1970); } public void change(int i){ i = 1234; }
int date = 9是局部变量,存在方法栈中;Test test =new Test(), new Test()是对象存在堆中,test是对象引用存在方法栈中;i是方法中的局部变量存放在方法栈中,函数调用完毕消失;BirthDate d1=newBirthDate(7,7,1970),数字在方法栈中,d1在方法栈中,new在堆中。
新生代占堆1/3,又分eden区,s0 s1,eden区是对象出生地。MinorGC采用复制算法,majorgc用标记清除算法。
对象是否存活:引用计数法:对象有没有引用(循环引用问题)「若 A 强引用了 B,那 B 引用 A 时就需使用弱引用,当判断是否为无用对象时仅考虑强引用计数是否为 0,不关心弱引用计数的数量」,可达性分析:gc root 出发是否可达。
强引用,软引用(内存空间足够不回收),弱引用(GC时立刻回收回收),虚引用(是跟踪对象被垃圾回收的状态)
G1收集器
Garbage first垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与CMS收集器,G1收集器两个最突出的改进是:
1.基于标记-整理算法,不产生内存碎片。2.可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获得最高的垃圾收集效率。
JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化。
加载:会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口;验证:确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求 ;准备:在方法区中分配这些变量所使用的内存空间;解析:虚拟机将常量池中的符号引用替换为直接引用的过程;初始化。
垃圾回收器:
Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;
ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;
Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;
Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;
Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;
CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。
point-out:指向引用的对象。
特点:基于标记-清除算法实现。并发收集、低停顿。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器的运行过程分为下列4步:
初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
并发清除:对标记的对象进行清除回收。
CMS收集器的缺点:
对CPU资源非常敏感。
无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。
因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。
G1(Garbage First)收集器 (标记-整理算法): 没有明确的年代划分,堆被划分为一系列大小相等的“小堆区”,也称为region。每个小堆区(region)的大小为1~32MB,整个堆默认划分出2048个小堆区。每个region逻辑上被称为eden、survivor、old、humongous区。
Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。
point-out:哪些分区引用了当前分区中的对象,只需要记录老年代到新生代之间的引用即可,新生代本身就会被扫描。
Remembered Set,作用是跟踪指向某个heap区内的对象引用。Card Table:当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,扫描回收时只需要扫描脏被引用的区域。
Young GC时只需要扫描当前对象是否被引用,避免对大范围老年代扫描。
Young GC 阶段:阶段1:根扫描:静态和本地对象被扫描;阶段2:更新RS:处理dirty card队列更新RS;阶段3:处理RS:检测从年轻代指向年老代的对象;阶段4:对象拷贝:拷贝存活的对象到survivor/old区域;阶段5:处理引用队列:软引用,弱引用,虚引用处理。
G1 Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。
步骤分2步:全局并发标记(global concurrent marking);拷贝存活对象(evacuation)。
标记步骤:初始标记(initial mark,STW):负责标记所有能被直接可达的根对象(原生栈对象、全局对象、JNI对象);根区域扫描(root region scan):为了保证标记算法的正确性,所有新复制到Survivor分区的对象,都需要被扫描并标记成根,根分区扫描必须在下一次年轻代垃圾收集启动前完成(并发标记的过程中,可能会被若干次年轻代垃圾收集打断),因为每次GC会产生新的存活对象集合;并发标记(Concurrent Marking):和应用线程并发执行,每个线程每次只扫描一个分区,从而标记出存活对象图,并发标记线程还会定期检查和处理STAB全局缓冲区列表的记录,更新对象引用信息;最终标记(Remark,STW):在该阶段中,G1需要一个暂停的时间,去处理剩下的SATB日志缓冲区和所有更新,找出所有未被访问的存活对象,同时安全完成存活数据计算;清除垃圾(Cleanup,STW):RSet梳理,整理堆分区,为混合收集周期识别回收收益高(基于释放空间和暂停目标)的老年代分区集合;识别所有空闲分区,即发现无存活对象的分区。该分区可在清除阶段直接回收,无需等待下次收集周期。
三色标记算法
黑色:根对象,或者该对象与它的子对象都被扫描
灰色:对象本身被扫描,但还没扫描完该对象中的子对象
白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象
在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。
在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:
1,在开始标记的时候生成一个快照图标记存活对象
2,在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的,即删除旧引用时把被引用对象变色)
3,可能存在游离的垃圾,将在下次被收集
SATB会创建一个对象图,相当于堆的逻辑快照,从而确保并发标记阶段所有的垃圾对象都能通过快照被鉴别出来。当赋值语句发生时,应用将会改变了它的对象图,那么JVM需要记录被覆盖的对象。因此写前栅栏会在引用变更前,将值记录在SATB日志或缓冲区中。
RSet
跨代引用:堆空间通常被划分为新生代和老年代。由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,那么回收新生代的话,需要扫描所有从老年代到新生代的所有引用,所以要避免每次YGC时扫描整个老年代,减少开销。
记忆集(RSet,Remembered Set):用来记录从其他Region中的对象到本Region的引用,是一种抽象的数据结构。每一个Region都设有一个RSet,有了这个数据结构,在回收某个Region的时候,就不必对整个堆内存的对象进行扫描了,它使得部分收集成为了可能。
G1主要在赋值语句中,使用写前栅栏(Pre-Write Barrrier)和写后栅栏(Post-Write Barrrier)。
写前栅栏 Pre-Write Barrrier
即将执行一段赋值语句时,等式左侧对象将修改引用到另一个对象,那么等式左侧对象原先引用的对象所在分区将因此丧失一个引用,那么JVM就需要在赋值语句生效之前,记录丧失引用的对象。JVM并不会立即维护RSet,而是通过批量处理,在将来RSet更新(见SATB)。
写后栅栏 Post-Write Barrrier
当执行一段赋值语句后,等式右侧对象获取了左侧对象的引用,那么等式右侧对象所在分区的RSet也应该得到更新。同样为了降低开销,写后栅栏发生后,RSet也不会立即更新,同样只是记录此次更新日志,在将来批量处理。
并发优化线程:只专注扫描日志缓冲区记录的卡片来维护更新RSet。并发优化线程永远是活跃的,一旦发现全局列表有记录存在,就开始并发处理。
收集集合 CSet:收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。
触发Full GC
在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求
并发模式失败
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。
晋升失败或者疏散失败
G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:
a,增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。
b,通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。
c,也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。
巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。
CMS收集器和G1收集器的区别
区别一: 使用范围不一样
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
区别二: STW的时间
CMS收集器以最小的停顿时间为目标的收集器。
G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
区别三: 垃圾碎片
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。
区别四: 垃圾回收的过程不一样
最终标记:并发标记期间用户程序会导致标记记录产生变动(好比一个阿姨一边清理垃圾,另一个人一边扔垃圾)虚拟机会将这段时间的变化记录在Remembered Set Logs 中。最终标记阶段会向Remembered Set合并并发标记阶段的变化。这个阶段需要线程停顿,也可以并发执行
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading) 验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。
在加载阶段,虚拟机需要完成以下3点:
通过一个类的全限定名来获取定义此类的二进制字节流。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据的访问入口。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,也就是说,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件, 被同一个虚拟机加载, 只要加载它们的类加载器不同,那这两个类就必定不相等。
类加载器可划分为:
启动类加载器(BootstrapClassLoader) 负责加载Java核心类,例如/JAVA_HOME/lib目录下的类
扩展类加载器(ExtClassLoader) 负责加载/JAVA_HOME/lib/ext目录下的类
应用程序类加载器(AppClassLoader) 负责加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。好处:向上委托给父类加载,父类加载不了再自己加载;避免重复加载,防止Java核心api被篡改。
GC Roots:
一个对象可以属于多个root,GC root有几下种:
Class- 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots.
Thread- 活着的线程
Stack Local- Java方法的local变量或参数
JNI Local- JNI方法的local变量或参数
JNI Global- 全局JNI引用
Monitor Used- 用于同步的监控对象
Held by JVM- 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。