learning, progress, future.

skydh


  • 首页

  • 归档

线程安全和锁优化

发表于 2018-05-16

线程安全的实现方法

互斥同步

  这是个常见的并发正确性保障手段,同步是指多个线程并发访问数据时,该数据同一时刻只有一个线程使用,而互斥则是实现同步的方式之一,临界区,互斥量,信号量都是实现互斥的方式。互斥是方法,同步是目的。
  在java中,实现同步的方式是synchronized关键字。这个关键字需要一个对象参数,来指明要锁住的对象,如果没有明确指定,那就根据synchronized修饰的实例方法或者类方法来取得相应的对象实例或者Class对象作为锁对象。获取到锁对象时,把锁的计数器加一,退出时把锁的计数器减一。前面说过java的线程是映射到操作系统的原生线程上的,如果阻塞和唤醒其他线程,都需要操作系统帮忙。需要从用户态转换到核心态,这种状态转换会很消耗时间,因此这个关键字是重量级操作。当然虚拟机自身也做了锁优化。
  出了synchronized这个关键字,还可以使用java.util.concurrent包下的重入锁来实现同步。特性都一样,都具有线程重入性,但是代码上有些区别,一个是API曾变的互斥锁,lock(),unlock()配合try finally使用,一个是原生语法上的,但是相对于synchronized,多了些特性:
  1.等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃,改为处理其他事情。
  2.公平锁,指的是多个线程等待同一个锁时,必须按照申请锁的时间顺序来获取锁,但是非公平的不是按照时间申请顺序来的,他是随机的,synchronized是非公平的,ReentrantLock默认也是非公平的,但是可以通过构造函数来改变。
  3.锁绑定多个条件:一个ReentrantLock对象可以绑定多个Condition对象,但是synchronized的wait和notify是一对一的
  性能对比:
  在jdk1.5,synchronized关键字效率比ReentrantLock要低,随之线程数目的增多,但是在1.6之后,2者就差不多了。

非阻塞同步

  上面的都是阻塞同步,阻塞同步是悲观的并发策略,认为不加锁,一定会出现问题。而非阻塞则是乐观加锁,先操作。如果没有线程冲突,成功,有冲突,不断重试。但是这个需要硬件指令的原子操作才能完成。现代处理器出现了 CAS(比较并交换)这是一个原子操作。来帮助实现了非阻塞加锁。就是需要写入时,先比较这个值是否变化,若是没变化就写,有变化不写。
  在jdk1.5之后,java才可以,比如原子类之类的,里面的加减操作都是原子操作。就是,对这个变脸不加锁,谁用都可以,但是你要写入的时候,就要对比,是不是你原来读取的旧值,是的话就写入,不是,就不写入,当然这是针对自身的值变化操作,比如自增之类的。当然这个还是有bug的,比如ABA问题。前面的文章写过了,这里就不多说了。但是JUC包加了一个带标记的原子引用类,来控制变量的版本来保证CAS的正确性,当然,如果可能发生ABA,那么还不如用互斥同步操作来的高效。

锁优化

  在JDK1.6有个重大改进,就是虚拟机实现了大量的锁优化技术,比如,适应性自旋,锁清楚,锁粗化,轻量级锁,偏向锁等。

自旋锁和自适应锁

  我们在前面说到,同步互斥对性能最大的影响是阻塞的实现,因为阻塞需要将操作转到内核去完成,这些操作很耗时,也对系统的并发性能有压力。但是虚拟机开发团队发现,共享数据的锁只会持续很短的一段时间。为了这么短的时间去挂起和恢复线程划不来。自旋锁就是如果物理机器上有1个以上的处理器,能让2个以上的线程并行执行,我们可以让后面请求锁的线程稍等一下,但是不放弃处理器的执行时间,看看持有锁的线程是否释放锁,我们只需要让线程执行一个忙循环(自旋),这就是自旋锁。
  自旋锁不能代替同步阻塞,1,处理器数量,2,占用处理器时间。但是如果锁占用时间极短,那么自旋效果就很好了,如果时间长,就不好了,因此要设置一个值,来判断自旋次数,如果在次数内,没话说,超过次数了,那么就只能按照传统的方式去挂起线程了,默认是10次,可以通过参数修改。
  在JDK1.6之后引入了自适应自旋锁。自适应那就是说时间不固定了,由前一次同一个锁上的自旋时间以及锁的拥有者的状态决定的,如果同一个锁对象,自旋刚成功过,那么虚拟机认为这次自旋也可能成功,进而允许自旋锁自旋更长的时间。如果一个锁自旋很少成功,那么虚拟机可能自动忽略这个自旋过程。

锁清除

  就是对一些代码上要求同步,但是被检测到不可能存在共享数据存在数据竞争的锁进行消除。

锁粗化

  原则上,建议同步块的作用范围越小越好,但是也有例外。就是对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中。

轻量级锁

  传统的互斥锁就是重量级锁,轻量级锁不是来代替重量级锁的,他的本意是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。
  这就涉及到对象头了,对象头分为2部分信息,第一部分是存储对象自身运行时数据,一部分是指向方法区对象类型的指针。第一部分有个锁标志位,当代码进入到同步块时,如果该对象没有被锁定,虚拟机将在当前线程的栈帧里面建立一个锁空间,用于存放目前对象的对象头的第一部分的copy称为Lock Record,然后将这个对象头的第一部分跟新为指向Lock Record的指针,且锁标志位跟新为00,,如果失败就是被其他线程抢占了,如果有2个以上的线程争取一个锁,那么轻量级锁无效。
  其实就是在无竞争的情况下用CAS来消除互斥量。

偏向锁

  意思就是如果该锁没有被其他线程获取,那持有偏向锁的线程永远不需要同步。

小结

  偏向锁、轻量级锁、用于不同的并发场景:
  偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
  轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争

java和线程

发表于 2018-05-15

线程的实现

  线程是CPU调度的基本单位,正如前面说的,一个进程的线程可以共享进程资源。也有自己独立的工作区间。在java中,Thread类的关键方法全是Native的。下面是实现线程的3种方式。

使用内核线程实现

  内核线程就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过线程进行调度,负责将线程任务映射到各个处理器上,每个线程可以视为内核的一个分身,这样操作系统就有能力同时处理很多事情,支持多线程的内核叫多线程内核。但是程序一般不会直接使用内核线程,而是使用内核线程的高级接口,轻量级进程,每个轻量级进程都有一个内核线程支持,也是一对一的关系。但是这个轻量级线程是基于内核线程实现的,因此很浪费系统资源。

用户线程实现

  一个线程不是内核线程那就是用户线程。狭义上来说就是建立在用户空间的线程库上,用户线程的建立,同步,销毁,调度完全在用户态执行。不需要内核的帮助,由于不需要切换到内核态,因此操作是快速且低消耗的,可以支持规模大的线程熟练。缺点是,没有内核的支援,线程的创建,切换,等都需要用户程序自己解决。

用户线程+轻量级线程混合

  把上面的2个一起使用,线程还是创建在用户空间里面,而轻量级线程则作为用户线程和内核线程的桥梁。这样就可以使用内核提供的线程调度功能

java线程的实现

  JDK1.2之前是用户线程实现的,1.2中线程魔性替换为基于操作系统原生线程模型。至于现在是什么模型,看虚拟机自身的实现。

java线程的调度

  线程调度分为2种方式:协同式线程调度,抢占式线程调度。
  协同式线程调度:线程的执行时间由线程本身来控制,线程自身的工作执行完了之后,要主动通知系统切换到另外一个线程上,优势是实现简单,而且线程执行完还要通知其他线程,切换操作对开发来说是可知的,所以没有线程同步问题,但是线程执行时间不可控制,如果一个线程代码有问题就会导致这个系统奔溃。
  抢占式调度:每个线程由系统来分配执行时间,线程的切换不由本身决定,在这种调度下,线程的执行时间是系统控制的,不会存在一个线程一直阻塞而导致整个系统奔溃。
  在java中,虽然线程调度是系统自动完成的,但是我们可以设置线程的优先级,但是这种优先级是不靠谱的,因为java线程的实现最终是通过映射到系统线程来实现的。优先级可能对应不上去。

java线程状态转换

  请看前面章节有介绍

java内存模型

发表于 2018-05-15

主内存和工作内存

  主内存是所有变量存储的地方,工作内存是当前线程的中要使用变量的主内存的副本,线程对变量的操作就在工作内存里面完成,不能直接修改主内存的变量,不同线程不能直接读写对方工作内存的变量,之间交互必须通过主内存来完成。

内存之间的交互操作

  虚拟机定义了8种原子操作来完成工作内存和主内存的交互。
  lock:将主内存中一个变量标识为一个线程独占的状态。
  unlock:将主内存中被锁住的变量解锁。
  read:将一个变量值从主内存中传输到线程的工作内存中去。
  load:将read操作读取的值放入到工作内存的变量副本中去。
  use:将工作内存中一个变量的值传递给执行引擎,当虚拟机需要使用时就会执行这个操作。
  assign:将执行引擎的值赋值给工作内存的变量。
  store:将工作内存中的值传到主内存中去。
  write:将从工作内存中得到的值放入到主内存的变量中。
  同时有以下规则:
  1.不许read和load,store和write分开执行。
  2.不许线程丢弃assign操作,也就是在工作内存中改变了值,必须同步到主内存中去。
  3.不许一个线程在没有发生assign的操作下就将数据从工作内存同步到主内存中去。
  4.一个变量在同一时刻只许一个线程对其lock.
  5.如果对一个变量lock,就会清空工作内存中该变量的值,在执行引擎使用这个变量前,需要重洗load和assign初始化这个变量的值。
  6.不许对其他线程或者没有lock的线程执行unlock。
  7.对变量执行unlock之前要先将变量同步到主内存中去。

volatile关键字

  可见性:当一个线程修改这个变量的值后,新值对其他变量是可见的。
  禁止指令重排序:指令重排序是在单线程下优化字节码的执行顺序,但是在多线程下可能出现问题,可以看本博客的单例模式下2重锁问题的解释为何要加volatile关键字。这个关键字通过内存屏障来禁止指令重排序。对于加锁和这个关键字,能用这个关键字的就不加锁,看你的业务场景吧。
  这个关键字通过以下规则保证可见性和禁止重排序。
  1.每次使用volatile变量时必须先从主内存中刷新读取最新的值。
  2.每次修改volatile变量时,必须立刻同步数据到主内存。

  

虚拟机字节码执行引擎

发表于 2018-05-14

java到底是解释语言还是编译语言

  java他是个混合类型的语言,他解释一行行代码执行,同时将反复执行的热点代码,以方法为单位即时编译。因为一个程序里面执行最多的往往是那20%的代码。

不同的即时编译器

  java有多款即时编译器。C1,C2,Graal,这几款是为了在编译时间和运行效率取个均衡。C1是编译时间短,同时性能差,C2正好和C1相反,编译时间稍长,但是性能更高。
  从 Java 7 开始,HotSpot 默认采用分层编译的方式:热点方法首先会被 C1 编译,而后热点方法中的热点会进一步被 C2 编译。在计算资源充足的情况下,字节码的解释执行和即时编译可同时进行。编译完成后的机器码会在下次调用该方法时启用,以替换原本的解释执行。

运行时栈帧结构

  java代码加载是放到方法区的,但是在运行过程中,每调用一个方法,就生成一个栈帧。栈帧是用于支持虚拟机进行方法调用的和方法执行的数据结构,是虚拟机运行时数据区的虚拟机栈的栈元素,栈帧存储了方法的局部变量表,操作数栈,动态链接,方法返回地址等。
  每一个栈帧需要多大的内存在编译期间就确认了。

局部变量表

  在其,存放方法参数和方法内部定义的局部变量。

操作数栈

  也叫操作栈,是一个后入先出栈,当一个方法开始执行时,操作数栈是空的,在执行过程中,各种字节码指令会往操作数栈写入和提取内容。

动态连接

  每个栈帧都包含一个指向运行时常量池的引用,在Class文件中会有大量符号引用,一部分会在类加载时就直接转换为直接引用,一部分是在运行时转换为直接引用。

方法返回地址

  方法有2个方式退出,第一个是执行引擎遇到返回的字节码指令,正常退出。第二个就是遇到了异常,且没有被处理也会退出,这个是异常完成退出。无论哪种退出都要返回到方法被调用的位置,程序才能继续执行,正常退出时才会有这个值,异常退出时,一般不会保存这个值。

附加信息

  一些不在规范里面描述的信息,例如调试相关的信息,一般吧动态连接,方法返回地址。

方法调用

  在java中没有传统的连接步骤,存放在Class文件的是符号引用。

解析

  在类加载的解析阶段,会将一部分符号引用变成直接引用。这一部分是有要求的,就是方法调用在编译器就确认好了,不会发生变化的调用。
  在java中符合这个规范(编译器可知,运行期不可变的方法)是静态方法属于这个类,私有方法,外部不可访问,这2个类型的方法注定不会被重写或者继承产生其他版本。因此是在编译器就确认好了。

分派

静态分派

  A a=new B();其中B继承A,其中这个A是静态类型,这个B是实际类型。静态类型编译期间就可以知道确认,而实际类型必须在运行期间在可以确认。编译器不知道一个对象的实际类型是什么,在编译期间。
  在重载中

public void test(A a){...} ...1
public void test(B b){...} ...2

  如果传入A a=new B();把这个对象当参数穿进去,那么调用的是第一个方法,为何?因为这个虚拟机在重载时根据的是参数的静态类型进行判断的,而静态类型却是在编译期间就可以知道的,因此在编译期间就将符号引用确认了。凡是通过静态参数确认的都是静态分配。

动态分配

  动态分配和java中的重写密切相关。动态分配哈java字节码的一些指令相关度很高,他就是更具对象的实际类型类指定的,和对象实际类型相关。这是和java字节码的解析相关的,字节码就是这么解析的。   

jvm类加载器

发表于 2018-05-11

类与类加载器

  对于任意一个类,都是需要加载它的类和其本身确认其在java虚拟机中的唯一性。每一个类加载器都有一个独立的类名称空间。换句话说如果在一个虚拟机中,2个类来源于一个Class文件,但是加载他们的类加载器不一样,那么这2个类也不一样。比如我们自己实现一个类加载器,并且实例化,那么这个对象确实是这个类实例化出来的,但是Class类型却和默认加载出来的Class类型不一样。
  从虚拟机角度讲,虚拟机分为2个,一个是C++语言实现的,是虚拟机的一部分,一部分是java代码实现的。全部继承于java.lang.ClassLoader.
  从开发角度来看,分为3种。
  1.启动类加载器
  这个类加载器是将\lib目录下,或者-Xbootclasspath参数所指定的路径中,且被虚拟机识别的类库加载到虚拟机内存里面。开发者无法直接使用。
  2.扩展类加载器
  这个加载器主要是加载\lib\ext目录下的。开发者可以使用。
  3.应用类加载器
  这个类加载器负责加载Classpath路径下的所指定的类库,也就是用户一般的类,如果代码没有明确定义只用自己的类加载器,那么这个就是程序默认的类加载器。
  4,用户自定义的类加载器
  这个就是开发者自己定义开发的类加载器。

双亲委派原则

  除了启动类加载器外,其余的的类加载器都有自己的父类加载器。加载器的顺序上下就是上面的顺序。其父子关系不是通过继承实现的,而是通过组合实现的。
  该原则是当改类加载收到请求时,他是不会先加载,而是委托给父类加载器,不断委托,当父类加载器无法在其搜索范围内找到这个类时,才会委托给下级加载器。这是个规范,系统的默认是这个,而自定义的类加载器要自己实现。
  使用这个有个好处。就是java类跟随类加载器有了一种优先级的层次关系了,比如Object类存放在\lib目录下,就默认只能是启动类加载器去加载的了,那么这个程序中就只会有一个Object类,程序也更加稳定。

jvm类加载机制

发表于 2018-05-10

类加载时机

  类的生命周期为:加载,连接(验证,准备,解析),初始化,使用,卸载。这5大阶段。
  其中,加载,验证,准备,初始化,卸载,这5个阶段的顺序是固定的,解析是可以在初始化之后的。
  对于类的加载,由虚拟机自行把控,但是类的初始化不是的,虚拟机严格规定了5种情况下必须对类进行初始化。
  1.使用new 关键字实例化对象的时候,读取或设置一个类的静态字段,调用一个类的静态方法。
  2.通过java.lang.reflect包对类进行反射调用的时候。
  3.初始化一个类,其父类还没被初始化时,父类必须先进行初始化。
  4.包含main()方法的类,虚拟机会优先进行初始化。
  5.jdk1.7以后的动态语言支持的。
  以下情况不会触发类的初始化。没有触发上面5种情况。
  1.通过子类调用父类的静态变量。不会对子类进行初始化。
  2.通过数组定义来引用类,A[] a=new A[10];A这个类不会触发初始化。
  3.引用类的静态常量时不会触发初始化,因为这个量存放到了常量池里面。这个常量的引用会在编译期间进行传播优化,会将这个常量存到NOInitiazation类的常量池中以后这个类对常量的访问都会被转换为对NOInitiazation类的常量池的引用,因此不会对该类进行初始化。

加载

  加载要做3个事情。
  1.通过一个类的全限定名来获取定义此类的2进制字节流
  2.将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。
  3.生成一个java.lang.Class对象来代表这个类,作为访问这个类的入口。
  第一件事就是很开放的有以下方式读取。
  1.从zip包中读取,比如从jar,war读取。
  2.网络中读取,比如Applet.
  3.运行时动态生成,比如java很经典的动态代理。
  4.由其他文件生成
  我们可以用虚拟机自身的类加载器来加载,也可以用我们自身的类加载器来获取字节流。
  数组类的加载与众不同。
  1.如果数组的组件类型是引用类型。数组将在加载这个类型的类加载器的类名称空间\上标示。
  2.如果数组的组件类型不是引用类型,是常见类型,那么虚拟机会把数组标记为与引导类加载器关联。
  类加载完成后将其放入java内存的中的方法区中(前面提到过java运行时数据分布存储),然后在java内存中(不一定是java堆)创建一个java.lang.Class类对象。

验证

  验证Class文件字节流是否满足当前虚拟机要求。
  1.文件格式验证
  2.元数据验证,主要判断是否符合java语法。
  3.字节码验证,保证程序语法是符合逻辑的。
  4.符号引用验证,对类自身信息外的验证,看看相关类是否可以找到之类的。

准备

  准备阶段为类变量分配内存,并且赋初始值。所需的内存从方法区里面获取,比如 public static int value=123;中,初始值为0,boolean为false,而123则是在初始化时才赋值上去。当然如果是final类型的量,那么是可以在准备阶段赋值123的。

解析

  解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
  符号引用:一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要在使用时可以定位到目标即可,符号引用和虚拟机的内存布局无关,引用的目标不一定加载到内存中。比如,在java编译时,java类不知道所引用类的实际地址,只能用符号引用来代替。
  直接引用就是直接指向目标的指针,相对偏移量,或者句柄等。

初始化

  类的初始化就是类加载的最后一步,也是执行类加载的最后一部分,初始化也就是执行类加载方法的过程。
  1.类加载方法会自动收集所有类变量的赋值动作,和静态代码块合并成的。
  2.类构造方法和类构造函数不一样,他不需要显示调用父类的构造函数,虚拟机会自动保证父类的类构造方法先执行。因此java.lang.Object类的类构造方法最先执行。
  3.类构造方法不是必须的,如果类或者接口里面没有静态代码块,那就没有类构造方法。
  4.接口不能有静态代码块,但是可以有静态变量,所以接口和类一样也会生成类构造方法,但是只有当负借口中定义的变量被使用时,父接口才会初始化。
  5.虚拟机保证一个类在多线程环境中被正确的加锁,同步。   

jvm内存分配策略

发表于 2018-05-09

对象优先在新生代分配

  就是新生的对象按照原则一般在新生代内存区给其分配空间。

大对象直接进入老年态

  大对象是指需要大量连续的内存空间的对象,比如非常长的字符串或者数组。因为出现这些对象意味着虚拟机要进行一次垃圾回收才可以安置这些对象,可以在虚拟机启动的时候设置 -XX pretenuresizethreshold 参数 来设置大于这个参数的大对象直接进入老年态。

长期存活的对象将进入老年态

  既然虚拟机采用了分带管理的策略来管理内存,那么如何判断一个对象时应该在老年态还是新生态中。虚拟机给每个对象设置了一个年龄计算器,当对象在Eden出生,并且活过了第一次垃圾回收,那么该对象将进入到Suivivor空间,年龄也会增加一,妹熬过一次就加一,当达到一个阈值时,就会被移动到老年态中,这个阈值默认是15,也可以通过参数配置和设置。

动态对象年龄判定

  为了更好的适应虚拟机的内存整理,虚拟机还有一个原则,如果在新生代的Suivivor空间的相同年龄对象所占的内存大于Suivivor一半时,将大于和等于这个年龄的对象加入到老年代。

空间分配担保

  在新生代发生垃圾回收时,虚拟机都会先检查这个老年态最大可用连续空间是否大于新生代所有对象总空间,如果成立,那么这次垃圾回收是安全的,可以直接进行垃圾回收。如果不成立,就会查看HandlePromotionFailure这个值是否设置开启的,如果开启,就会继续判断老年代最大可用连续空间是否大于历次垃圾回收晋升到老年代对象的平均大小,如果大于,则进行一次啊垃圾回收。如果小于或者这个参数设置为关闭状态,那么就要进行一次啊Full GC。
  为何如此?前面说了,新生代垃圾回收是复制算法,新生代内存分为8:1:1,每次垃圾回收就会将9份的移到剩余一份里面的,可能溢出,那么就用老年代担保,所以如此哦。一般上面的参数会默认设置为开启状态。

jvm垃圾回收算法

发表于 2018-05-08

引用

  强引用:这个引用在程序代码间很常见,类似于Object a=new Object(),这样直接声明的对象,只要强引用还存在,那么这个对象就不会被gc回收。
  软引用:java用SoftReference类来实现软引用。软引用关联的对象会在系统即将发生内存溢出,也就是内存不够时,将会把这些对象进行回收,如果回收后依旧没有足够的内存才会抛出内存溢出异常。
  弱引用:java用WeakReference类实现弱引用,其强度更弱于软引用,被弱引用的对象会在下次垃圾回收时直接被回收,无论内存是否足够。
  虚引用:java用PhantomReference类来实现虚引用。这个是最弱的一种引用,甚至无法通过虚引用来获取对象。

判断对象是否被引用

引用计数算法

  给对象加一个引用计数器,每当一个地方引用它时,计数器就加一,当引用失效时,计数器就减一,任何时刻计数器为0时的对象就是不可能再被使用的。但是有bug,2个对象相互引用,那么这个对象就永远无法被回收。

可达性分析算法

  这个算法是通过一个叫”GC Roots”的对象作为起始点,从这个节点开始向下搜索,搜索的路径叫做引用链,如果可以通过引用链找到这个对象,那么这个对象时有效的,否则,无效,解决了上面2个对象相互引用但是没有别的对象可达的问题,可以作为”GC Roots”的对象有方法区常量引用的对象,虚拟机栈引用的对象,等。目前java就是这种判断模式。

gc垃圾回收算法

标记-清除算法

  最基础的算法,就是把要回收的对象标记后进行回收,缺点是容易产生大量不连续的内存碎片,如果产生一个大的对象时,由于全是内存碎片,没有整个的内存,因此内存不够就会提前触发垃圾回收,效率很低。

复制算法

  目前主流虚拟机的收集算法之一主要针对新生态对象,初始化时是一比一的算法,就是把内存分为2部分一样大的部分,平时只使用其中一个部分也就是其一半,当发生垃圾回收时,就会把这个部分存活的对象放到另一个部分,这样子就避免了内存碎片,但是要只使用一半的内存,亏损太大划不来,目前主流虚拟机都是用这个算法来回收新生代。IBM研究发现,98%的新生态对象生存周期很短的,于是一个新的算法出来了,就是把内存分为3部分,8:1:1,每次使用其中的0.9部分,当发生垃圾回收的时候那就把剩余的对象就放到剩余的0.1那里,当然剩余的0.1可能不够那就可以把一部分对象转化为老年态暂时存到老年态里面,这样子就只浪费0.1的内存了,性价比可以。

标记-整理算法

  主要针对老年态对象,由于生存周期较长,不适合复制算法,该算法就是和标记-清除算法的加强版,就是多了一个移动的算法,就是把存活的对象都向一端移动,并且把端边缘的字段干掉

分代收集算法

  就是把对象划分为新生态和老年态,不同态使用不同不同的收集算法,新生代采用复制算法,老年态使用标记-整理算法或者标记-清除算法,这个也就是主流虚拟机所采用的。

垃圾回收器

  垃圾回收器就是垃圾回收算法的具体实现,我们看看都有哪些垃圾回收器。

serial收集器

  一个单线程的垃圾回收器,它在进行垃圾回收时,会让其他所有线程都暂停,直到他垃圾回收完毕,很显然,问题很大。采用的是复制算法。回收新生代对象。

parnew收集器

  是serial收集器的多线程版本,没有其他本质的区别

parallel sacvenge收集器

  这是个新生带收集器,采用的是复制算法。

serial old收集器

  采用标记-整理算法。单线程。主要配合serial收集器

parnew old收集器

  主要是配合parallel sacvenge收集器这个新生代垃圾回收器使用。

cms收集器

  也是老年态收集器,采用标记整理算法。

G1收集器

  上述收集器都要相互配合才可以使用,一个负责收集新生代对象,一个负责收集老年态对象。
  但是这个G1不一样,这是个面向服务端的垃圾回收器。
  以下优势:
  1.能充分利用多核CPU来减少暂停时间。
  2.分代收集,自身就可以实现分代收集。
  3.空间整合:整体是采用标记-整理算法实现的收集器,但是局部上也是基于复制算法实现的。
  4.可预测的停顿:让使用者在一定时间内消耗的垃圾回收时间不超过n毫秒。
  其他垃圾回收器回收要么是新生代,要么是老年代,但是G1不一样,虽然依旧保留了新生代和老年代这个概念,但是实际上将这个java堆分成了多个大小相等的独立区域Region,新生代和老年代不再是物理隔离了。他们都是一部分Region的集合(不需要连续)。
  G1使用了一个方法来进行可预测的停顿,G1会跟踪每一个Region堆的垃圾回收价值(回收可用空间和对应时间),并且维护一个优先列表,当需要进行垃圾回收时就按照这个优先列表来处理。

GC

  1.这个Minor GC是针对年轻代的回收,当虚拟机无法为一个新生对象分配空间时。就会发生这个GC操作。
  2.Major GC 是清理老年代。Full GC 是清理整个堆空间—包括年轻代和老年代
  3.每一次GC都会使得整个程序暂停几毫秒,这对用户程序几乎没有反应。

规则

  目前虚拟机,老年代:新生代=2.27:1。
  新生代,eden:from:to=6.4:1:1。
  对象优先在eden区分配,大对象直接进入老年代(经历了多个gc存活的对象,from,to,大于某个年纪的对象总和大于其一半时,将其也挪到老年代。),长期存活对象将进入老年代。
  当给对象分配内存时,如果没有足够的空间分配,虚拟机将发起一次垃圾回收。垃圾回收后,还是不够的话,会根据分配担保机制。就会把eden区的数据丢到老年代里面去。

  

java 泛型

发表于 2018-05-04

为何有泛型

  1.泛型是在编译期间进行校验的以保证类型安全。

  2.集合不会保证对象的类型,如果不用泛型。那么集合统一用父类object类型来指代,由于java语言的特性,在编译期间,父类和子类都可以用父类来指代,即使在运行期间都是自己的实际类型,集合这个类也是这样的,如果没用用泛型,他们用object这个统一的父类来指代,但是如果在编译期间你要使用子类特有的方法时的话还是要转换成子类类型,现在问题来了,集合类里面什么元素几乎都可以添加,毕竟都是object的子类,但是我们要使用的话,就会出现问题了,我们不知道到底是不是我们要使用的子类,万一集合里面添加了一个不是我们要用的子类,我们转换使用的时候就会报错:java.lang.ClassCastException。

最简单的泛型实例

  List list = new ArrayList();

  就像这个加了String这个限制,放到集合里面的元素必须是String类型,取出数据的时候也没必要转换为string类型了,因为编译器已经知道了,不需要转换了(额外说下这个多态,多态的原理也正是这个原理,父类可以在编译期间指代子类,但是实际运行还是本身的类型,要是想用子类特有的方法,父类是不行的,虽然确实可以使用,但是编译时不会通过的,会报错,说找不到这个方法。)

自定义泛型接口、泛型类和泛型方法

class Box<T> {
    private T data;
    public Box() {
    }
    public Box(T data) {
        this.data = data;
    }
    public T getData() {
      return data;
    }
}


public class DemoTest {
    public static void main(String[] args) {
        Box<String> name = new Box<String>("dh");
        Box<Integer> age = new Box<Integer>(123);
        System.out.println("name class:" + name.getClass());      // com.dh.Box
        System.out.println("age class:" + age.getClass());        // com.dh.Box
        System.out.println(name.getClass() == age.getClass());    // true
    }
}

  在这里我们用T来来接收来自外部的参数,泛型终究只是编译期的东西,不会带到运行期,主要就是校验所传信息是否正确罢了,当然在逻辑上我们可以理解这是一些不同类(毕竟一个是String类型,一个是Integer类型),但是实际上这些都是相同的类,就是我们在生成对象的时候,给这个对象传了一个类型参数过去,就是这个类型参数是这个类的一点限制,多了一个参数,一个校验参数罢了。

类型通配符

public class DemoTest {
    public static void main(String[] args) {
        Box<String> name = new Box<String>("dh");
        Box<Integer> age = new Box<Integer>(123);
        Box<Number> number = new Box<Number>(456);
        getData(name);
        getData(age);
        getData(number);
          get1rData(number); // 1
        getUpperNumberData(age);    // 2
        getUpperNumberData(number); // 3
    }
    public static void getData(Box<?> data) {
        System.out.println("data :" + data.getData());
    }
    public static void getUpperNumberData(Box<? extends Number> data){
        System.out.println("data :" + data.getData());
    }
    public static void get1rData(Box<? super Number> data) {
        System.out.println("data :" + data.getData());
    }
}

  结果如下:
data :dh
data :123
data :456
data :456
data :123
data :456
  我们用?来代替具体的参数类型,主要是为了匹配我们创建类BOX时用了T这个形参。?可以说是所有形参的父类。可以指代任何泛型,是实际参数,不是形参。就是固定好的。

  <? extends T>必须是T和T的子类

  <? Super T>必须是T或T的父类
  从上面的例子可以很容易的看出来。

java 重写规则

发表于 2018-05-04

重写规则之一:重写方法不能比被重写方法限制有更严格的访问级别。
(但是可以更广泛,比如父类方法是包访问权限,子类的重写方法是public访问权限。)
重写规则之二: 参数列表必须与被重写方法的相同。
重写有个孪生的弟弟叫重载,也就是后面要出场的。如果子类方法的参数与父类对应的方法不同,那么就是你认错人了,那是重载,不是重写。
重写规则之三:返回类型必须与被重写方法的返回类型相同。
父类方法A:void eat(){} 子类方法B:int eat(){} 两者虽然参数相同,可是返回类型不同,所以不是重写。
父类方法A:int eat(){} 子类方法B:long eat(){} 返回类型虽然兼容父类,但是不同就是不同,所以不是重写。
重写规则之四:重写方法不能抛出新的异常或者比被重写方法声明的检查异常更广的检查异常。但是可以抛出更少,更有限或者不抛出异常。也就是说你自己定义的异常类中,你可以抛出子类,但是不能抛出父类以及以上的类。
注意:这种限制只是针对检查异常,至于运行时异常RuntimeException及其子类不再这个限制之中。
重写规则之五: 不能重写被标识为final的方法。常量方法啊,很显然。
重写规则之六:如果一个方法不能被继承,则不能重写它。如private方法

1…8910…13

skydh

skydh

126 日志
© 2020 skydh
本站访客数:
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.3