Switch Case 支持哪几种数据类型?
switch 表达式后面的数据类型只能是 byte,short,char,int 四种整形类型,枚举类型和 java.lang.String 类型(从 java 7 才允许),不能是 boolean 类型。
StringBuffer 和 StringBuilder 的区别?
1、线程安全
StringBuffer:线程安全,StringBuilder:线程不安全。因为 StringBuffer 的所有公开方法都是 synchronized 修饰的,而 StringBuilder 并没有 synchronized 修饰。
2、缓冲区
StringBuffer 每次获取 toString 都会直接使用缓存区的 toStringCache 值来构造一个字符串。
StringBuilder 则每次都需要复制一次字符数组,再构造一个字符串。
所以, StringBuffer 对缓存区优化,不过 StringBuffer 的这个 toString 方法仍然是同步的。
3、性能
既然 StringBuffer 是线程安全的,它的所有公开方法都是同步的,StringBuilder 是没有对方法加锁同步的,所以毫无疑问,StringBuilder 的性能要远大于 StringBuffer。
String 与 StringBuffer 的区别
String:
1、String 创建的对象是不可变的,一旦创建不可改变
2、对象值可以改变其实是创建了一个新的对象,然后把新的值保存进去
3、String 类被 final 修饰,不可以被继承
4、String 创建的对象的值存在于常量池,不用的时候不会被销毁
5、String 运行时间较长
6、String 适用于比较短而小的字符串
StringBuffer:
1、StringBuffer 创建的对象是可变的
2、它的改变不像 String 那样重新创建对象,而是通过 构造方法
3、StringBuffer 创建的对象的值存在于栈区,不用的时候会被销毁
4、StringBuffer 运行时间较短
5、StringBuffer 适用于比较长的字符串、比较多的字符串
StringJoiner 有什么用?
StringJoiner 是 JDK1.8,java.util 包中的一个类,用于构造一个由分隔符重新连接的字符序列
String aa = "今天";
String bb = "明天";
StringJoiner sj = new StringJoiner(":", "[", "]");
sj.add(aa).add(bb);
System.out.println(sj.toString());
普通类和抽象类有什么区别?
1、抽象类的存在时为了被继承,不能实例化,而普通类存在是为了实例化一个对象
2、抽象类的子类必须重写抽象类中的抽象方法,而普通类可以选择重写父类的方法,也可以直接调用父类的方法
3、抽象类必须用 abstract 来修饰,普通类则不用
4、普通类和抽象类都可以含有普通成员属性和普通方法
5、普通类和抽象类都可以继承别的类或者被别的类继承
6、普通类和抽象类的属性和方法都可以通过子类对象来调用
静态内部类和普通内部类有什么区别?
1、普通内部类和静态内部类的区别:
a) 普通内部类持有对外部类的引用,静态内部类没有持有外部类的引用。
b) 普通内部类能够访问外部类的静态和非静态成员,静态内部类不能访问外部类的非静态成员,他只能访问外部类的静态成员。
c) 一个普通内部类不能脱离外部类实体被创建,且可以访问外部类的数据和方法,因为他就在外部类里面。
2、区别还有
a) 第一,内部类可以访问其所在类的属性(包括所在类的私有属性),内部类创建自身对象需要先创建其所在类的对象
b) 第二,可以定义内部接口 ,且可以定义另外一个内部类实现这个内部接口
c) 第三,可以在方法体内定义一个内部类,方法体内的内部类可以完成一个基于虚方法形式的回调操作
d) 第四,内部类不能定义 static 元素
e) 第五,内部类可以多嵌套
f) static 内部类是内部类中一个比较特殊的情况,Java 文档中是这样描述 static 内部类的:一旦内部类使用 static 修饰,那么此时这个内部类就升级为顶类。也就是说,除了写在一个类的内部以外,static 内部类具备所有外部类的特性(和外部类完全一样)。
静态方法可以直接调用非静态方法吗?
静态 static 方法中不能直接调用非静态 non-static 方法,但可以通过将对象引用传入静态方法内,进而再调用该对象非静态(non-static)方法;
在主函数 (static 方法) 中,我们经常需要创建某个类的实例,再引用其非静态方法。
静态变量和实例变量有什么区别?
(1) 实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量。
(2) 静态变量不属于某个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。
(3) 总之,实例变量必须创建对象后才可以通过这个对象来使用,静态变量则可以直接使用类名来引用。
(4) 举例:对于下面的程序,无论创建多少个实例对象,永远都只分配了一个 staticVar 变量,并且每创建一个实例对象,这个 staticVar 就会加 1;但是,每创建一个实例对象,就会分配一个 instanceVar,即可能分配多个 instanceVar,并且每个 instanceVar 的值都只自加了 1 次。
内部类可以访问其外部类的成员吗?
内部类可以访问所在外部类的成员。
但有一点需要注意:静态成员不能访问非静态成员,因此静态内部类(属于静态成员)就不能访问外部类的非静态成员。
内部类可以引用他包含类的成员吗?有没有什么限制?
完全可以。如果不是静态内部类,那没有什么限制! 一个内部类对象可以访问创建它的外部类对象的成员包括私有成员。
如果你把静态嵌套类当作内部类的一种特例,那在这种情况下不可以访问外部类的普通成员变量,而只能访问外部类中的静态成员。
接口和抽象类有什么区别?
1、抽象类和接口都不能直接实例化。如果要实例化,抽象类变量必须指向实现所有抽象方法的子类对象,接口变量必须指向实现所有接口方法的类对象。
2、抽象类要被子类继承,接口要被类实现。
3、接口只能做方法申明,抽象类中可以做方法申明,也可以做方法实现。
4、接口里定义的变量只能是公共的静态的常量,抽象类中的变量是普通变量。
5、抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,实现接口的时候,如不能全部实现接口 方法,那么该类也只能为抽象类。
6、抽象方法只能申明,不能实现。
7、抽象类里可以没有抽象方法
8、如果 — 个类里有抽象方法,那么这个类只能是抽象类
9、抽象方法要被实现,所以不能是静态的,也不能是私有的。
10、接口可以继承接口,并且可多继承接口,但类只能单 — 继承。
- 接口可以通过匿名内部类实例化。接口是对动作的抽象,抽象类是对根源的抽象。抽象类表示的是,这个对象是什么。而接口表示的是,这个对象能做什么。
扩展说明设计层面区别如下
1,抽象类是对事物的抽象,即对类抽象;接口是对行为抽象,即局部抽象。抽象类对整体形为进行抽象,包括形为和属性。接口只对行为进行抽象。
例子:举个简单的例子,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类 Airplane,将鸟设计为一个类 Bird,但是不能将飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。此时可以将 飞行 设计为一个接口 Fly,包含方法 fly (),然后 Airplane 和 Bird 分别根据自己的需要实现 Fly 这个接口。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承 Airplane 即可,对于鸟也是类似的,不同种类的鸟直接继承 Bird 类即可。从这里可以看出,继承是一个 "是不是" 的关系,而 接口 实现则是 "有没有" 的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。
2,抽象类是多个子类的像类,是一种模板式设计;接口是一种形为规范,是一种辐射式设计。
例子:最简单例子,大家都用过 ppt 里面的模板,如果用模板 A 设计了 ppt B 和 ppt C,ppt B 和 pptC 公共的部分就是模板 A 了,如果它们的公共部分需要改动,则只需要改动模板 A 就可以了,不需要重新对 ppt B 和 pptC 进行改动。而辐射式设计,比如某个电梯都装了某种报警器,一旦要更新报警器,就必须全部更新。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。
接口是否可以继承接口?
1、接口可以继承接口,抽象类不可以继承接口,但可以实现接口。
2、抽象类可以继承实体类。抽象类可以实现 (implements) 接口,抽象类是否可继承实体类,取决于实体类必须是否有明确的构造函数。
3、抽象类可以继承实体类,这是因为抽象类可继承性且有方法。
4、一个接口可以继承多个接口. interface C extends A, B {} 是可以的;
5、 一个类可以实现多个接口: class D implements A,B,C {}, 但是一个类只能继承一个类,不能继承多个类 class B extends A {}
在继承类的同时,也可以实现多个接口: class E extends D implements A,B,C {} 这也正是选择用接口而不是抽象类的原因。
接口与类的区别:
1、接口不能用于实例化对象。
2、接口没有构造方法。
3、接口中所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。
4、接口不能包含成员变量,除了 static 和 final 变量。
5、接口不是被类继承了,而是要被类实现。
6、接口支持多继承。
抽象类和接口的区别
1、抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
2、抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
3、接口中不能含有静态代码块以及静态方法 (用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
4、一个类只能继承一个抽象类,而一个类却可以实现多个接口。
注:JDK 1.8 以后,接口里可以有静态方法和方法体了。
注:JDK 1.8 以后,接口允许包含具体实现的方法,该方法称为 "默认方法",默认方法使用 default 关键字修饰。
注:JDK 1.9 以后,允许将方法定义为 private,使得某些复用的代码不会把方法暴露出去。
74、接口里面可以写方法实现吗?
接口之所以成为接口,就在于它没有实现,只是声明。但后来一切都变了,Java 里出现了默认方法,C# 也出现了默认方法。接口已经不像传统意义上的接口,其概念开始向抽象类靠近,一个纯抽象的东西,突然出现了实体,于是开始傻傻分不清了。
在 jdk8 之前,接口里的方法都是定义的方法,没有方法体。需要实现类去实现。
那在 JDK8,就出现了默认方法,就是接口里面方法,有方法体,只需要在方法上增加个 default
75、抽象类必须要有抽象方法吗?
抽象类中不一定要有抽象方法。
在编程语句中用 abstract 修饰的类是抽象类。抽象类是不完整的,它只能用作基类,不能生成对象。抽象类可以包含抽象方法、非抽象方法和抽象访问器。可以创建一个变量,其类型是一个抽象类,并让它指向具体子类的一个实例。不能有抽象构造函数或抽象静态方法。
扩展资料:
抽象类不能直接实例化,并且对抽象类使用 new 运算符会导致编译时错误。虽然一些变量和值在编译时的类型可以是抽象的,但是这样的变量和值必须或者为 null,或者含有对非抽象类的实例的引用
抽象类提供多个派生类共享基类的公共定义,它既可以提供抽象方法,也可以提供非抽象方法。如果派生类没有实现所有的抽象方法,则该派生类也必须声明为抽象类。另外,实现抽象方法由 overriding 方法来实现。
76、抽象类能使用 final 修饰吗?
不能
77、抽象类是否可以继承具体类?
抽象类是可以继承实体类,但前提是实体类必须有明确的构造函数
78、抽象类是否可以实现接口?
可以的,而且这是抽象类的一个重要作用。
当一个类需要去实现一个接口时,如果该类实现了接口中的所有方法,则该类既可以定义为实体类也可以定义为抽象类;
如果该类实现了接口中的部分方法,还有部分方法没有实现,没有实现的部分方法只能继续以抽象方法的形式存在该类中,则该类必须定义为抽象类。
这么做的目的是:当我们需要定义一个类去实现接口中的部分方法时,我们可以先通过抽象类来实现其它部分的方法,然后去继承这个抽象类,就可以达到只实现接口中部分方法的目的;
试想如果是需要定义多个类都需要去实现接口中的部分方法,这时抽象类的作用就突出了,可以降低实现类实现接口的难度,同时解决了代码冗余的问题,所以这种情况在实际开发中的应用场景也是很多的。
新生代 Eden 与两个 Survivor 区的解释
1、为什么会有年轻代
我们先来屡屡,为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化 GC 性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC 的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当 GC 的时候先把这块存 “朝生夕死” 对象的区域进行回收,这样就会腾出很大的空间出来。
2、年轻代中的 GC
HotSpot JVM 把年轻代分为了三部分:1 个 Eden 区和 2 个 Survivor 区(分别叫 from 和 to)。默认比例为 8:1, 为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到 Eden 区 (一些大对象特殊处理), 这些对象经过第一次 Minor GC 后,如果仍然存活,将会被移到 Survivor 区。对象在 Survivor 区中每熬过一次 Minor GC,年龄就会增加 1 岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
因为年轻代中的对象基本都是朝生夕死的 (80% 以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在 GC 开始的时候,对象只会存在于 Eden 区和名为 “From” 的 Survivor 区,Survivor 区 “To” 是空的。紧接着进行 GC,Eden 区中所有存活的对象都会被复制到 “To”,而在 “From” 区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值 (年龄阈值,可以通过 - XX:MaxTenuringThreshold 来设置) 的对象会被移动到年老代中,没有达到阈值的对象会被复制到 “To” 区域。经过这次 GC 后,Eden 区和 From 区已经被清空。这个时候,“From” 和 “To” 会交换他们的角色,也就是新的 “To” 就是上次 GC 前的 “From”,新的 “From” 就是上次 GC 前的 “To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,直到 “To” 区被填满,“To” 区被填满之后,会将所有对象移动到年老代中。
-
一个对象的这一辈子
我是一个普通的 java 对象,我出生在 Eden 区,在 Eden 区我还看到和我长的很像的小兄弟,我们在 Eden 区中玩了挺长时间。有一天 Eden 区中的人实在是太多了,我就被迫去了 Survivor 区的 “From” 区,自从去了 Survivor 区,我就开始漂了,有时候在 Survivor 的 “From” 区,有时候在 Survivor 的 “To” 区,居无定所。直到我 18 岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了 20 年 (每次 GC 加一岁),然后被回收。 -
有关年轻代的 JVM 参数
1)-XX:NewSize 和 - XX:MaxNewSize
用于设置年轻代的大小,建议设为整个堆大小的 1/3 或者 1/4, 两个值设为一样大。
2)-XX:SurvivorRatio
用于设置 Eden 和其中一个 Survivor 的比值,这个值也比较重要。
3)-XX:+PrintTenuringDistribution
这个参数用于显示每次 Minor GC 时 Survivor 区中各个年龄段的对象的大小。
4).-XX:InitialTenuringThreshol 和 - XX:MaxTenuringThreshold
用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次 Minor GC 之后,年龄就加 1。
JVM 垃圾回收算法及回收器详解
一、GC Roots
我们先来了解一下在 Java 中是如何判断一个对象的生死的,有些语言比如 Python 是采用引用计数来统计的,但是这种做法可能会遇见循环引用的问题,在 Java 以及 C# 等语言中是采用 GC Roots 来解决这个问题。如果一个对象和 GC Roots 之间没有链接,那么这个对象也可以被视作是一个可回收的对象。
Java 中可以被作为 GC Roots 中的对象有:
虚拟机栈中的引用的对象。
方法区中的类静态属性引用的对象。
方法区中的常量引用的对象。
本地方法栈(jni)即一般说的 Native 的引用对象。
回到顶部
垃圾收集算法
标记 - 清除
标记 - 清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。在标记阶段首先通过根节点,标记所有从根节点开始的对象,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。标记清除算法带来的一个问题是会存在大量的空间碎片,因为回收后的空间是不连续的,这样给大对象分配内存的时候可能会提前触发 full gc。
1、标记清除标记清除
复制算法
将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
2、复制算法复制算法
现在的商业虚拟机都采用这种收集算法来回收新生代,IBM 研究表明新生代中的对象 98% 是朝夕生死的,所以并不需要按照 1:1 的比例划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中的一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地拷贝到另外一个 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 的空间。HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1 (可以通过 - SurvivorRattio 来配置),也就是每次新生代中可用内存空间为整个新生代容量的 90%,只有 10% 的内存会被 “浪费”。当然,98% 的对象可回收只是一般场景下的数据,我们没有办法保证回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。
3、标记 - 整理
标记 - 整理算法是一种老年代的回收算法,它在标记 - 清除算法的基础上做了一些优化。首先也需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
4、分代收集算法
当前商业虚拟机的垃圾收集都采用 “分代收集”(GenerationalCollection)算法,这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记 - 清理” 或 “标记 - 整理” 算法来进行回收。
5、增量算法
增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
二、垃圾回收器
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了 7 种作用于不同分代的收集器,其中用于回收新生代的收集器包括 Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括 Serial Old、Parallel Old、CMS,还有用于回收整个 Java 堆的 G1 收集器。不同收集器之间的连线表示它们可以搭配使用。
1、Serial 收集器
Serial 收集器是最古老的单线程的收集器,它的缺点是当 Serial 收集器想进行垃圾回收的时候,必须暂停用户的所有进程,即 stop the world。到现在为止,它依然是虚拟机运行在 client 模式下的默认新生代收集器,与其他收集器相比,对于限定在单个 CPU 的运行环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾回收自然可以获得最高的单线程收集效率。
2、Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本,它同样是一个单线程收集器,使用” 标记-整理 “算法。这个收集器的主要意义也是被 Client 模式下的虚拟机使用。在 Server 模式下,它主要还有两大用途:一个是在 JDK1.5 及以前的版本中与 Parallel Scanvenge 收集器搭配使用,另外一个就是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 的时候使用。
通过指定 -UseSerialGC 参数,使用 Serial + Serial Old 的串行收集器组合进行内存回收。
3、ParNew 收集器
ParNew 收集器是 Serial 收集器新生代的多线程实现,注意在进行垃圾回收的时候依然会 stop the world,只是相比较 Serial 收集器而言它会运行多条进程进行垃圾回收。
ParNew 收集器在单 CPU 的环境中绝对不会有比 Serial 收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个 CPU 的环境中都不能百分之百的保证能超越 Serial 收集器。当然,随着可以使用的 CPU 的数量增加,它对于 GC 时系统资源的利用还是很有好处的。它默认开启的收集线程数与 CPU 的数量相同,在 CPU 非常多(譬如 32 个,现在 CPU 动辄 4 核加超线程,服务器超过 32 个逻辑 CPU 的情况越来越多了)的环境下,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。
-UseParNewGC: 打开此开关后,使用 ParNew + Serial Old 的收集器组合进行内存回收,这样新生代使用并行收集器,老年代使用串行收集器。
4、Parallel Scavenge 收集器
Parallel 是采用复制算法的多线程新生代垃圾回收器,似乎和 ParNew 收集器有很多的相似的地方。但是 Parallel Scanvenge 收集器的一个特点是它所关注的目标是吞吐量 (Throughput)。所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
5、Parallel Old 收集器
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,采用多线程和” 标记-整理” 算法。这个收集器是在 jdk1.6 中才开始提供的,在此之前,新生代的 Parallel Scavenge 收集器一直处于比较尴尬的状态。原因是如果新生代 Parallel Scavenge 收集器,那么老年代除了 Serial Old (PS MarkSweep) 收集器外别无选择。由于单线程的老年代 Serial Old 收集器在服务端应用性能上的” 拖累 “,即使使用了 Parallel Scavenge 收集器也未必能在整体应用上获得吞吐量最大化的效果,又因为老年代收集中无法充分利用服务器多 CPU 的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有 ParNew 加 CMS 的组合” 给力 “。直到 Parallel Old 收集器出现后,” 吞吐量优先 “收集器终于有了比较名副其实的应用,在注重吞吐量及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
-UseParallelGC: 虚拟机运行在 Server 模式下的默认值,打开此开关后,使用 Parallel Scavenge + Serial Old 的收集器组合进行内存回收。-UseParallelOldGC: 打开此开关后,使用 Parallel Scavenge + Parallel Old 的收集器组合进行垃圾回收
6、CMS 收集器
CMS (Concurrent Mark Swep) 收集器是一个比较重要的回收器,现在应用非常广泛,我们重点来看一下,CMS 是一种获取最短回收停顿时间为目标的收集器,这使得它很适合用于和用户交互的业务。从名字 (Mark Swep) 就可以看出,CMS 收集器是基于标记清除算法实现的。它的收集过程分为四个步骤:
初始标记 (initial mark)
并发标记 (concurrent mark)
重新标记 (remark)
并发清除 (concurrent sweep)
注意初始标记和重新标记还是会 stop the world,但是在耗费时间更长的并发标记和并发清除两个阶段都可以和用户进程同时工作。
不过由于 CMS 收集器是基于标记清除算法实现的,会导致有大量的空间碎片产生,在为大对象分配内存的时候,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前开启一次 Full GC。为了解决这个问题,CMS 收集器默认提供了一个 - XX:+UseCMSCompactAtFullCollection 收集开关参数(默认就是开启的),用于在 CMS 收集器进行 FullGC 完开启内存碎片的合并整理过程,内存整理的过程是无法并发的,这样内存碎片问题倒是没有了,不过停顿时间不得不变长。虚拟机设计者还提供了另外一个参数 - XX:CMSFullGCsBeforeCompaction 参数用于设置执行多少次不压缩的 FULL GC 后跟着来一次带压缩的(默认值为 0,表示每次进入 Full GC 时都进行碎片整理)。
不幸的是,它作为老年代的收集器,却无法与 jdk1.4 中已经存在的新生代收集器 Parallel Scavenge 配合工作,所以在 jdk1.5 中使用 cms 来收集老年代的时候,新生代只能选择 ParNew 或 Serial 收集器中的一个。ParNew 收集器是使用 - XX:+UseConcMarkSweepGC 选项启用 CMS 收集器之后的默认新生代收集器,也可以使用 - XX:+UseParNewGC 选项来强制指定它。
由于 CMS 收集器现在比较常用,下面我们再额外了解一下 CMS 算法的几个常用参数:
UseCMSInitatingOccupancyOnly:表示只在到达阈值的时候,才进行 CMS 回收。
CMS 默认启动的回收线程数目是 (ParallelGCThreads+3)/4,如果你需要明确设定,可以通过 - XX:+ParallelCMSThreads 来设定,其中 - XX:+ParallelGCThreads 代表的年轻代的并发收集线程数目。
CMSClassUnloadingEnabled: 允许对元类数据进行回收。
CMSInitatingPermOccupancyFraction:当永久区占用率达到这一百分比后,启动 CMS 回收 (前提是 - XX:+CMSClassUnloadingEnabled 激活了)。
CMSIncrementalMode:使用增量模式,比较适合单 CPU。
UseCMSCompactAtFullCollection 参数可以使 CMS 在垃圾收集完成后,进行一次内存碎片整理。内存碎片的整理并不是并发进行的。
UseFullGCsBeforeCompaction:设定进行多少次 CMS 垃圾回收后,进行一次内存压缩。
一些建议
对于 Native Memory:
使用了 NIO 或者 NIO 框架(Mina/Netty)
使用了 DirectByteBuffer 分配字节缓冲区
使用了 MappedByteBuffer 做内存映射
由于 Native Memory 只能通过 FullGC 回收,所以除非你非常清楚这时真的有必要,否则不要轻易调用 System.gc ()。
另外为了防止某些框架中的 System.gc 调用(例如 NIO 框架、Java RMI),建议在启动参数中加上 - XX:+DisableExplicitGC 来禁用显式 GC。这个参数有个巨大的坑,如果你禁用了 System.gc (),那么上面的 3 种场景下的内存就无法回收,可能造成 OOM,如果你使用了 CMS GC,那么可以用这个参数替代:-XX:+ExplicitGCInvokesConcurrent。
此外除了 CMS 的 GC,其实其他针对 old gen 的回收器都会在对 old gen 回收的同时回收 young gen。
7、G1 收集器
G1 收集器是一款面向服务端应用的垃圾收集器。HotSpot 团队赋予它的使命是在未来替换掉 JDK1.5 中发布的 CMS 收集器。与其他 GC 收集器相比,G1 具备如下特点:
并行与并发:G1 能更充分的利用 CPU,多核环境下的硬件优势来缩短 stop the world 的停顿时间。
分代收集:和其他收集器一样,分代的概念在 G1 中依然存在,不过 G1 不需要其他的垃圾回收器的配合就可以独自管理整个 GC 堆。
空间整合:G1 收集器有利于程序长时间运行,分配大对象时不会无法得到连续的空间而提前触发一次 GC。
可预测的非停顿:这是 G1 相对于 CMS 的另一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
在使用 G1 收集器时,Java 堆的内存布局和其他收集器有很大的差别,它将这个 Java 堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不需要连续)的集合。
虽然 G1 看起来有很多优点,实际上 CMS 还是主流。
GC 相关的常用参数:
Xmx: 设置堆内存的最大值。
Xms: 设置堆内存的初始值。
Xmn: 设置新生代的大小。
Xss: 设置栈的大小。
PretenureSizeThreshold: 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。
MaxTenuringThrehold: 晋升到老年代的对象年龄。每个对象在坚持过一次 Minor GC 之后,年龄就会加 1,当超过这个参数值时就进入老年代。
UseAdaptiveSizePolicy: 在这种模式下,新生代的大小、eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和停顿时间 (MaxGCPauseMills),让虚拟机自己完成调优工作。
SurvivorRattio: 新生代 Eden 区域与 Survivor 区域的容量比值,默认为 8,代表 Eden: Suvivor= 8: 1。
XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的。
XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在 MaxGCPauseMills 以内。
XX:GCTimeRatio: 设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。
NewRatio: 设置新生代(包括 Eden 和两个 Survivor 区)与老年代的比值(除去持久代),设置为 3,则新生代与老年代所占比值为 1:3,新生代占整个堆栈的 1/4。
g1 和 cms 介绍 (适合面试)
CMS 垃圾回收器:
CMS 是作用于老年代的并发垃圾回收器 , 使用标记清除算法,工作流程是:初始标记,并发标记,再次标记,并发清除
优点
耗时最长的并发标记和并发清除阶段 gc 线程和用户线程是并发执行的,因此其 STW 时间短,适合对延迟有要求的任务
缺点:
CMS 在老年代使用的是标记清除算法,会产生大量内存碎片
GC 线程与用户线程并发执行,二者会抢占 cpu, 并且会产生浮动垃圾
初始标记阶段会发生短暂的 stw, 用于标记 GCRoot 对象能够直接到达的对象
并发标记阶段 gc 线程根据 GCRoot 对象标记可到到的存活对象,应用程序可以和 gc 线程并行进行,不需要 stw
再次标记阶段会进行 stw, 目的是为了修正因为并发标记阶段应用程序和 gc 线程并发执行产生的浮动垃圾
并发清除阶段 gc 线程清除垃圾对象,gc 线程和应用线程并发执行因此会产生浮动垃圾,在下一次 gc 清理该浮动垃圾
G1 垃圾回收器:
G1 垃圾回收器是一款可以同时管理新生代和老年代,在老年代使用标记整理算法,其最大的特点是将内存划分为多个大小相等 region, 每个 region 都可以作为伊甸区,survivor 区,老年代
优点:
老年代使用标记整理算法,不会产生内存碎片
使用 region, 不会出现新生代或者老年代分配空间过大而造成浪费
每次只选择垃圾对象多的 region, 而不是整个堆,大幅减少了 STW 时间 (但 region 与 region 之间是有依赖关系的,g1 维护了一个 Remembered Set 记忆集记录了 region 的依赖关系,只需要扫描关联的 region, 而不是整个堆)
用于可预测停顿的模型,可以指定 STW 时间 (也就是可预测停顿), 比如在一小时内垃圾回收导致的 "stop the world" 时间不超过一分钟。
G1 垃圾回收过程主要包含三个阶段,
当伊甸区慢时,年轻代使用标记复制算法进行回收
当堆空间的内存占用达到阈值时,老年代使用标记整理算法进行回收,前三个过程和 cms 类似,都为初始标记,并发标记,并发清除,区别在于最终清除阶段,CMS 是并发的,而 G1 会进行 STW, 不是并发的
当老年代占比达到阈值,触发混合回收,为了防止堆内存耗尽,会回收所有年轻代和部分老年代
CMS 和 G1 比较:
G1 和 CMS 都分为 4 个阶段,前三个阶段基本相同都为初始标记,并发标记,再次标记,区别在于最后清除阶段 CMS 是并发的,G1 不是并发的,因此 CMS 最终会产生浮动垃圾,只能等待下次 gc 才能清除
G1 可以管理整个堆,而 CMS 只能作用于老年代,并且 CMS 在老年代使用的是标记清除算法,会产生内存碎片,而 G1 使用标记整理算法,不会产生内存碎片
G1 相比于 CMS 最大的区别是 G1 将内存划分为大小相等的 Region, 可以选择垃圾对象多的 Region 而不是整个堆从而减少 STW, 同时使用 Region 可以更精确控制收集,我们可以手动明确一个垃圾回收的最大时间
补充:
因为耗时最长的并发标记和并发清除 gc 线程和应用线程都是并发执行的,所以总体来看 cms 收集器的 gc 线程和应用线程是并发执行的
G1 之所以能做到回收时间可控,主要是得益于 Region 这个结构。G1 会记录每个 Region 里的对象有多少是垃圾对象,如果要对某个 Region 进行垃圾回收,他会计算出对该 Region 回收的时间,可以回收多少垃圾。
实际上除了 CMS 收集器,其他都不存在只针对老年代的收集。
但是每个 region 之间是有互相引用的依赖关系的!这导致在 MinorGC 的时候会同时对老年代进行扫描(甚至是整个堆扫描),那就会导致 MinorGC 的效率低下,时间变长!
解决方法是:维护一个 Remebered Set 集合来存放各个 Region 之间的引用关系!当进行 GC Roots Tracing 的时候就可以只扫描 set 里的关联 region!而不用全堆扫描啦!!! 在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏了。