美文网首页
JAVA基础

JAVA基础

作者: hom03 | 来源:发表于2021-01-27 20:22 被阅读0次

    强引用、软引用、弱引用、虚引用

    强引用:是使用最普遍的引用,置为null才会被垃圾回收

    软引用:只有在内存不足的时候JVM才会回收该对象

    弱引用:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

    虚引用:也称为幻影引用:一个对象是都有虚引用的存在都不会对生存时间都构成影响,也无法通过虚引用来获取对一个对象的真实引用。唯一的用处:能在对象被GC时收到系统通知,JAVA中用PhantomReference来实现虚引用。

    Java多态怎么实现?多态内部原理?

    在java里,多态是同一个行为具有不同表现形式或形态的能力,在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

    原理:动态绑定。先在this对象方法表中找该方法,如果没有重写父类该方法,按照继承关系从下往上搜索。

    Java的内存模型

    Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

    Java内存模型(不仅仅是JVM内存分区):调用栈和本地变量存放在线程栈上,对象存放在堆上。

    1.一个本地变量可能是原始类型,在这种情况下,它总是“呆在”线程栈上。

    2.一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。

    3.一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量仍然存放在线程栈上,即使这些方法所属的对象存放在堆上。

    4.一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。

    5.静态成员变量跟随着类定义一起也存放在堆上。存放在堆上的对象可以被所有持有对这个对象引用的线程访问。

    6.当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个成员变量的私有拷贝。

    多CPU:一个现代计算机通常由两个或者多个CPU。其中一些CPU还有多核。从这一点可以看出,在一个有两个或者多个CPU的现代计算机上同时运行多个线程是可能的。每个CPU在某一时刻运行一个线程是没有问题的。这意味着,如果你的Java程序是多线程的,在你的Java程序中每个CPU上一个线程可能同时(并发)执行。

    CPU寄存器:每个CPU都包含一系列的寄存器,它们是CPU内内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存。

    高速缓存cache:由于计算机的存储设备与处理器的运算速度之间有着几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。每个CPU可能有一个CPU缓存层,一些CPU还有多层缓存。在某一时刻,一个或者多个缓存行(cache lines)可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。

    内存:一个计算机还包含一个主存。所有的CPU都可以访问主存。主存通常比CPU中的缓存大得多。

    运作原理:通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。

    需要解决的问题

    缓存一致性问题:在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存)。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等:

    指令重排序问题:为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化。

    内存屏障:禁止特定类型的处理器重排序:重排序可能会导致多线程程序出现内存可见性问题。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。

    ClassLoader

    JVM中提供了三层的ClassLoader:

    Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。

    ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。

    AppClassLoader:主要负责加载应用程序的主函数类。

    类加载过程(双亲委派机制)

    类加载过程使用了双亲委派机制,判断是否已经加载过该class从子类classloader往上执行。加载从父类开始执行。

    类加载过程使用双亲委派机制的优势

    保证类的安全性和唯一性

    1.避免系统类被篡改:如果有人想替换系统级别的类:java.lang.String,自定义了相同包名相同类名的String,但是双亲委派机制会保证先从bootclassloader开始加载,所以该自定义类无法被加载成功。

    2.避免重复加载:如果一个类被子类加载器加载过了,则不会被父类重复加载。

    双亲委派机制

    一个编译后的class文件,想要在JVM中运行,就需要先加载到JVM中。java中将类的加载工具抽象为类加载器,而通过加载工具加载类文件的具体方式被称为双亲委派机制。

    JVM运行时数据区

    运行时常量池:属于方法区一部分,用于存放编译期生成的各种字面量和符号引用。编译器和运行期(String 的 intern() )都可以将常量放入池中。内存有限,无法申请时抛出 OutOfMemoryError。

    对象的内存布局

    在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

    对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’。第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。

    实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。

    对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。

    JVM垃圾回收

    垃圾回收主要回收 Java 堆和方法区这两部分内存区域。

    程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

    判定对象是否存活算法

    1.引用计数法:难以解决循环引用问题。

    2.GC ROOT可达性分析法。

    可作为 GC Roots 的对象:

    虚拟机栈(栈帧中的本地变量表)中引用的对象

    方法区中类静态属性引用的对象

    方法区中常量引用的对象

    本地方法栈中 JNI(即一般说的 Native 方法) 引用的对象

    回收方法区

    在堆中,尤其是在新生代中,一次垃圾回收一般可以回收 70% ~ 95% 的空间,而永久代的垃圾收集效率远低于此。

    永久代垃圾回收主要两部分内容:废弃的常量和无用的类。

    判断废弃常量:一般是判断没有该常量的引用。判断无用的类:要以下三个条件都满足:

    1.该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例。

    2.加载该类的 ClassLoader 已经被回收。

    3.该类对应的 java.lang.Class 对象没有任何地方呗引用,无法在任何地方通过反射访问该类的方法

    垃圾回收算法

    1.标记、清除算法

    两个不足:效率不高,空间会产生大量碎片。

    2.复制算法:把空间分成两块,每次只对其中一块进行 GC。当这块内存使用完时,就将还存活的对象复制到另一块上面。

    解决标记、清除算法的不足,但是会造成空间利用率低下。

    3.标记-整理算法:不同于针对新生代的复制算法,针对老年代的特点,创建该算法。主要是把存活对象移到内存的一端。

    4.分代回收:根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法。

    新生代:每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。

    老年代:老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记 —— 清除 或者 标记 —— 整理 算法回收。

    当前商业虚拟机的GC都是采用分代收集算法,这种算法并没有什么新的思想,而是根据对象存活周期的不同将堆分为:新生代和老年代,方法区称为永久代(在新的版本中已经将永久代废弃,引入了元空间的概念,永久代使用的是JVM内存而元空间直接使用物理内存)。

    新生代和老年代回收算法

    新生代中的对象“朝生夕死”,每次GC时都会有大量对象死去,少量存活,使用复制算法。新生代又分为Eden区和Survivor区(Survivor from、Survivor to),大小比例默认为8:1:1。

    老年代中的对象因为对象存活率高、没有额外空间进行分配担保,就使用标记-清除或标记-整理算法。

    新产生的对象优先进去Eden区,当Eden区满了之后再使用Survivor from,当Survivor from 也满了之后就进行Minor GC(新生代GC),将Eden和Survivor from中存活的对象copy进入Survivor to,然后清空Eden和Survivor from,这个时候原来的Survivor from成了新的Survivor to,原来的Survivor to成了新的Survivor from。复制的时候,如果Survivor to 无法容纳全部存活的对象,则根据老年代的分配担保(类似于银行的贷款担保)将对象copy进去老年代,如果老年代也无法容纳,则进行Full GC(老年代GC)。

    大对象直接进入老年代:JVM中有个参数配置-XX:PretenureSizeThreshold,令大于这个设置值的对象直接进入老年代,目的是为了避免在Eden和Survivor区之间发生大量的内存复制。

    长期存活的对象进入老年代:JVM给每个对象定义一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳,将被移入Survivor并且年龄设定为1。每熬过一次Minor GC,年龄就加1,当他的年龄到一定程度(默认为15岁,可以通过XX:MaxTenuringThreshold来设定),就会移入老年代。但是JVM并不是永远要求年龄必须达到最大年龄才会晋升老年代,如果Survivor 空间中相同年龄(如年龄为x)所有对象大小的总和大于Survivor的一半,年龄大于等于x的所有对象直接进入老年代,无需等到最大年龄要求。

    垃圾收集器

    垃圾收集算法是方法论,垃圾收集器是具体实现。JVM规范对于垃圾收集器的应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。JDK7/8后,HotSpot虚拟机所有收集器及组合(连线)如下:

    引用计数法不能解决循环用的问题,但我想用引用计数法怎么办(智能指针)

    智能指针,靠内部维护引用管理来判断,弱引用等方式。

    若 A 强引用了 B,那 B 引用 A 时就需使用弱引用,当判断是否为无用对象时仅考虑强引用计数是否为 0,不关心弱引用计数的数量。

    动态代理与静态代理

    静态代理:手写代理类。

    动态代理:运行期自动创建代理类,底层靠反射调用代理方法。


    面向对象六大基本原则

    一、单一职责原则

    定义:一个类只负责一项职责。

    问题由来:一个类A负责两个不同的职责,职责A1,职责A2,当职责A1发生改变时候,可能会导致A2功能发生故障。

    解决方案:遵循单一职责原则,分别建立两个类A1(职责1),A2(职责2),这样修改职责1的时候就不会影响到职责2了。

    遵循单一职责原则的优点:

    1.可以降低类的复杂度

    2.提高类的可读性,提高系统的可维护性

    3.降低变更引起的风险

    二、里氏替换原则

    定义:只要父类能出现的地方子类就可以出现。

    面向对象的语言有三大特点,继承、封装、多态,里氏替换原则就是依赖于继承和多态这两大特性。简单说,就是所有引用基类的地方必须透明的使用其子类对象。

    三、依赖倒置原则

    定义:高层模块不应该依赖底层模块,二者都应该依赖其抽象,抽象不应该依赖细节,细节应该依赖抽象

    问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

    解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。

    依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。

    四、接口隔离原则

    定义:客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小接口上

    问题由来:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。

    解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。

    五、迪米特法则

    定义:一个对象应该对其他对象保持最少的了解。

    问题由来:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

    解决方案:尽量降低类与类之间的耦合。

    六、开闭原则

    定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

    面向对象和面向过程

    面向过程

    1.注重步骤与过程,不注重职责分工

    2.如果需求复杂,代码会变得更复杂

    3.开发复杂项目,没有固定的套路,开发难度很大!

    面向对象

    1.注重对象和职责,不同的对象承担不同的职责

    2.更加适合应对复杂的需求变化,是专门应当复杂项目开发,提供的固定套路。

    3.需要在面向过程基础上,再学习一些面向对象的语法

    内存抖动

    在大循环里或者onDraw等高频刷新的地方创建对象分配内存容易进行频繁的垃圾回收和内存再分配的恶性循环,这种现象叫内存抖动

    String

    String特点

    不变性,可缓存

    不变意味着可以缓存,需要的时候可以随时拿出来使用。Java中的字符串是有缓存机制的,下面的代码输出的结果是true,这意味着编译器有能力优化字符串的内存占用,让所有内容相同的字符串只需要创建一份:

    Stringstr1="octopus";Stringstr2="octopus";

    System.out.println(str1==str2);

    这样,性能也有提高,因为在编译期很多字符串的内容是可以确定的,此时编译器会自动优化减少内存占用和计算资源。

    使用连接符的时候,实际上是经过了StringBuilder的优化处理的。并不是在原来的String对象中做追加

    StringBuffer为啥比String快

    String有不变性特点,任何对字符串的操作都会产生新的字符串,内存空间分配,内容复制这些都是非常占资源的。而StringBuffer和StringBuilder是字符串操作类,不会产生新的字符串。

    相关文章

      网友评论

          本文标题:JAVA基础

          本文链接:https://www.haomeiwen.com/subject/gnllzktx.html