今天学习了JVM中的字符串常量池和集中垃圾手机算法,收获很大。
剑指Offer 9
用2×1的小矩形去填充2×n的矩形,问共有多少种填充的方法?
//用2×1的小矩形去填充2×n的矩形,问共有多少种填充的方法?
public class Offer9 {
public static void main(String[] args) {
Offer9 offer9 = new Offer9();
int i = offer9.rectCover(9);
System.out.println(i);
}
public int rectCover(int n ) {
if (n <= 2) {
return n;
}
return rectCover(n - 1) + rectCover(n - 2);
}
public int rectCover1(int n){
if (n<=2){
return n;
}
int[] a = new int[n+1];
a[0]=0;
a[1]=1;
a[2]=2;
for (int i = 3; i <=n ; i++) {
a[i]=a[i-1]+a[i-2];
}
return a[n];
}
}
字符串的拼接
public class StringSplicing {
public static void main(String[] args) {
String s1 ="java";
String s2 ="String";
String s3 = "javaString";
String s4 = s1+s2;
String s5 = s1+"String ";
String s6 = "java" + s2;
String s7 = "java"+"String";
String s8 = s6.intern();
//在字符串拼接的时候,如果两边有变量,相当于在堆中new了一个变量,所以内存地址不同false
System.out.println(s3==s4);//false
System.out.println(s3==s5);//false
System.out.println(s3==s6);//false
System.out.println(s3==s7);//true
//intern()会判断字符串常量池中是否有这个字符串,如果没有,则放入字符串常量池中,并加载一份javaString返回
// 如果有则不放入,并返回字符串常量池中的javaString
System.out.println(s3==s8);//true
}
}
- 常量与常量的拼接结果在常量池,原理是编译期优化
- 常量池中不会存在相同内容的变量
- 只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder
- 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
加上final修饰之后
public class FinalStringTest {
public static void main(String[] args) {
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1+s2;
System.out.println(s3 == s4);//true
}
}
但是如果使用的是final修饰,则是从常量池中获取。所以说拼接符号左右两边都是字符串常量或常量引用 则仍然使用编译器优化,即用StringBuilder的方式。也就是说被final修饰的变量,将会变成常量,类和方法将不能被继承、
字符串拼接与StringBulider
public class Test06 {
private static int i = 100000;
public static void main(String[] args) {
//System.out.println(method1()); 5043mm
//System.out.println(method2());4mm
}
public static long method1(){
String s = "";
long start = System.currentTimeMillis();
for (int j = 0; j <i ; j++) {
s+="a";
}
long end = System.currentTimeMillis();
return end - start;
}
public static long method2(){
StringBuilder s = new StringBuilder();
long start = System.currentTimeMillis();
for (int j = 0; j <i ; j++) {
s.append("a");
}
long end = System.currentTimeMillis();
return end - start;
}
}
结论:
-
通过StringBuilder的append()方式添加字符串的效率,要远远高于String的字符串拼接方法
好处 -
StringBuilder的append的方式,自始至终只创建一个StringBuilder的对象
-
对于字符串拼接的方式,还需要创建很多StringBuilder对象和 调用toString时候创建的String对象
-
内存中由于创建了较多的StringBuilder和String对象,内存占用过大,如果进行GC那么将会耗费更多的时间
java垃圾回收机制
java中采用的是自动垃圾回收,无需开发人员动手清除出垃圾。这样降低内存泄漏和内存溢出的风险。
java堆中的内容是垃圾回收的重点区域
在回收的次数上:
- 频繁收集年轻代
- 较少收集老年代
- 几乎不动元空间
判断一个对象应该被回收
1.该对象没有与GC Roots相连
2.没有引用指向该对象
引用计数算法
引用计算算法是发生在标记阶段。在启动GC之前要先判断哪些是可以回收的垃圾对象
如何判断一个对象已经死亡::当一个对象不再被其他任何一个对象引用时。
**具体实现:**对每个对象保存一个整数型的引用计数器属性,用于记录对象被引用的情况。如果被对象引用就加1,引用玩失效就减1,如果对象的引用计数器的数值为0.则该对象就可以被回收。
**优点:**实现简单,垃圾对象易于辨识,判定效率高
**缺点:**计数器需要占用引用空间。加1减1草书消耗时间,最重要的时它不能解决循环引用这一问题。
可达性分析算法(根搜索算法,追踪性垃圾收集)
- 可达性分析算法时以根对象集合为起点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否可达。
- 使用可达分析算法后,内存中的存活对象都会被根对象集合直接或间接的连接着,如果目标对象没有任何相连,则不可达,该对象已死亡,可被回收。
GC Roots包括以下几类 - 虚拟机栈中的引用对象
- 本地方法栈内引用对象
- 放啊去中类静态属性引用对象
- 方法区中常量引用的对象 如:字符串常量池
- 所有被synchroni持有的对象
垃圾清除阶段算法值标记-清除算法(Mark-Sweep)
**执行过程:**当堆中的有效空间被耗尽的时候,就会停止整个程序(Stop The World),然后进行两项工作标记和清除
**标记:**垃圾收集器引用根节点开始遍历,标记所有被引用的对象
**清除:**垃圾收集器对堆内存从头到尾进行线性遍历,如果某个对象没有标记为可达现象,则将其回收。
**缺点:**效率不高,在进行GC的时候,需要停止整个应用程序,这种方式清理处来的空闲欸村是不连续的。
复制算法
**核心思想:**将活着的内存空间分为两块,每次只使用其中的一块,在垃圾回收时将正在使用的内存的存活的对象复制到未被使用的内存块中。之后清除正在使用的内存快中的所有对象,交换两个内存的角色,最后完成垃圾回收。
**优点:**没有标记和清除的过程,实项简单高效。
**缺点:**需要占用的空间很大,如果存活的对象的很多,需要复制的操作就很多,效率就很低。
标记-压缩算法(标记-整理算法)
**执行过程:**第一阶段和标记-清除算个发一样,从根节点开始标记所有被引用的对象第二阶段将所有的存活对象压缩到内存的一段,按顺序存放。
执行的效果相当于在标记-清除算法中进行了一次清理碎片化的过程,相当于标记-清除-整理算法。
**优点:**解决了标记-清除算法中的内存区域分散问题,消除了复制算法中内存减半的高额代价。
**缺点:**从效率上看,标记-压缩算法的效率低于复制算法,移动对象时,如果对象被引用,还需要改变引用的地址。移动过程中,需要全程暂停用户的应用程序,即STW。
三种清除算法的对比
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。
而为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。
分代收集算法
目前几乎所有的GC都运用了分代收集算法。
在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须解和年轻代和老年代的各自特点。
年轻代:区域相对老年代的较小,对像生命周期短,内火效率低,回收频繁。
老年代:区域打,对象生命周期场,存活率高,回收不频繁
(一般有标记-清除算法和标记-整理算法混合使用)