刷题心得(1)

抽象类与接口的区别

比较抽象类接口
默认方法有默认的方法可以实现1.8之前,接口不存在方法的实现
实现方式实现抽象类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现。子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现
构造器
与正常类的区别不能被实例化不同的类型
访问权限修饰public、protected和defaultpublic
多继承一个子类存在一个父类一个子类可以存在多个接口
添加新方法抽象类中添加新方法,可以提供默认的实现不用修改子类的代码接口中添加新方法,子类需要实现改方法
方法体抽象方法不能有方法体,子类可以不覆写父类的抽象方法,但子类也要申明为抽象类;子类可以选择覆写父类的非抽象方法

例子
在JDK1.8之前的版本(不包括JDK1.8),接口中不能有静态方法,抽象类中因为有普通方法,故也可以有
静态方法。
在JDK1.8后(包括JDK1.8),在抽象类中依旧可以有静态方法,同时在接口中也可以定义静态方法了。
以下代码在JDK1.8之后是没有问题的(可以通过接口名来调用静态方法 :Main.prinf(); ):

public interface Demo{
    public static void print() {         
      System.out.println("Hello World!");      }
}

PS:
在JDK1.7,接口中只包含抽象方法,使用public abstract 修饰。

public interface Demo{
    public abstract void method();
}

在JDK1.8,接口中新加了默认方法和静态方法:
默认方法:使用default修饰,在接口的实现类中,可以直接调用该方法,也可以重写该方法。
静态方法:使用static修饰,通过接口直接调用。

public interface Demo{
    //默认方法
    public default void method(){
        System.out.println("default method...");
    }
 
    //静态方法
    public static void print(){
        System.out.println("static method...");
    }
}

在JDK1.9,接口中新加了私有方法,使用private修饰,私有方法供接口内的默认方法调用。
复制代码

public interface Demo{
    private void method() {
        System.out.println("Hello World!");
    }
}

JVM垃圾回收机制

1、在java中,对象的内存在哪个时刻回收,取决于垃圾回收器何时运行。
2、一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法, 并且在下一次垃圾回收动作发生时,才会真正的回收对象占用的内存(《java 编程思想》)
3、在C++中,对象的内存在哪个时刻被回收,是可以确定的,在C++中,析构函数和资源的释放息息相关,能不能正确处理析构函数,关乎能否正确回收对象内存资源。在java中,对象的内存在哪个时刻回收,取决于垃圾回收器何时运行,在java中,所有的对象,包括对象中包含的其他对象,它们所占的内存的回收都依靠垃圾回收器,因此不需要一个函数如C++析构函数那样来做必要的垃圾回收工作。当然存在本地方法时需要finalize()方法来清理本地对象。在《java编程思想》中提及,finalize()方法的一个作用是用来回收“本地方法”中的本地对象
4、线程的释放是在run()方法结束以后,此时线程的引用可能并未释放

详解

垃圾回收包含的内容不少,但顺着下面的顺序捋清知识也并不难。首先要搞清垃圾回收的范围(栈需要GC去回收吗?),然后就是回收的前提条件如何判断一个对象已经可以被回收(这里只重点学习根搜索算法就行了),之后便是建立在根搜索基础上的三种回收策略,最后便是JVM中对这三种策略的具体实现。

  1. 范围:要回收哪些区域?

Java方法栈、本地方法栈以及PC计数器随方法或线程的结束而自然被回收,所以这些区域不需要考虑回收问题。Java堆和方法区是GC回收的重点区域,因为一个接口的多个实现类需要的内存不一样,一个方法的多个分支需要的内存可能也不一样,而这两个区域又对立于栈可能随时都会有对象不再被引用,因此这部分内存的分配和回收都是动态的。

  1. 前提:如何判断对象已死?
    (1)引用计数法
    引用计数法就是通过一个计数器记录该对象被引用的次数,方法简单高效,但是解决不了循环引用的问题。比如对象A包含指向对象B的引用,对象B也包含指向对象A的引用,但没有引用指向A和B,这时当前回收如果采用的是引用计数法,那么对象A和B的被引用次数都为1,都不会被回收。
    下面是循环引用的例子,在Hotspot JVM下可以被正常回收,可以证实JVM
    采用的不是简单的引用计数法。通过-XX:+PrintGCDetails输出GC日志。
public class ReferenceCount {  
    final static int MB = 1024 * 1024;  
    byte[] size = new byte[2 * MB];  
    Object ref;  

      

    public static void main(String[] args) {  

        ReferenceCount objA = new ReferenceCount();  

        ReferenceCount objB = new ReferenceCount();  

        objA.ref = objB;  

        objB.ref = objA;  

          

        objA = null;  

        objB = null;  

          

        System.gc();  

        System.gc();  

    }  

  

}  

(2)根搜索
通过选取一些根对象作为起始点,开始向下搜索,如果一个对象到根对象不可达时,则说明此对象已经没有被引用,是可以被回收的。可以作为根的对象有:栈中变量引用的对象,类静态属性引用的对象,常量引用的对象等。因为每个线程都有一个栈,所以我们需要选取多个根对象。
附:对象复活
在根搜索中得到的不可达对象并不是立即就被标记成可回收的,而是先进行一次标记放入F-Queue等待执行对象的finalize()方法,执行后GC将进行二次标记,复活的对象之后将不会被回收。因此,使对象复活的唯一办法就是重写finalize()方法,并使对象重新被引用。

public class DeadToRebirth {  

  

    private static DeadToRebirth hook;   

      

    @Override  

    public void finalize() throws Throwable {  

        super.finalize();  

        DeadToRebirth.hook = this;  

    }  

      

    public static void main(String[] args) throws Exception {  

        DeadToRebirth.hook = new DeadToRebirth();  

        DeadToRebirth.hook = null;  

        System.gc();  

        Thread.sleep(500);  

        if (DeadToRebirth.hook != null)  

            System.out.println("Rebirth!");  

        else  

            System.out.println("Dead!");  

          

        DeadToRebirth.hook = null;  

        System.gc();  

        Thread.sleep(500);  

        if (DeadToRebirth.hook != null)  

            System.out.println("Rebirth!");  

        else  

            System.out.println("Dead!");  

    }  

      

}  

要注意的两点是:

第一,finalize()方法只会被执行一次,所以对象只有一次复活的机会。

第二,执行GC后,要停顿半秒等待优先级很低的finalize()执行完毕。

  1. 策略:垃圾回收的算法
    (1)标记-清除

前面两种策略的缺点很明显,回收后内存碎片很多,如果之后程序运行时申请大内存,可能会又导致一次GC。虽然缺点明显,这种策略却是后两种策略的基础。正因为它的缺点,所以促成了后两种策略的产生。

(2)标记-复制

将内存分为两块,标记完成开始回收时,将一块内存中保留的对象全部复制到另一块空闲内存中。实现起来也很简单,当大部分对象都被回收时这种策略也很高效。但这种策略也有缺点,可用内存变为一半了!

为什么不是全部呢?如果回收时,空闲的那一小块Survivor不够用了怎么办?这就是老年代的用处。当不够用时,这些对象将直接通过分配担保机制进入老年代。那么老年代也使用标记-复制策略吧?当然不行!老年代中的对象可不像新生代中的,每次回收都会清除掉大部分。如果贸然采用复制的策略,老年代的回收效率可想而知。

(3)标记-整理

根据老年代的特点,采用回收掉垃圾对象后对内存进行整理的策略再合适不过,将所有存活下来的对象都向一端移动。

  1. 实现:虚拟机中的收集器

(1)新生代上的GC实现

Serial:单线程的收集器,只使用一个线程进行收集,并且收集时会暂停其他所有工作线程(Stop the world)。它是Client模式下的默认新生代收集器。

ParNew:Serial收集器的多线程版本。在单CPU甚至两个CPU的环境下,由于线程交互的开销,无法保证性能超越Serial收集器。

Parallel Scavenge:也是多线程收集器,与ParNew的区别是,它是吞吐量优先收集器。吞吐量=运行用户代码时间/(运行用户代码+垃圾收集时间)。另一点区别是配置-XX:+UseAdaptiveSizePolicy后,虚拟机会自动调整Eden/Survivor等参数来提供用户所需的吞吐量。我们需要配置的就是内存大小-Xmx和吞吐量GCTimeRatio。

(2)老年代上的GC实现

Serial Old:Serial收集器的老年代版本。

Parallel Old:Parallel Scavenge的老年代版本。此前,如果新生代采用PS GC的话,

老年代只有Serial Old能与之配合。现在有了Parallel Old与之配合,可以在注重吞吐量

及CPU资源敏感的场合使用了。

CMS:采用的是标记-清除而非标记-整理,是一款并发低停顿的收集器。但是由于

采用标记-清除,内存碎片问题不可避免。可以使用-XX:CMSFullGCsBeforeCompaction

设置执行几次CMS回收后,跟着来一次内存碎片整理。

  1. 触发:何时开始GC?

Minor GC(新生代回收)的触发条件比较简单,Eden空间不足就开始进行Minor GC回收新生代。而Full GC(老年代回收,一般伴随一次Minor GC)则有几种触发条件:

  • 老年代空间不足

  • PermSpace空间不足

  • 统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间

这里注意一点:PermSpace并不等同于方法区,只不过是Hotspot JVM用PermSpace来实现方法区而已,有些虚拟机没有PermSpace而用其他机制来实现方法区。

6.补充:对象的空间分配和晋升

(1)对象优先在Eden上分配

(2)大对象直接进入老年代

虚拟机提供了-XX:PretenureSizeThreshold参数,大于这个参数值的对象将直接分配到

老年代中。因为新生代采用的是标记-复制策略,在Eden中分配大对象将会导致Eden区

和两个Survivor区之间大量的内存拷贝。

(3)长期存活的对象将进入老年代

对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度

(默认为15岁)时,就会晋升到老年代中。

多线程与锁

一个是run和start区别,Thread.run()是调用方法,Thread. start()是启动线程;

另一个是锁持有问题。这个题是调用方法,和多线程就无关。本题只有一个线程,持有HelloSogou.class

锁。那么,就是另一个问题:同步方法调用另一个同步方法的锁问题?

public synchronized void methodA(int a, int b){}
public synchronized void methodB(int a){
     methodA(a, 0);
}

首先要明白两个问题,1.锁的对象是谁?2.谁持有了锁?

第一点是run与start方法区别,调用run时只是单纯执行方法,故按代码顺序执行下来结果就是SogouHello。

接着我们再来看看把run换成start,结果是HelloSogou,这是为什么呢。

首先调用start是开始了一个线程,那么现在程序中有了两个线程主线程main和线程T。

这就涉及本题第二个知识点了,函数使用的锁是this(即对象本身),若函数被static修饰则锁为 类

名.class.

那么题目中两个函数都是使用了同一个锁即HelloSogou.class,当执行t.start时,t线程准备调用Sogou

方法,但是锁对象已被主线程占用,故要等待主线程执行完System.out.print(“Hello”)后释放锁才可以

执行自己的Sogou方法。故此结果是HelloSogou

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值