Java类加载器(ClassLoader)是Java运行时环境的一部分,负责将java类编译后的字节码文件(.class)加载到Jvm内存中,生成对应的class对象。
类加载器加载完class文件,还需要经历验证、准备、解析三个阶段,最后才能初始化该类。
初始化一个类,一定会触发类加载器;但是类加载器加载了该类,该类不一定初始化。
首先类加载器分为四种:自定义类加载器、应用类加载器、扩展类加载器、启动类加载器。当一个类加载器收到请求之后,首先会依次向上查找到最顶层的类加载器(启动类加载器),然后再依次向下加载class文件,如果已经加载到class文件,子加载器不会继续加载该class文件。
目的就是为了防御开发者定义的类与jdk定义的源码类产生冲突问题,保证该类在内存中的唯一性。
继承ClassLoader类,重写findClass()方法,调用defineClass()方法。
Java SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
实现方式:
ServiceLoader<MyService> load = ServiceLoader.load(MyService.class);
load.forEach((t) -> {
System.out.println(t.get());
});
第一种方式:重写ClassLoader的loadClass()方法;
第二种方式:指定线程类加载器+SPI机制,绕开ClassLoader的loadClass()方法;
首先自定义一个类加载器去加载该类,然后创建一个线程(while循环)去监听该类是否发生了变化(根据最后修改的时间来判断),如果发生了变化的话,则使用类加载器重新读取该类。
不会报错,但是创建的Object不会被类加载器加载到。
堆:Java堆是各线程共享的内存区域,在JVM启动时创建,这块区域是JVM中最大的, 用于存储应用的对象和数组,也是GC主要的回收区,一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行。在JDK1.8之前堆内存分为三部分:新生代、老年代、永久代。
JDK1.8及之后堆可分为:年轻代和老年代两块区域。使用NewRatio参数来设定比例。对于年轻代,一个Eden区和两个Suvivor区,使用参数SuvivorRatio来设定大小。
注意:
Jdk1.6及之前:常量池分配在永久代。
Jdk1.7:有,但已经逐步“去永久代”。
Jdk1.8及之后:无永久代,改用元空间代替(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)。
栈(线程栈):Java栈是线程私有的,是在线程创建时创建,它的生命期跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致。基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
每个方法执行的时候都会创建一个栈帧,栈帧中主要存储3类数据:
局部变量表:输入参数和输出参数以及方法内的变量;
栈操作:记录出栈和入栈的操作;
栈帧数据:包括类文件、方法等等。
栈帧:一个方法对应一个栈帧内存空间,每个方法都有自己独立的栈帧内存空间,栈是一种数据结构:保持先进后出的原则。
栈帧内部细节结构:局部变量表、操作数栈、动态链接、方法出口。
栈帧就是每个方法需要的运行时内存空间。
程序计数器(PC寄存器):记录当前线程执行下一行指令的执行地址,作用主要记录多线程因上下文切换过程中记录当前线程的下一行指令。也就是记录当前线程执行的行号。
本地方法栈:本地方法栈和JVM栈发挥的作用非常相似,也是线程私有的,区别是JVM栈为JVM执行Java方法(也就是字节码)服务,而本地方法栈为JVM使用到的Native方法服务。它的具体做法是在本地方法栈中登记native方法,在执行引擎执行时加载Native Liberies.有的虚拟机(比如Sun Hotpot)直接把两者合二为一。也就是 java调用c语言代码 jni技术。
方法区:是线程共享的,主要存储类信息、常量池、静态变量、JIT编译后的代码等数据。方法区理论上来说是堆的逻辑组成部分。
JDK6、JDK7时,方法区就是 PermGen(永久代);JDK8时,方法区就是 Metaspace(元空间)。
空间分配:堆需要程序员申请,而栈由系统自动分配;
线程是否共享:堆是存储单位(线程共享),而栈是运行时的单位,主要存放局部变量信息(线程私有);
缓存方式:堆存放在二级缓存中,生命周期由垃圾回收算法决定,而栈使用的是一级缓存,调用时处于存储空间中,调用完毕立即释放(生命周期和线程保持一致);
数据结构:堆可以被看成是一棵树(堆排序),而栈是一种先进后出的数据结构。
方法(栈帧)调用过多会导致栈内存溢出,比如说方法递归调用。原因:在栈空间中产生了非常多的栈帧空间一直没有被释放。JVM就会报java.lang.StackOverflowError的错误。
-Xss256k
增加栈内存;减少递归深度调用(通过一定条件退出)。
在申请内存的时候,内存不足,产生堆内存溢出,JVM就会报java.lang.OutOfMemoryError: Java heap space的错误。
-Xmx8m
被占用的内存经过多次长时间的GC操作都无法回收,导致可用内存越来越少,俗称内存泄露,JVM就会报java.lang.OutOfMemoryError: GC overhead limit exceeded错误。
-Xmx3M -Xms3M
JDK命令行工具:
可视化分析工具:jconsole、jvisualvm。
当方法区的内存满了的时候,JVM会报java.lang.OutOfMemoryError: Metaspace(JDK1.8及之后)的错误(JDK1.8之前:java.lang.OutOfMemoryError: PermGen space)。
JDK1.8之前版本:-XX:MaxPermSize=8m
JDK1.8及之后版本:-XX:MaxMetaspaceSize=8m
-XX:-UseCompressedClassPointers
通过一张表,虚拟机根据该常量表找到执行的类名、方法名、参数类型、字面量。常量池的分为:静态常量池、字符串常量池、运行时常量池。
静态常量池(class常量池):我们都知道,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
运行时常量池:则是JVM虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在元空间中,我们常说的常量池,就是指方法区中的运行时常量池。
字符串常量池:JVM在运行时使用了全局的StringTable(就是一个哈希表)。Hotspot从Java7开始,存放于堆里。比如String a=”a”。
JDK1.6及之前常量池都是放入方法区(永久区);
Jdk1.7常量池放入到堆中(不合理);
Jdk1.8只是将字符串常量池放入到堆,其他常量都是放在元空间。
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;// StringBuilder
String s5 = "ab";
String s6 = s4.intern();// 将对象放入到字符串常量池中,如果存在,则不会放入
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
结果:false,true,true
类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式:
选择以下两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的。
内存分配并发问题:
创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
TLAB: 为每一个线程预先在 Eden 区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配。
初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
执行init方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
Java对象分为:对象头、实例数据、对齐填充组合。
对象头:HotSpot虚拟机的对象头(Object Header)包括两部分信息:
第一部分"Mark Word":用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等.
第二部分"Klass Pointer":对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。)
Mark Word:
注意:在64位的虚拟机情况下mark word占用64位,32位虚拟机占32位。64位=8字节
Klass Pointer:这一部分用于存储对象的类型指针,该指针指向它的类元数据,jvm通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64的JVM将会比32位的JVM多耗费50的内存。为了节约内存可以使用选项 -XX:+UseCompressedOops 开启指针压缩。其中 oop即ordinary object pointer 普通对象指针。
-XX:+UseCompressedOops 开启指针压缩
-XX:-UseCompressedOops 不开启指针压缩
对象头:Mark Word+Klass Pointer类型指针 未开启压缩的情况下
32位 Mark Word =4bytes ,类型指针 4bytes ,对象头=8bytes =64bits
64位 Mark Word =8bytes ,类型指针 8bytes ,对象头=16bytes=128bits;
注意:默认情况下,开启了指针压缩 可能只有12字节。
实例属性:就是定义类中的成员属性
对齐填充:对齐填充并不是必然存在的,也没有特定的含义,仅仅起着占位符的作用。
由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。
句柄方式和直接指针方式。
句柄:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
直接指针:如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象的地址。
这两种对象访问方式各有优势。使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
核心思想:堆内存中的对象没有被任何引用。
引用计数法:每次当该对象被引用一次的时候,引用次数都会+1,如果引用的次数为0,则认为没有被引用,直接被垃圾回收器清理掉。
最大的缺陷:当两个对象相互引用时,虽然他们不会再被引用了,但他们的计数不能归为0,所以无法垃圾回收(如下图)。Java 没有使用此类算法。
可达性分析算法:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
JVM采用可达性分析算法判断对象是否可被回收。(解决了循环引用的问题)
强引用:被引用关联的对象永远不会被垃圾收集器回收。
例如:Object object = new Object();那object就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
软引用:软引用关联的对象,在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。使用SoftReference类创建。
弱引用:无论内存是否足够,只要JVM开始进行垃圾回收,那些被弱引用关联的对象都会被回收。使用WeakReference类创建。
虚引用:如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收。使用PhantomReference类创建。
效果:引用队列可以配合软引用、弱引用及虚引用使用,当引用的对象将要被JVM回收时,会将其加入到引用队列中。
应用:通过引用队列可以了解JVM垃圾回收情况。
Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是堆内存中对象的分配与回收。
Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
经过这次 GC 后,Eden 区和"From"区已经被清空。这个时候,“From"和"To"会交换他们的角色,也就是新的"To"就是上次 GC 前的“From”,新的"From"就是上次 GC 前的"To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。
部分收集 (Partial GC):
新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
整堆收集 (Full GC):收集整个Java堆和方法区。
因为新生代gc非常频繁,所以选择效率比较高的垃圾回收算法。
在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
补充:所谓的Stop the World机制,简称STW,即在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时,系统只能允许GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运行。这些工作都是由虚拟机在后台自动发起和自动完成的,是在用户不可见的情况下把用户正常工作的线程全部停下来,这对于很多的应用程序,尤其是那些对于实时性要求很高的程序来说是难以接受的。所以在服务器项目中,应该设计能够去减少Stop-The-World问题。
注意:市面上所有的垃圾收集器都有Stop-The-World问题,开发中尽量不要调用 System.gc(),有可能会导致Stop-The-World问题。
1)停止所有的Java执行线程(“stop the world”)可达性分析必须在一致性的快照中进行,一致性指的是不可以出现分析过程中对象引用关系还在不断变化的情况。这点是导致GC进行时必须停顿所有java线程的一个原因。
2)准确式GC:当系统停下来时,不需要一个不漏的检查完所有执行上下文和全局的引用位置。HotSpot的实现是:在特定位置上(即安全点),虚拟机通过OopMap数据结构在类加载时,将对象内什么偏移量上是什么类型的数据计算出来,并存储到其中,来达到这个目的。在OopMap的协助下,HotSpot可以快速且准确的完成GCRoots的枚举。
GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat。
串行收集器:SerialGC 只有一个GC线程清理堆内存垃圾,堆内存不是很大;
并行(多线程)收集器: Parallel 开启多个GC线程同时清理堆内存垃圾。
共同特征:当GC线程开始清理堆内存垃圾的时候,都会让我们用户线程暂停,因为采用标记清理或者复制算法引用地址有可能会发生变化。
最大的缺点:会导致STW非常长。
并行(Parallel)收集器:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态;如ParNew、Parallel Scavenge、Parallel Old;
并发(Concurrent)收集器:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;如CMS、G1(也有并行)。
JDK8(含Jdk8)之前的组合关系:Serial/Serial 0ld、Serial/CMS、 ParNew/Serial 0ld、 ParNew/CMS、Paral1el Scavenge/Serial 0ld、Paral1el Scavenge/Parallel 0ld、G1;
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
比如:应用程序运行了100s,其中垃圾收集花费1s,那么吞吐量占比为99%。(100/100+1)。
Serial收集器和Serial Old收集器:是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。新生代采用标记复制算法,老年代采用标记整理算法。优点:简单高效。主要应用于:桌面应用程序(堆内存空间很小)。参数配置:-XX:+PrintCommandLineFlags -XX:+UseSerialGC。
ParNew收集器:其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。采用标记-复制算法。应用场景:在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作;但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。参数配置:"-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器;"-XX:+UseParNewGC":强制指定使用ParNew;"-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;-XX:+PrintCommandLineFlags -XX:+UseParNewGC。
Parallel Scavenge收集器:Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。采用标记-复制算法。参数配置:"-XX:MaxGCPauseMillis"(减少用户线程暂停时间):控制最大垃圾收集停顿时间,大于0的毫秒数;MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;因为可能导致垃圾收集发生得更频繁;"-XX:GCTimeRatio" (吞吐量优先):设置垃圾收集时间占总时间的比率,0 Parallel Old收集器:Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。 CMS(Concurrent Mark Sweep)收集器:以获取最短回收停顿时间【也就是指Stop The World的停顿时间】为目标,多数应用于互联网站或者B/S系统的服务器端上。其中“Concurrent”并发是指垃圾收集的线程和用户执行的线程是可以同时执行的。CMS是基于“标记-清除”算法实现的。 G1(Garbage一First)收集器:是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征,JDK9中已经默认使用G1收集器,可以全功能的垃圾收集器,采用标记整理算法避免堆空间冗余性问题。 CMS是基于“标记-清除”算法实现的,整个过程分为4个步骤: 注意:“标记”是指将存活的对象和要回收的对象都给标记出来,而“清除”是指清除掉将要回收的对象。 优点: 缺点: 因为如果采用标记-整理算法,为了保证内存空间的连续性,必须移动内存中的地址,而在内存地址的移动过程中,会暂停所有的用户线程,STW的时间增长,与CMS收集器以获取最短回收停顿时间为目标的观念相背离。虽然采用标记-清除算法,会导致内存空间不连续,在存放大对象的时候无法进行存放,从而导致Full GC,但是当触发Full GC的时候开始采用备选方案(串行老年代GC),使用标记-整理算法整理堆内存空间。 -XX:+UseConcMarkSweepGc 手动指定使用CMS收集器执行内存回收任务。 因为G1是一个并行/并发回收器,它把堆内存分割为很多不相关的区域(Region) (物理上 不连续的)。使用不同的Region来表示Eden、幸存者0(S0)区,幸存者(S1)1区,老年代等。由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)。G1 GC有计划地避免在整个Java 堆中进行全区域的垃圾收集。G1跟踪各个Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。 使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB、2MB、4MB、8MB、16MB、32MB。 JDK9已经默认开启了G1收集器,如果在JDK8开启G1收集器。需要配置 在G1收集器中也有一个新的内存区域,称作为:Humongous (H)区(巨型对象),主要存放一些比较大的对象,一个对象大于region的一半时,称之为巨型对象,G1不会对巨型对象进行拷贝,回收时会考虑优先回收。 在一个region中可能会引入到其他的region,为了避免不必要的全局扫描,在每个region中都对应一个Remembered Set(记忆集),使用CarTable记录每个region区相互引用的关系。 收集集合(CSet)代表每次GC暂停时回收的一系列目标分区。在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。年轻代收集CSet只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。 GC如果想查找到存活的对象,根据可达分析算法 根据GCRoot引用链遍历存活对象。根据GC Root遍历过程中,按照是否访问过该对象分为三种不同颜色: 在并发标记阶段,由于用户线程和GC线程同时运行,可能会存在多标和漏标问题。 漏标问题: 漏标的两个充要条件: 为了解决漏标问题,需要破坏漏标的两个充要条件。 CMS收集器采用增量更新+写屏障的方式实现强三色不变式:当黑色对象指向了白色对象之后,利用写屏障把这个引用记录下来,在重新标记阶段,再以黑色对象为根,对它的引用进行重新扫描。优点:避免浮动垃圾;缺点:需要对整个链进行扫描,效率低。 G1收集器采用原始快照(Snapshot At The Beginning,SATB)+写屏障的方式实现弱三色不变式:当灰色对象取消对白色对象的引用时,利用写屏障把这个引用记录下来,在重新标记阶段,把白色对象变成灰色对象,以灰色对象为根,继续对它的引用进行扫描。优点:效率高,无需扫描整个引用链;缺点:会产生浮动垃圾。 写屏障(Store Barrier):所谓的写屏障,其实就是指在赋值操作前后,加入一些处理(可以参考AOP的概念):记录赋值的操作。
52. 能具体谈一谈CMS收集器的实现过程吗?
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。
53. CMS收集器有什么优缺点?
54. 为什么CMS收集器采用标记-清除算法,而不采用标记-整理算法?
简述:因为CMS收集器采用并行的方式,清除垃圾与用户线程可以同时运行,为了保证用户线程与GC线程同时运行,所以采用标记清除算法,如果采用标记整理算法,有可能会导致移动内存地址,会发生的stw问题。55. 能说说CMS收集器中有哪些配置参数吗?
开启该参数后会自动将-XX: +UseParNewGc打开。即:ParNew(Young区用)+CMS(0ld区用)+Serial 0ld的组合。
-XX:CMS1nitiatingOccupanyFraction设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一 次CMS 回收。JDK6 及以上版本默认值为92%,如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。
-XX:+UseCMSCompactAtFullCollection用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了。
-XX:CMSFullGCsBeforeCompaction设置在执行多少次Full GC后对内存空间进行压缩整理。
-XX:ParallelCMSThreads设置CMS的线程数量。CMS默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads是年轻代并行收集器的线程数。当CPU资源比较紧张时,受到CMs收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕。新生代回收线程数:和当前cpu核数相等。56. 为什么叫G1收集器?
57. 能具体谈谈G1收集器的分区原理吗?
可以通过-XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变。
虽然还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。
一个region(分区)只能属于一个角色,有可能为eden区、S区、老年代等,E表示为Eden区、S区表示为S1,S0区,老年代O区,空白的表示为未使用的分配的内存,H区存放巨型对象。
58. G1收集器的核心参数有哪些?
-XX:G1HeapRegionSize 设置每个Region的大小。值是2的幂,范围是1MB 到32MB之间,目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。 也就是G1收集器最小堆内存应该是2GB以上,最大堆内存64GB
-XX:MaxGCPauseMillis 设置期望达到的最大Gc停顿时间指标 ,默认值是200ms
-XX:ParallelGCThread 设置垃圾回收线程数 最大设置为8
-XX:ConcGCThreads 设置并发标记的线程数。将n设置为并行垃圾回收线程数(ParallelGCThreads)的1/4左右。
-XX:InitiatingHeapOccupancyPercent 设置触发并发GC周期的Java堆占用率阈值。超过此值,就触发GC。默认值是45。
-XX:+UseG1GC 设置开启G1收集器
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+PrintGCDetails -verbose:gc59. 为什么G1收集器需要设计存放巨型对象(H区)?
在以前收集器中,如果是一个大对象是直接放入到老年代中,而触发老年代GC不是很频繁,万一该大对象不是非常频繁的使用,则会非常浪费堆内存,为了解决这个问题在G1收集器专门弄一个H区存放巨型对象,如果一个H区装不下的情况下,则会寻找连续H区存储,如果还没有足够的空间,有可能会引发FULLGC。60. G1收集器的RSet有什么作用?
61. G1收集器的CSet有什么作用?
简述:CSet用于记录用户线程暂停的区域。62. 谈谈G1收集器的回收过程?
63. 能谈谈三色标记算法吗?
白色:本对象没有访问过(有可能是垃圾对象);
灰色:本对象已经被访问过,且本对象的所有属性没有访问过;本对象所有属性都访问过后,本对象由灰色变为黑色;
黑色:本对象已经被访问过,且本对象的所有属性都被访问过;
原理:
64. 使用三色标记算法会存在什么样的问题?
多标问题(浮动垃圾):
在并发标记阶段,用户线程与GC线程同时运行,如果现在C对象已经扫描完毕,C对象由灰色变成黑色,扫描到的E对象和D对象由白色变成灰色,但是用户线程执行C.E=null,也就是说此时的E对象已经变为了垃圾对象,可是GC线程仍然会认为E对象是可达对象,会将E对象作为起始点继续扫描E对象以下的整个引用链,整个引用链的对象则是多标对象。该问题可以在重新标记阶段进行修复。
在并发清除阶段,用户线程与GC线程同时运行,会产生新的对象,但是没有及时被GC清理,只能在下一次的GC中进行修复。
在并发标记阶段,用户线程与GC线程同时运行,如果现在B对象已经扫描完毕,B对象由灰色变成黑色,扫描到的C对象由白色变成灰色,然后用户线程执行C.E=null,C与E断开连接,则认为E是不可达对象,可是用户线程突然又执行了B.E=E,此时的E对象应该是可达对象,但是由于B对象已经变为了黑色对象不会继续扫描,导致E对象没有被GC线程扫描到,则认为E对象是不可达对象,这里的E对象则是漏标对象。
65. CMS收集器和G1收集器如何解决漏标问题?
强三色不变式:保证永远不会存在黑色对象到白色对象的引用(破坏情况1)。
弱三色不变式:所有被黑色对象引用的白色对象都处于灰色保护状态,即直接或间接从灰色对象可达(破坏情况2)。66. 如何减少堆内存触发GC的频率?