• 面试题总结


    1.你了解的各种锁?

    偏向锁、轻量级锁、重量级锁

    简单说下先偏向锁、轻量级锁、重量级锁三者各自的应用场景:

    • 偏向锁:只有一个线程进入临界区;
    • 轻量级锁:多个线程交替进入临界区
    • 重量级锁:多个线程同时进入临界区。

    偏向锁

    在没有实际竞争的情况下,还能够针对部分场景继续优化。如果不仅仅没有实际竞争,自始至终使用锁的线程都只有一个,那么维护轻量级锁都是浪费的。偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要执行一次CAS。偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此只需要在Mark Word中CAS记录owner(线程ID),记录成功则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本地直接获得锁;否则说明有其他线程竞争,膨胀为轻量级锁。
    特点:只有等到线程竞争出现才释放偏向锁,持有偏向锁的线程不会主动释放偏向锁。之后的线程竞争偏向锁,会先检查持有偏向锁的线程是否存活,如果不存活,则对象变为无锁状态,重新偏向;如果仍存活,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。

    轻量级锁

    轻量级锁的目标就是减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。

    重量级锁

    等待锁的线程会被阻塞,由于Linux下Java线程与操作系统内核态线程一一映射,所以涉及到用户态和内核态的切换、操作系统内核态中的线程的阻塞和恢复。这种方式的成本非常高,因此后来称这种锁为“重量级锁”。synchronized就属于这种锁。

    轻量级锁什么时候会膨胀到重量级锁?

    若一直是线程交替进入临界区,那么没有问题,轻量锁hold住。一旦在轻量级锁上发生竞争,即出现许多线程同时进入临界区的情况,如果轻量级锁自旋到达阈值后,轻量级锁就hold不住了。 (根本原因是轻量级锁没有足够的空间存储额外状态,此时若不膨胀为重量级锁,则所有等待轻量锁的线程只能自旋,可能会损失很多CPU时间。

    优点缺点适用场景
    偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
    轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程使用自旋会消耗CPU 追求响应时间,锁占用时间很短
    重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,锁占用时间较长


    公平锁和非公平锁

    ReentrantLock、ReadWriteLock默认都是非公平模式,非公平锁的效率为何高于公平锁呢?究竟公平与非公平有何区别呢?

    有人说非公平锁获取锁时各线程的的概率是随机的,这也是一种很不确切的说法。

    在公平的锁上,线程按照他们发出请求的顺序获取锁;但在非公平锁上,则允许‘插队’:

    当一个线程请求非公平锁时,如果在发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中所有的等待线程而获得锁。 

    假设线程A持有一个锁,并且线程B请求这个锁。由于锁被A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此B会再次尝试获取这个锁。与此同时,如果线程C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。 

    在公平的锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个锁,那么新发出的请求的线程将被放入到队列中。

    而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。

     

    非公平锁性能高于公平锁性能的原因:

    在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。

    上文说到的线程切换的开销,其实就是非公平锁效率高于公平锁的原因,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。

     

    悲观锁和乐观锁

    乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

    悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

    乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

    从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

    悲观锁在Java中的使用,就是利用各种锁。

    乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

     

    可重入锁

    可重入锁的概念是自己可以再次获取自己的内部锁。

    举个例子,比如一条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的(如果不可重入的锁的话,此刻会造成死锁)。说的更高深一点可重入锁是一种递归无阻塞的同步机制。 ReentrantLock,Synchronized都是可重入锁。

    好处:可一定程度避免死锁。

     

    独享锁/共享锁

    独享锁是指该锁一次只能被一个线程所持有。

    共享锁是指该锁可被多个线程所持有。

    对于Java ReentrantLock(互斥锁)而言,其是独享锁。

    但是对于Lock的另一个实现类ReadWriteLock(读写锁),其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。

    对于Synchronized而言,当然是独享锁。

     

    分段锁

    分段锁其实是一种锁的设计,并不是具体的一种锁。对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

    我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,Segment继承了ReentrantLock。

    当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。

    分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

     

    自旋锁

    自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁

    好处:减少线程上下文切换的消耗

    缺点:如果某个线程持有锁的时间过长,导致其他获取锁的线程一直循环会消耗CPU。

    场景:一般应用于加锁时间很短(1ms左右或更低)的场景。总之,使用自旋锁必须非常慎重。

     

    互斥锁:

    无法获取琐时,进线程立刻放弃剩余的时间片并进入阻塞(或者说挂起)状态,同时保存寄存器和程序计数器的内容(保存现场,上下文切换的前半部分),当可以获取锁时,进线程激活,等待被调度进CPU并恢复现场(上下文切换下半部分),ReentrantLock和Sychronized线程间都是完全互斥的,一个时刻只能有一个线程获取到锁。

    缺点:上下文切换会带来数十微秒的开销,不要在性能敏感的地方用互斥锁

     

    可中断锁

    synchronized 是不可中断的,Lock 是可中断的,这里的可中断建立在阻塞等待中断,运行中是无法中断的。

     

    锁优化

    以上介绍的锁不是我们代码中能够控制的,但是借鉴上面的思想,我们可以优化我们自己线程的加锁操作;

    减少锁的时间

    不需要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可以让锁尽快释放;

    减少锁的粒度

    它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;

    java中很多数据结构都是采用这种方法提高并发操作的效率:

    1.ConcurrentHashMap

    java中的ConcurrentHashMap在jdk1.8之前的版本,使用一个Segment 数组

    Segment< K,V >[] segments

    Segment继承自ReenTrantLock,所以每个Segment就是个可重入锁,每个Segment 有一个HashEntry< K,V >数组用来存放数据,put操作时,先确定往哪个Segment放数据,只需要锁定这个Segment,执行put,其它的Segment不会被锁定;所以数组中有多少个Segment就允许同一时刻多少个线程存放数据,这样增加了并发能力。

    2.LongAdder

    LongAdder 实现思路也类似ConcurrentHashMap,LongAdder有一个根据当前并发状况动态改变的Cell数组,Cell对象里面有一个long类型的value用来存储值; 
    开始没有并发争用的时候或者是cells数组正在初始化的时候,会使用cas来将值累加到成员变量的base上,在并发争用的情况下,LongAdder会初始化cells数组,在Cell数组中选定一个Cell加锁,数组有多少个cell,就允许同时有多少线程进行修改,最后将数组中每个Cell中的value相加,在加上base的值,就是最终的值;cell数组还能根据当前线程争用情况进行扩容,初始长度为2,每次扩容会增长一倍,直到扩容到大于等于cpu数量就不再扩容,这也就是为什么LongAdder比cas和AtomicInteger效率要高的原因,后面两者都是volatile+cas实现的,他们的竞争维度是1,LongAdder的竞争维度为“Cell个数+1”为什么要+1?因为它还有一个base,如果竞争不到锁还会尝试将数值加到base上;

    3.LinkedBlockingQueue

    LinkedBlockingQueue也体现了这样的思想,在队列头入队,在队列尾出队,入队和出队使用不同的锁,相对于LinkedBlockingArray只有一个锁效率要高;

    拆锁的粒度不能无限拆,最多可以将一个锁拆为当前cup数量个锁即可;

    锁粗化

    大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度; 
    在以下场景下需要粗化锁的粒度: 
    假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的;

    使用读写锁

    ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可以并发读,写操作使用写锁,只能单线程写;

    读写分离

    CopyOnWriteArrayList 、CopyOnWriteArraySet 
    CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。 
     CopyOnWrite并发容器用于读多写少的并发场景,因为,读的时候没有锁,但是对其进行更改的时候是会加锁的,否则会导致多个线程同时复制出多个副本,各自修改各自的;

    使用cas

    如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用volatiled+cas操作会是非常高效的选择;

     

    2.Sleep()和wait()的区别,使用wait()方法后,怎么唤醒线程

    3.Mybatis的缓存

    4.Redis缓存是怎么运用的

    5.Redis有几种数据结构,并且对于hash数据结构,如果要删除数据,那么redis的底层是怎么处理的

    6.用过哪些线程池,说一下常见的四种线程池和区别

    7.反向代理、负载均衡解释一下

    8.AOP的切点怎么切,动态代理

    9.Sping中IOC怎么将类注入到里面去,并且怎么实例化对象

    10.简单介绍一下java的反射机制?反射在哪些地方有应用场景?

    java反射机制在运行状态中,对于任意一个类(class文件),都能知道这个类的所有属性和方法;对于任意一个对象,能够调用它的任意一个方法和属性.这种动态获取的信息以及动态调用对象的方法的动能称为java语言的反射机制。

    //早期:new时候,先根据被new的类的名称找寻该类的字节码文件,并加载进内存,
    //并创建该字节码文件对象,并接着创建该字节码文件的对应的Person对象。
    com.xidian.Person p=new com.xidian.Person();
    //现在
    String name="com.xidian.Person";
    //找寻该文件类文件,并加载进内存,并产生Class对象。
    Class clazz=Class.forName(name);
    //如何产生该类的对象呢?
    Object obj=clazz.newInstance();

    用反射类加载的方式,从表面上看形式较为复杂但是可扩展性却更强。原来需要自己在程序文件手动中创建一个对象,

    现在只用在配置文件中写入字符串,就可以创建对应的对象。

    11.代理模式,静态代理模式?动态代理模式?

    代理模式:

    为某个对象提供一个代理,以控制对这个对象的访问,实现方法的增强。 代理类和委托类有共同的父类或父接口,这样在任何使用委托类对象的地方都可以用代理对象替代。

    优点:

    让委托类专注于业务逻辑的处理

    静态代理模式:

    静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。

    实现方式:

    (1)聚合:代理类聚合了被代理类,且代理类及被代理类都实现了同一接口,则可实现灵活多变

    (2)继承:继承不够灵活,随着功能需求增多,继承体系会非常臃肿。

    缺点:需要为每一个代理方法进行实现,维护复杂。

    动态代理模式:

    就是指动态生成的代理,不需要我们编写,这样就可以解决这个代理类很多的问题,这样会极大地减少了我们的工作。

    在生成动态代理类的时候,Proxy.newProxyInstance(Moveable.class, h)方法里头传入的是代理对象接口和处理方法类;

    有接口类就能获取通过反射获取接口的方法;执行代理对象的每个方法时都会被替换执行InvocationHandler对象的invoke方法,在invoke方法里会反射调用被代理对象的方法,并对该方法进行增强。

    桥接模式

    比如我们设计类的时候,可能从抽象的维度扩展也有可能从具体实现上也会扩展,形成各种各样的排列组合,如果使用继承会显得非常臃肿,桥接模式的脱耦是将抽象画和实现化之间使用关联关系而不是继承关系,是两者可以独立变化。

    以画图为例,可以画正方形、长方形、圆形。但是现在我们需要给这些形状进行上色,这里有三种颜色:白色、灰色、黑色。这里我们可以画出3*3=9中图形:白色正方形、白色长方形、白色圆形。。。。。。

    到这里了我们几乎到知道了这里存在两种解决方案:

          方案一:为每种形状都提供各种颜色的版本。

          方案二:根据实际需要对颜色和形状进行组合。

     通过组合关系,在抽象类中持有实现类的接口引用

    public abstract class Shape {
        Color color;
    
        public void setColor(Color color) {
            this.color = color;
        }
        
        public abstract void draw();
    }

    适配器模式

    把一个类的接口变换成客户端所期待的另一种接口,从而使原本接口不匹配而无法一起工作的两个类能够在一起工作。

    IO中的适配器模式:

    InputStreamReader和OutputStreamWriter类分别继承了Reader和writer接口,但是要创建他们的对象必须在构造函数中传入一个InputStream和OutputStream

    的实例,InputStreamReader和OutputStreamWriter类的作用也就是将InputStream和OutputStream适配到Reader和Writer。

    适配器是InputStreamReader,源角色是InputStream代表的实例对象,目标接口就是Reader类。

    命令模式

    命令模式是对命令的封装。

    命令模式把发出命令的责任和执行命令的责任分割开,A负责发出命令,B负责接收命令并执行。

    策略模式

    策略模式属于对象的行为模式。每一个策略都有一个共同的接口,从而使得它们可以相互替换。

    我理解的策略模式其实本质就是Java的多态思想:用父类型的引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为。

    这个模式涉及到三个角色:

      ●  环境(Context)角色:持有一个Strategy的引用。

      ●  抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。

      ●  具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。

    在这里也是一样,在环境角色中,抽象策略类持有具体策略的引用,会根据传入的不同具体策略类对象而执行不同的策略行为。

    责任链模式

    责任链将处理请求在链条的节点上传递,每到一个节点可被处理或者也可不被处理。

    比如SpringMVC中的Filter就是采用的责任链模式,它有preHandler和postHandler、afterCompletion,只有第一个preHandler执行完返回true才传到下一个preHandler,否则不进行传递,而postHandler也是挨个传递,只是顺序和preHandler相反。

    11. 你设计一个数据库连接池你需要对数据库连接进行怎么样的封装,要设计哪些模块,怎么解决长时间无操作服务器把某连接置为不可用但是客户端还是以为是可用的问题

    12. 连接池是什么

    在JDBC的操作中,打开和关闭数据库连接,是最耗费数据库资源的。连接池基本的思想是在系统初始化的时候,将数据库连接作为对象存储在内存中,当用户需要访问数据库时,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象。使用完毕后,用户也并非将连接关闭,而是将连接放回连接池中,以供下一个请求访问使用。而连接的建立、断开都由连接池自身来管理。同时,还可以通过设置连接池的参数来控制连接池中的初始连接数、连接的上下限数以及每个连接的最大使用次数、最大空闲时间等等,也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。

    最大并发连接数

    spring.druid.maxActive=20

    最小空闲连接数
    spring.druid.minIdle=3

    获取连接等待超时的时间
    spring.druid.maxWait=10000

    用来检测连接是否有效的sql
    spring.druid.validationQuery=SELECT 'x'

    配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    spring.druid.timeBetweenEvictionRunsMillis=60000

    配置一个连接在池中最小生存的时间,单位是毫秒
    spring.druid.minEvictableIdleTimeMillis=300000

    13.线程有几种状态?

    14.说下hashmap?hashmap是插入链表头还是链表尾部?

    15.线程池?参数?当超过最大线程数的时候采取

    1.  GET跟POST的区别是什么?

    (1)       传参方式,GET放在url后面,post放在http的body,GET更不安全

    (2)       参数长度,浏览器对url后面的参数长度有限制,post也有限制,但是post要比get大得多。这是浏览器加的限制,跟Http协议无关

    (3)       GET不能做文件上传,POST可以。

    (4)       以上都是表象,最根本的区别是语义上的区别:GET的语义是请求获取指定的资源。GET方法是安全、幂等、可缓存的(除非有 Cache-ControlHeader的约束)。POST的语义是根据请求报文对指定的资源做出处理,具体的处理方式视资源类型而不同。POST不安全,不幂等,(大部分实现)不可缓存。简单地说GET是获取数据,POST是修改数据。跟Restful还有点区别,Restful规范里面,GET是获取,POST是添加,PUT是修改,DELETE是删除。

     Spring的IOC和AOP?

    spring实际上是一个容器框架,可以配置各种bean(action/service/domain/dao),并且可以维护bean与bean的关系,当我们需要使用某个bean的时候,我们可以getBean(id),使用即可.

    ioc是什么?

    答 :ioc(inverse of controll ) 控制反转: 所谓控制反转就是把创建对象(bean),和维护对象(bean)的关系的权利从程序中转移到spring的容器(applicationContext.xml),而程序本身不再维护.

    DI是什么?

    答: di(dependency injection) 依赖注入: 实际上di和ioc是同一个概念,组件之间依赖关系由容器在运行期决定,spring设计者认为di更准确表示spring核心技术

    以前如果类A要调用类B的方法,以前我们都是在类A中,通过自身new一个类B,然后在调用类B的方法,现在我们把new类B的事情交给spring来做,在我们调用的时候,容器会为我们实例化。

    使用@Autowired注解时,将自动在代码上下文中找到和其匹配(默认是类型匹配)的Bean,并自动注入到相应的地方去。

    SpringIOC实现原理:

    反射+简单工厂模式

    把IOC容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用反射机制,根据配置文件中给出的类名动态地生成对象实例

    Spring AOP

    它利用一种称为"横切"的技术,将多个类的公共行为比如日志、事务封装到一个可重用模块,并将其命名为"Aspect",然后通过配置或者注解的方式来声明该横切逻辑起作用的位置。

    Spring AOP的原理:

    动态代理(利用反射和动态编译将代理模式变成动态的)

    jdk动态代理是需要实现接口的,而CGLIB则不用

    在Spring中,如果目标对象实现了接口,则spring使用jdk 动态代理技术,如果目标对象没有实现接口,则spring使用CGLIB技术.

    SpringMVC的工作流程?

    1.用户向服务器发送请求,请求被Spring 前端控制Servelt DispatcherServlet捕获;

    2. 前端控制器会找到处理器映射器(HandlerMapping),通过HandlerMapping完成url到controller映射

    3.处理器映射器HandlerMapping向前端控制器返回Handler,包括Handler对象以及Handler对象对应的拦截器

    4.DispatcherServlet拿到Handler后,找到HandlerAdapter(处理器适配器),(附注:如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler(...)方法)

    5.  提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。

        在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:
          HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息
          数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中
    6.  Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象;
    7. 根据返回的ModelAndView,接着ViewResolver根据逻辑视图名解析成真正的视图(jsp)
    8. 当得到真正的视图对象后,DispatcherServlet 会利用视图对象对模型数据进行渲染
    9. 客户端得到相应,可能是一个普通的html页面,也可以是xml或者json字符串。
     

    垃圾回收机制

    目的:回收堆内存中不再使用的对象,释放资源
    回收时间:当对象永久地失去引用后,系统会在合适的时候回收它所占的内存

    1. 什么是垃圾呢?

    指所有不再存活的对象。

     常见的判断是否存活有两种方法:引用计数法和可达性分析。

    引用计数法

    为每一个创建的对象分配一个引用计数器,用来存储该对象被引用的个数。当该个数为零,意味着没有人再使用这个对象,可以认为“对象死亡”。但是,这种方案存在严重的问题,就是无法检测“循环引用”:当两个对象互相引用,即时它俩都不被外界任何东西引用,它俩的计数都不为零,因此永远不会被回收。而实际上对于开发者而言,这两个对象已经完全没有用处了。因此,Java 里没有采用这样的方案来判定对象的“存活性”。

    可达性分析

    思路是把所有引用的对象想象成一棵树,从树的根结点 GC Roots 出发,持续遍历找出所有连接的树枝对象,这些对象则被称为“可达”对象,或称“存活”对象。其余的对象则被视为“死亡”的“不可达”对象,或称“垃圾”。

    GC Roots 究竟指谁呢?

    GC Roots 本身一定是可达的,这样从它们出发遍历到的对象才能保证一定可达。那么,Java 里有哪些对象是一定可达呢?主要有以下四种:

    • 虚拟机栈(帧栈中的本地变量表)中引用的对象。
    • 方法区中静态属性引用的对象。
    • 方法区中常量引用的对象。
    • 本地方法栈中 JNI 引用的对象。

    2. 垃圾回收算法

    标记-清除法

        标记阶段:通过可达性遍历进行标记,未被标记的为垃圾对象

        清除阶段:清除所有未被标记的对象

        标记-清理 方案,简单方便 ,但是容易产生 内存碎片。

    标记-整理法

        标记阶段:通过可达性遍历进行标记,未被标记的为垃圾对象

        清除阶段:清除所有未被标记的对象的时候,把所有 存活 对象扎堆到同一个地方

        该方案存活对象多,垃圾少 的情况,它只需要清理掉少量的垃圾,然后挪动下存活对象就可以了。

    复制法

       将原有的内存空间分为两块,每次只是用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,然后清除正在使用的内存块中的对象。

      方案适合 存活对象少,垃圾多 的情况,这样在复制时就不需要复制多少对象过去,多数垃圾直接被清空处理。

    3.Java 的分代回收机制

    Java的堆结构

    新生代区

    存放刚刚创建的对象。这个区域的对象新创建后很快会变成 不可达 的对象,快速死去 。

    这块区域的特点是 存活对象少,垃圾多 。

    老年代区

    存活了一段时间的对象。在新生代经历N次垃圾回收后仍然存活的对象就会被放到老年代区。而且大对象直接进入老年代。

    这块区域存活对象多、垃圾少

    新生代-复制 回收机制

    对于新生代区域,由于每次 GC 都会有大量新对象死去,只有少量存活。因此采用 复制 回收算法,GC 时把少量的存活对象复制过去即可。

    算法的设计:

    将内存区分为Eden、Survivor A、Survivor B 区,其中 Eden 意为伊甸园,形容有很多新生对象在里面创建;Survivor区则为幸存者,即经历 GC 后仍然存活下来的对象。

    1.首先,Eden区最大,对外提供堆内存。当 Eden 区快要满了,则进行 Minor GC,把存活对象放入 Survivor A 区,清空 Eden 区;

    2.Eden区被清空后,继续对外提供堆内存;

    3.当 Eden 区再次被填满,此时对 Eden 区和 Survivor A 区同时进行 Minor GC,把存活对象放入 Survivor B 区,同时清空 Eden 区和Survivor A 区;

    4.Eden区继续对外提供堆内存,并重复上述过程,即在 Eden 区填满后,把 Eden 区和某个 Survivor 区的存活对象放到另一个 Survivor 区;

    5.当某些对象反复 Survive 一定次数后,则进入老年代区

    5.老年代区满了之后就进行Full GC。

    老年代-标记整理 回收机制

    老年代一般存放的是存活时间较久的对象,所以每一次 GC 时,存活对象比较较大,也就是说每次只有少部分对象被回收。

    总结:

    什么时候进行MinGC和FullGC

    MinGC

          MInGC是新生代中的垃圾回收机制,采用的是复制算法。对于一些在新生代区反复GC后存活下来的就会进入老年代。

    FullGC

         FullGC是老年代中的垃圾回收动作,采用标记整理的算法。由于老年代队形几乎都是Survivaor区熬出来的,不会那么容易死掉,因此FullGC不会有Minor GC那么频繁。

  • 相关阅读:
    MVC(一)
    C# 泛型(二)
    C# 泛型(一)
    ASP.NET MVC Razor
    ASP.NET 服务端接收Multipart/form-data文件
    centos(网易163)软件源更换
    xshell中文乱码问题
    centos7修改主机名
    sqlalchemy python中的mysql数据库神器
    mysql 更新与查询(排序 分组 链接查询)
  • 原文地址:https://www.cnblogs.com/xiangkejin/p/9277213.html
Copyright © 2020-2023  润新知