主页

垃圾回收算法

2020-09-27
java

垃圾标记算法 #

  • 引用计数器法

    • 给一个对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
    • 优点:实现简单,垃圾对象便于识别;判定效率高,回收没有延迟性。
    • 缺点:需要单独的字段存储计数器,增加了存储空间的开销;每次赋值都要更新计数器,伴随着加减法的操作,增加了时间开销;引用计数器有一个严重的问题,即无法处理循环引用情况。这是一个致命缺陷,导致java在垃圾回收器中没有使用这类算法。
  • 可达性分析算法

    • 基本思路:以根对象集合(GC Roots)为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
    • GC Roots包括:①虚拟机栈中引用的对象;②类静态属性引用的对象;③方法区中常量引用的对象;④本地方法栈中(Native方法)引用的对象;⑤被synchronized持有的对象;⑥jvm内部的引用。

finalize #

  • 垃圾回收之前总会调用finalize方法,该方法可以被重写:通常是在这个方法中进行一些资源释放和清理的工作。

  • 不要主动调用该方法,该方法的执行时间是没有保障的,它完全由gc线程决定。垃圾回收机制会主动调用该方法。

  • finalize方法只能被调用一次。

  • 对象的三种状态:可触及;可复活;不可触及。

  • 判断一个对象是否可回收,至少经历两次标记过程:

    1. 如果对象到gc roots没有引用链,则进行第一次标记
    2. 进行筛选,判断该对象是否有必要执行finalize方法:①如果对象没有重写finalize方法,或者finalize方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,该对象被判为不可及的;②如果对象重写了finalize方法且还未执行过,那么该对象将被插入到F-Queue队列中,由一个低优先级、虚拟机自动创建的Finalizer线程区执行它;③finalize方法是对象逃脱死亡命运的最后一次机会,稍后GC会对F-Queue中的对象进行第二次标记,如果对象在finalize方法中重新与引用链上的任意一个对象建立了联系,那么在第二次标记时它将被移出“即将回收”的集合。之后,如果该对象再次出现没有引用存在的情况下,finalize方法不会再次调用,对象会直接变为不可及的状态。也就是说一个对象的finalize方法就被调用一次。

垃圾收集算法 #

  • 标记-清除算法(Mark-Sweep)

    • 执行过程:当堆中的有效内存空间被耗尽的时候,就会停止整个程序(STW),然后进行两项工作,第一项是标记,第二项是回收。
    • 标记:从根节点开始,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
    • 清除:对堆内存从头到尾进行线性的遍历,如果发现某个对象在其header中没有被标记为可达对象,则将其回收。
    • 优点:实现简单。
    • 缺点:效率不够高;导致STW;会导致内存空间不连续,产生内存碎片,需要维护一个空闲列表。

标记-清除算法

  • 复制算法

    • 执行过程:将可用内存按容量分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还活着的对象复制到另外一块上面去,然后再把已使用过的内存空间一次清理掉。
    • 优点:实现简单;运行高效,不会出现内存碎片。
    • 缺点:需要两倍的内存空间。
    • 适用场景:存活对象比较少,垃圾对象比较多的场景。

复制算法

  • 标记-压缩(整理)算法
    • 执行过程:第一阶段和标记-清除算法一样,从根节点标记所有被引用的对象。第二阶段是将所有存活对象整理到内存的一端,按顺序排放。之后清理边界外的所有对象。
    • 指针碰撞:如果内存空间以规整、有序的方式分布,即已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配起始点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上,这种分配方式就叫指针碰撞(Bump the Pointer)。
    • 优点:解决了内存碎片化的问题;消除了复制算法中内存减半的问题。
    • 缺点:效率低于复制算法和标记-清除算法;移动对象的同时,还要调整引用的地址;会导致stw。

标记-整理算法

  • 分代收集算法

    • 不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采用不同的收集方式,以提高回收效率。一般把java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
  • 增量收集算法

    • 如果一次将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成。
    • 总的来说,增量收集算法的基础仍然是传统的标记-清除算法和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集器以分阶段的方式完成标记、清理和复制工作。
    • 缺点:线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量下降。
  • 分区算法

    • 将堆空间划分为连续不同的小区间region,每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

String

2020-09-25
java

String的基本特性 #

  • String类的声明为final。

  • String实现了Serializable接口:表明字符串是支持序列化的。实现了Comparable接口:表面字符串是可以比较大小。

  • String再jdk8及以前使用 final char[] value存储字符串数据,jdk9改为byte[]。

  • String的字符串常量池是一个固定大小的hashtable,在jdk7中默认大小是60013,(jdk8中1009是可以设置的最小值),使用-XX:StringTableSize=可以设置StringTable的长度。如果字符串常量池中的字符串非常多,就可能会造成hash冲突,从而导致链表变得很长(链表长度大于8时会转化成红黑树),但还是会导致性能下降(比如在调用intern时)。

  • 字符串的拼接操作

    • 常量与常量的拼接是放在String Pool中,原因是编译期优化。
    • 只要其中有一个是变量,结果就放在堆中。变量拼接的原理是StringBuilder。
    • 如果拼接的结果调用intern方法,则主动将常量池中还没有的字符串放入池中,并返回其地址(如果String Pool中有,则直接返回其地址),下面还要对intern进行讨论。

字符串的拼接操作 #

String s1="a";
String s2="b";
String s3="ab";
String s4=s1+s2;
/*
如果被拼接的字符串中有变量,执行字符串拼接操作会进行如下几个步骤
①StringBuilder s=new StringBuilder();
②s.append(s1); s.append(s2);
③s.toString();
如果要被拼接的字符串中全是常量或者常量引用,则仍然使用编译器优化,不会涉及到上面三步。
*/
System.out.println(s4==s3);//false

部分源码分析 #

//下面这两行代码输出的结果为什么是nullabc呢?一起来分析一下源码吧
String s=null+"abc";
System.out.println(s);//nullabc

//StringBuilder类,当传入一个对象时会将该对象转化成一个字符串
public StringBuilder append(Object obj) {
	return append(String.valueOf(obj));
}
//String类,当该对象为null时,会返回一个"null"字符串,否则返回该对象的toString方法
public static String valueOf(Object obj) {
	return (obj == null) ? "null" : obj.toString();
}

部分考点分析 #

String s=new String("ab");
/*
创建了两个对象
对象1:new String("ab");
对象2:常量池中的"ab";
*/

String s1=new String("a")+new String("b");
/*
创建了6个对象
对象1:new StringBuilder();
对象2:new String("a");
对象3:常量池中的"a";
对象4:new String("b");
对象5:常量池中的"b";
对象6:当append操作结束后,会调用StringBuilder的toString方法,将StringBuilder对象转化为String对象,
	此时又发生了一次new的操作:new String(value, 0, count);
tips:这里虽然创建了6个对象,但实际上在常量池中并没有创建"ab";
*/

intern #

从jdk7开始,当我们调用String对象的intern()方法:

  • 如果常量池中有这个字符串,则返回常量池中该串的地址。
  • 如果常量池中没有该串,则会把对象的引用地址复制一份,放入常量池,并返回常量池中的引用地址。
String s1=new String("ab");
s1.intern();
String s2="ab";
System.out.println(s1==s2);//false;
/*
这是因为一个是堆中的对象,一个是常量池中的对象
*/

String s3=new String("a")+new String("b");
s3.intern();
String s4="ab";
System.out.println(s3==s4);//true(jdk7及以上版本的结果);
/*
这个是不是感觉很匪夷所思?
原因是在创建了s3之后,常量池中并没有"ab"这个对象,
而在执行s3.intern()后,常量池中多了一个指向堆中的对象的指针,
所以当执行s4="ab"时,s4实际上也是指向了堆中创建的那个对象。
为什么这么做呢?指针才占4个字节,用指针省空间。
*/

对象实例化及内存布局

2020-09-25
java

创建对象的步骤 #

  1. 当遇到一条new指令时,首先判断能否在常量池中定位到一个类的符号应用,并检查这个符号引用代表的类是否加载、解析和初始化

  2. 为对象分配内存,对象所需的内存大小在类加载完后就可完全确定

    • 如果内存规整,指针碰撞。
    • 如果内存不规整,虚拟机需要维护一个列表,空闲列表分配。
  3. 处理并发安全问题

    • 采用cas失败重试
    • 每个线程预先分配一块TLAB,通过-XX:+/-UseTLAB参数来设置。
  4. 对象属性初始化,即所有属性设置0值。

  5. 设置对象的对象头。

  6. 属性的显式初始化、代码块中初始化、执行init方法进行初始化。

对象的内存布局 #

  1. 对象头,包括如下信息:

    • 运行时元数据:哈希值,gc分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间戳。
    • 类型指针:指向类元数据的指针,确定该对象是哪个类的实例。
    • 如果是数组,还需记录数组的长度。
  2. 实例数据:它是真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)。

    • 规则:相同宽度的字段总是被分配在一起;父类定义的变量会出现在子类之前;如果compactFields参数为true(默认为true),子类的窄变量可能插入到父类变量的空隙。
  3. 对齐填充

    • 仅仅起着占位符的作用,hotspot虚拟机要求任何对象的大小都必须是8字节的整数倍。

内存布局

访问定位 #

  • 直接指针
  • 句柄访问
    • java堆中会划分出一块内存来作为句柄池,引用中存放的就是对象的句柄地址,句柄中包含了对象实例数据的指针和对象类型的指针。

观察者模式

2020-09-25
设计模式

  • 定义:对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,能够自动通知其他关联对象,自动刷新对象状态。
  • Observer模式提供给关联对象一种同步通信的手段,使某个对象与依赖它的其他对象之间保持状态同步。

类图 #

观察者模式

  • 简单解释一下:在Subject类中维护了一个Observer对象列表,通过调用Subject的Attach方法将对象加入该列表,调用Detach将对象从该列表中移除。Subject中某些事件发生改变时,就会调用Notify方法。Notify方法中有一个循环,它会调用所有在列表中的对象的Update方法,达到通知的目的。

抽象工厂

2020-09-22
设计模式

定义 #

提供一个接口,让该接口负责创建一系列“相关或者相互依赖的对象”,无序指定他们具体的类。

理解 #

  • 一系列相互依赖对象的创建。比如servlet使用 mysql对数据库处理的对象 或者 sqlserver对数据库处理的对象 ,由于mysql对应的类是不能使用sqlserver对应的类的,所以在这里我们就可以使用抽象工厂创建对象。
  • 主要在于应对对“新系列”的需求变动。缺点在于难以应对“新对象”的需求变动。

类图 #

抽象工厂类图

工厂方法

2020-09-22
设计模式

定义 #

定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory Method使得一个类的实例化延迟。

理解 #

  • 使用new违背了依赖倒置原则,导致程序间的紧耦合。
  • 该模式隔离了对象的使用者和具体类型之间的耦合关系。面对一个经常变化的具体类型,紧耦合(new)会导致软件的脆弱。
  • 通过面向对象的手法,将所要创建对象的工作延迟到了子类。从而实现了一种扩展的策略(而非更改),较好的解决了这种紧耦合的关系。

代码 #

//代表所有车
interface Car{
    public void run();
}
//比亚迪
class Biyadi implements Car{
    public void run(){
        System.out.println("比亚迪run");
    }
}
//奥迪
class Aodi implements Car{
    public void run(){
        System.out.println("奥迪run");
    }
}
//工厂
interface CarFactory{
    public void createCar();
}
//具体的实现
class BiyadiCarFactory implements CarFactory{
    public void createCar(){
        return new Biyadi();
    }
}

class AodiCarFactory implements CarFactory{
    public void createCar(){
        return new Aodi();
    }
}

场景 #

//使用工厂方法
public void travel(CarFactory carFactory){
	//我开什么车去旅游全凭其他人给我什么车,假如公司有钱了,可能给我传递了更好的车
    Car car=carFactory.createCar();
    car.run();
}
/*虽然可能还是会有依赖,但是在本实现中依赖基本上已经消失了
其实依赖不可能完全消失,我们使用模式的时候可能只是将散落在程序各个部分的依赖都集中起来*/
//不使用工厂方法
public void travel(){
	//我只能开比亚迪去旅游,不能被更改
    Car car=new BiyadiCar();
    car.run();
}

装饰模式

2020-09-21
设计模式

定义 #

动态(组合)地给一个对象增加一些额外的职责。就增加功能而言,Decorator模式比生成子类(继承)更为灵活(消除重复代码&减少子类个数)。

理解 #

  • java中的各种包装流就是用的装饰模式。
  • 使用组合代替了不好的继承,使类的数量大大大的减少。
  • 解决主体类在多个方向上的扩展功能。

案例 #

有一个类叫OutputStream,其完成一个打印的功能。

现在A想对该类进行扩充,使其有缓冲的功能。

B也想对该类进行扩充,使其输出更加安全。

C也想对该类进行扩充,使其输出到文件中。

这我们都可以通过类继承来完成,并且看上去似乎也没有什么问题。但是假如D也想对该类进行缓冲,使其既有缓冲,又更安全,这时候再使用类继承就不合适了。

此时我们可以使用装饰模式来解决该问题。

类图

interface OutputStream{
    public void print();
}

class ConcreteOutputStream implements OutputStream{
    public void print(){}
}
//装饰器
abstract class Decorator implements OutputStream{
    protected OutputStream  outputStream;
    public Decorator(OutputStream outputStream){
        this.outputStream=outputStream;
    }

    public  void print(){
        outputStream.print();
    }
}
//缓冲
class BufferedOutputStream extends Decorator{

    public BufferedOutputStream(OutputStream outputStream){
        super(outputStream);
    }

    public void print(){
        System.out.println("I am BufferedOutputStream");
        super.print();
    }
}
//安全
class SecureOutputStream extends Decorator{

    public SecureOutputStream(OutputStream outputStream){
        super(outputStream);
    }

    public void print(){
        System.out.println("I am SecureOutputStream");
        super.print();
    }
}
//输出到文件
class FileOutputStream extends Decorator{

    public FileOutputStream(OutputStream outputStream){
        super(outputStream);
    }

    public void print(){
        System.out.println("I am FileOutputStream");
        super.print();
    }
}

策略模式

2020-09-21
设计模式

  • 定义:定义一系列算法,把他们一个个封装起来,并且使它们可以互相替换(变化),该模式使得算法可独立于使用他的客户程序而变化。
  • 在软件构建过程中,某些对象使用的算法可能多种多样,策略模式做到了运行时根据需要透明的更改对象的算法,将对象与自身解耦。
  • 比如税率,如果要求一个程序支持多国税率,可以使用if-else语句。
class Sale{
    public double buySomething(){
        double d=xxx;
        //...
        if(tax==ChineseTax){
            //...
        }else if(tax==AmericeTax){
            //...
        }elseif(tax==JapanTax){
            //...
        }
        //...
    }
}
  • 但是如果后期需要加上其他国家的税率,就要修改程序,违背了开闭原则。此时可以使用策略模式。实现如下:
interface Tax{
    public double calculate(double money);
}

class ChineseTax implements Tax{
    public double calculate(double money){
        //...
    }
}
class AmericaTax implements Tax{
    public double calculate(double money){
        //...
    }
}

class Sale{
    private Tax tax;
    public Sale(Tax tax){this.tax=tax;}
    
    public double buySomething(){
        double d=xxx;
        //...
        tax.calculate(d);//替换掉先前的if-else
        //...
    }
}