java面试的知识点和我的回答

下面的内容都是传我自己的理解, 其中肯定有很多的错误之处, 欢迎指正。

java基础知识

Java 的基本语法都要了解,并发编程、NIO、JVM 等,你多少要有点儿了解,Spring、Netty 这些框架也要了解。
String类为何要设计成final不可变的?
答:String类是java中最常用的类, 并且String可以表示任何的数据。 所以java对于String类做了还多的优化,其中之一就是不可变, String 的 intern() 的方法会把字符串添加到常量池中。 new的方式会新生成常量池, 一艘字符串不建议使用new的方式, 建议直接使用引号。 new String 的方式会会生成一个常量, 同时堆中会生成字符串对象并且持有这个常量的引用

不可变的优点:

设计成不可变可以简单的防止并发问题, 因为不可变就可以简单的避免线程的问题。 每个线程只能可读,自然就可以避免并发问题
设计成不可变的, 就可以进行字符串的缓存共享(字符串池),共享可以大量节约缓存。
不可变的设计可以放心的作为Map等key值

缺点:

由于现实中总是有字符串改变的需求, 并且我们平常使用中也总是会用到字符串拼接等操作,
由于不可变性, 需要实现字符串的拼接就需要生成新的字符串常量, 导致字符串常量池很容易膨胀
每次字符串的拼接都需要生成新的字符串常量, 生成对象很耗性能问题, 所以java引入了StringBuffer和StringBuilder
字符串的内部维护了char[] 数组,同样的字符在不同的编码格式中是不一样的,如果进行格式转换,需要把字符转成byte数组在转换成目标字符 。 ps: java11 中String的内部已经改成了byte[]数组, 如果进行编码转换就可以少一步操作

java中==和equals和hashCode的区别

== 比较的就是对象在内存中存储位置,如果是统一对象,自然内存地址信息是一样的。
equals 在Object中是native实现, 本质还是比较内存地址。 因为java的每个类都是继承自Object类,所以默认也是比较内存地址
hashCode也是native方法, 是根据内存中的内容来计算的hash值,所以不同的对象的内存中值肯定是不同的,所以hash值大概率不会相同。

总结:如果需要确定两个引用指向的是同一个内存中的区域, 使用== 就可以, 使用默认的equals也行。 如果不用需要判断两个对象中的业务上的相等就需要重写equals和hashCode方法。 并且Map和Set确定重复的依据也是根据equal和hashCode两个方法, 很多的常用类都重写了这两个方法, 比如String 和基本类型的包装方法

int、char、short long各占多少字节数

int 4字节, short2字节, long8字节, char的字节数和编码有关, gbk都是两个字节, utf8字节数在1到3直接, 一般英文1个字节, 中文2到3个字节

int与integer的区别

int 是基本变量, 直接分配在栈上, integer是其包装类, 内部成员变量是int类型,内存分配在堆上。

对java多态的理解

java的多态体现在 接口, 继承, 虚拟类方面。
java 中子类和可以覆盖父类的方法
java中支持同样类名的重载,jdk会根据调用传入的参数动态选用正常的方法
可以使用父类或者接口来引用子类的实现, 可以传入不同的实现子类
但是由于java还是不支持多继承, 并且严格的类型限制, 导致java的多态性还是和脚本语言还是有很大差距, 所以jdk推出了基于接口的动态代理, 并且诞生了很多的字节码框架ASM和Javssent等

String、StringBuffer、StringBuilder区别

String不可变, StringBuffer、StringBuilder是可变的,
String 和StringBuffer 是线程安全的, StringBuilder不是

什么是内部类?内部类的作用

java中将一个类定义在另外一个类的内部, 我们称内部类。 其实内部类在编译成的class还是两个文件。 常用内部类分四种: 成员内部类、局部内部类、匿名内部类和静态内部类

成员内部类:

可以很方便的访问外部类的成员包括静态成员, 不受private的限制。
如果存在和外部类同名变量, 默认访问的是内部类的变量, 访问外部类需要使用 外部类名.this.变量名的方式
外部类访问内部类,必须通过对象的方式。 外部类可以访问内部类的private成员
内部类不能单独存在, 必须依附于外部类的对象

局部内部类: 在外部类的方法中定义的类

这个类只在这个方法作用域内有用, 超出方法范围无妨访问,
在方法作用域内的访问特性和成员内部类一样

匿名内部类:

使用很多, 特别是在一些监听的处理类的设计上。
匿名内部类没有构造方法
匿名内部类不能扩展方法, 只能覆盖接口中的方法

静态内部类:

是最接近正常类的内部类, 静态内部类不用依赖外部类的对象, 但是依赖外部类的类。 由于他是属于类的内部类, 所以不能访问外部类对象的成员
内部类为何能够访问外部类的成员。
其实内部类编译后都有独立的class文件, 可以反编译看到实际上内部类都在构造参数中传入了外部类的引用, 内部类就是通过这个应用访问外部类的。

问题: 为何局部和匿名内部类只能访问外部的final变量?

成员内部类传入的是外部对象的引用, 成员内部类又不能脱离外部类存在,所以对于内部类来说就是外部类就是不可变, 所以无论外部类怎么变,被传入的引用都是有效的指向外部对象的,
静态内部类传入的是class的引用, class类是不可变的, 也没有问题
但是局部内部类,方法内部持有的变量也是引用, 如果传入进去后,变量有改变了(引用变量),就会导致内外部是不同的变量,无法实现同步, 传入的引用就无意义, 不如规定只能传入不可变的引用,这样外部和内部就能保证持有的是同一个对象
匿名内部类同理

优点

可以很方便的把关联的逻辑的类封装在一起,从而实现对外部不可见
对于时间驱动的逻辑编写很方便
可以很方便的编写多线程代码, 可以一定的程度的避免并发问题
内部类可以继承接口, 并且可以和外部类互相访问, 所以可以和外部类实现同样的接口, 相当于变相的实现java的多继承机制

抽象类和接口区别,抽象类的意义,抽象类与接口的应用场景,抽象类是否可以没有方法和属性,接口的意义?
  • 抽象类:

抽象类不能被实例化, 只能被继承
抽象类中可以没有抽象方法, 但是包含抽象方法必须的必须是抽象类

  • 接口:

在1.8之前接口中只能包含未实现的方法, 1.8之后可以有默认方法
接口可以实现接口

两者异同:

  • 都不能实例化,只能继承

  • 抽象类可以有正常的方法和成员变量, 但是接口中只能包含未实现方法和默认方法,不能包含成员变量和正常方法。 接口中成员变量都是默认的static的。

  • 接口的方法都是默认的public的, 不能定义private和protected

抽象类和接口意义:

接口是更像一种约束,约束实现类的实现方法的签名和参数。 所以子类可以实现多个接口,
子类只能实现一个虚拟类, 虚拟类更像一个一个半成品的类。
对于不同的子类的相同的实现逻辑, 不用每个子类都实现一遍, 可以提取到虚拟父类实现, 对于子类的不同点可以电仪抽象方法来满足。
抽象类和接口都可以没有成员变量和方法:
抽象类和接口的引用场景:
接口就是一种约定, 比如dubbo的消费者和服务者是使用的同样的接口, 其实就是约定好两方使用相同的方法签名,这样就可以正确的通信
对于设计模式中的模板方法模式,使用抽象方法就方便实现
对于不同子类的共同逻辑可以放到抽象父类

泛型中extends和super的区别?

List<? super Fruit> 规定只能放入Fruit类和其父类
List<? extends Fruit> 规定只能放入Fruit类和其子类类

父类的静态方法能否被子类重写? 静态属性和静态方法是否可以被继承?是否可以被重写?以及原因?
不能, 静态方法是属于类的, 父类和子类始终是不同的两个类, 不能覆盖

进程和线程的区别?

进程是操作系统级别的, 每个进程有独立的内存区间
线程是进程下级的, 线程是共享进程的内存空间的, 所以多线程需要考虑并发问题

final,finally,finalize的区别

finalize是在对象回收的时候会触发调用的

序列化的方式,Serializable 和Parcelable 的区别?

java自带了序列化的方式, 要序列化的类必须实现Serializable接口, java的序列化因为需要序列化类的信息和关联类的信息,效率并不高, 所以常用的都是第三方的序列化方式, 比如通用的json 和高效率的Protocol

静态内部类的设计意图

每个内部类的都有的意义, 就是可以把关联的逻辑封装在一起, 不用在单独的写类

谈谈对kotlin的理解

kotlin 是jvm上的另一种语言, 他的实现本质还编译成class 文件, 所以可以和java互通。 由于java的严谨的特点和必须兼容以前的版本的问题,java在更新方面总是显得很谨慎。
kotlin没有历史包裹, 在设计上实现前卫大胆, 有很多的实用特性

String 转换成 integer的方式及原理

String能够转成数字的前提, 是String中必须都是数字类型的字符, 转换成int就是逐个字符解析

哪些情况下的对象会被垃圾回收机制处理掉?

没有引用的对象会被回收
没有实例的类和加载器也被回收的了, 这个类会被回收
如果常量池中常量不存在引用了,在内存不够的时候会被回收

静态代理和动态代理的区别,什么场景使用?

静态代理:
就是定义一个代理类,实现和被代理类一样的方法(接口), 代理类中持有被代理类引用,但是代理类中的逻辑都是调用被代理类来实现的。 门面模式中就是使用静态代理来很方便的实现
缺点: 每个代理类只能为一个代理类服务, 无法代理接口的所有实现类

动态代理:
jdk通过反射的方式可以代理接口的所有实现了,
优点: 可以代理一系列的接口实现类, 不会再限制在一个类上。

场景:
如果我需要修改某个接口的所有实现类的子类方法的逻辑,如果每个子类都改, 会很麻烦, 可以通过动态代理的方式生成对象, 而不用改源码
对于第三方的jar类, 我们无权限修改, 就可以使用代理方式来改变其逻辑
由于jdk的动态代理还是有借口的限制, 有一些场景还是不灵活。 所以诞生了修改字节码来实现代理的框架,有Cglib 和ASM等

Java的异常体系

Java把异常作为一种类,当做对象来处理。所有异常类的基类是Throwable类,两大子类分别是Error和Exception
Error 是程序运行抛出的不可能通过程序自己解决的异常, 比如内存溢出等
Exception 表示的是程序运行中的异常, 可以程序自己来捕捉处理。 正确的捕捉处理异常,可以有效的提高程序的健壮性
Exception 可以分为运行时异常和编译器异常
运行时异常RuntimeException: 在程序运行中才会出现的, 程序运行之前并不清楚。
编译器异常: 比如ClassNotFundException , 在编译器就会报出来的异常, 如果不处理会编译不通过, 所以能跑起来的程序都没有编译器异常。

关于异常可以参考 :

Java常用集合类
ConcurrentHashMap的实现原理?

HashMap在多线程的情况下会有线程安全的问题。 所以java就提供了HashTable和ConcurrentHashMap两个Map实现类来保证线程安全。 HashTable只是简单的给每个方法都加锁来保证线程安全, 但是这种简单的加锁方式有性能问题,所以还是不推荐使用。 ConcurrentHashMap对于HashTable的性能问题进行了优化。 使用分段加锁的机制, 它将内部的数据分成数段,每段都是用独立的锁, 这样如果多线程分别读取map中不同段的数据,他们获取的是不同段的锁,这样就不存在锁的竞争问题,只有不同线程同时获取同一段的数据才会有竞争问题。

分段锁 是一种分而治之的思想, Map中虽然保存有很多的数据,但是每个线程大概率都只是访问其中一部分的数据,如果加上全局锁,锁上所有的数据,明显会有性能问题。

HashMap ?

内部维护了一个数组。 这个数组存放的是Entity对象
每次存入key-value的时候, 实际是构建一个Entity对象, 然后根据key的Hash值放入到数组的对应位置
如果存入k-v的构建的Entity的时候,如果发现数组的对应位置已经有值了, 就会构建一个链表,这个链表的头部就是数组中这个位置的Entity对象, 后加入的对象会自动加在链表的尾部
链表长度到达了6 后, 这个链表就会变成红黑树, 主要因为链表的遍历性能低下,链表长度长了后用性能更好的红黑树
根据K获取值的时候,根据K的hashcode确定数组位置, 再遍历链表找到key一样的Entity。
HashMap可以允许key值为null, 但是只允许1个。hastTable不运行null的key
当容量到达0.75的时候, 会进行扩容, 扩容就是内部数组替换成一个更大的。
扩容比例是翻倍扩容

LinkHashMap

继承自HashMap,使用方式几乎和HashMap一样。 放入的数据是有序的, 可以根据放入顺序遍历
实现原理,是内部比HashMap多维护了一套链表。 链表就记录了放入的顺序,所以可以有序遍历

HashSet

内部实现几乎和HashMap一样, 只是它屏蔽了Map中的value值, 每次存入的Entity对象的value值都是固定的。
这个集合类经常作为去重工具处理,如果大量的数据去重,还是建议使用布隆过滤器
因为是根据HashCode来确定数组的位置的,所以内部的数据是没有特定顺序的

TreeSet

内部维护了二叉树的结构, 保证存入的数据是有序的。
根据顺序就可以很方便的判断重复性了
存入的对象必须实现Comparable接口, 内部排序就是根据这个接口来判断的
总结: 由于二叉树的特性, 在修改的时候效率会低,在查询的时候会高效, 并且数据还是有序的。

ArrayList , Vector

内部维护了一个数组, 每次存入对象都是按顺序存入数组中
当容量不够用的时候, 就会生成一个更大的数组替换旧数组,同时会复制内部的值到新数组中。
扩容的比例是每次都新增已有容量的一半, 比如当前容量10, 扩容后变成15.
因为是数组的方式, 如果删除中间部分的数据, 数据中的删除位置后面的数据都需要前移, 性能问题。
同样因为数组的方式, 遍历十分高效
Vector 内部实现几乎和ArrayList一样, 只是在每个方法中都加上了synchronized来保证同步

LinkList

内部维护的是链表的结构, 每次加入对象都是加入这个链表中
链表的形式,不存在扩容的问题, 容量不够直接在链表中增加就可以
删除和中间插入都十分方便, 只要改变下插入位置的链的指向就可以。
有与链表的特性, 插入删除 十分高效。 但是遍历就有性能问题了。

LinkList和ArrayList都是十分常用的List集合。他们的特点刚好互补, 所以日常使用中需要根据场景来选择合适的集合类型

BlockingQueue ?

线程安全的队列, 可以很方便的用户生产者—消费者模型, LinkedBlockingQueue是最常用的一种实现类。 内部是使用了两个锁和两个Condition来保证线程安全的。
/** Lock held by take, poll, etc / //获取对象必须获取这个锁 private final ReentrantLock takeLock = new ReentrantLock(); /* Wait queue for waiting takes / //如果容量中有值了, 会通知获取线程 private final Condition notEmpty = takeLock.newCondition(); /* Lock held by put, offer, etc / private final ReentrantLock putLock = new ReentrantLock(); /* Wait queue for waiting puts */ //容量中可以放值了,通知插入线程 private final Condition notFull = putLock.newCondition();
获取的时候,如果没有值可以阻塞住,等待其他线程放入值后, 阻塞结束自动返回值
放入的时候,如果容量已满也会阻塞住,等待其他线程消费了容量后, 阻塞结束对象放入

JVM的类加载机制

Class的载入7大阶段
  • 加载: 从文件或者其他地方将一个类的字节信息加载到内存中,
  • 验证:验证字节信息是否符合虚拟机的规范,防止格式不对的字节码信息载入到虚拟机中
  • 准备:开始在内存中准备一些静态变量区域,并且给这些变量赋初始值。 比如Int的初始值为0
  • 解析: 替换符号引用为直接引用。比如: 类中是通过类名来关联其他类的, 但是类名无法表示其他类的在内存中的位置, 这一步就是把类名替换成真正的类在内存中的应用。
  • 初始化: 在前面已经为类在内存中开辟空间了, 这一步就是把类中定义的静态值赋值进去。 这一步结束后,程序就可以正常使用类了。

类构造器: 是虚拟机根据类的内部定义自动生成的, 在初始化阶段会执行这个构造器。 在构造器中会给一些静态变量赋初值。 虚拟机会保证父构造器先于子构造器执行, 如果类中没有静态变量或者代码块。 构造器可以不用执行。

类的初始化时懒初始化的, 只有在必须初始化的时候才会初始化:
通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

定义对象数组,不会触发该类的初始化。

常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
通过类名获取 Class 对象,不会触发类的初始化。
通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。

通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。

类加载器

jdk自带三个类加载器
启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。
扩展类加载器(Extension ClassLoader): 负责加载JAVA_HOME\lib\ext目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。
应用中我们自定义的类对通过Application ClassLoader 来加载的。 java的类的加载有双亲委派的机制

双亲委派机制

当一个加载器开始加载一个类之前,会调用上级的加载器委托上级加载器来加载, 上级加载器也是同样的操作,一直到启动类加载器它没有上级了,此时启动类加载器就会判断要加载的类是否是自己可以加载的类,如果不是会将加载任务在返回下级下载器, 下级加载器也会判断下自己可以加载不, 如果可以就会加载类, 加载成功后在通知下级下载器不要忙了,类已经加载了。

双亲模型的优点:

无论什么加载器,都无法加载虚拟机的类。 这样对虚拟机是一种保护,防止应用程序破坏虚拟机的正常运行。
防止一个类被不同的加载器重复加载

双亲模式的缺点:

Jdk中的类无法加载用户的类, 比如JDBC的DriverManager类是接口在rt.jar中, 是属于启动类加载器加载的, 但是他的java.sql.DriverManager#getConnection(String, Properties,Class<?>)方法需要加载的了是各个数据库厂商实现的jdbc的驱动类。 数据库厂商实现的类肯定是需要应用程序类加载器或子加载器来加载的。

疑问: 调用A类的方法,这个方法内部又有加载B类, 那么B类的加载会使用A类一样的加载器。

DriverManager的getConnection方法会加载用户传入的驱动类, 如果按照双亲模型,应该使用启动类加载器来加载,但是启动类加载器只能加载特定目录(jdk)中的类。

解决: 线程上下文类加载器。
一个程序的启动类肯定是用户类, 他的加载肯定是应用类加载器, jdk会在启动的时候把这个加载器放入主线程上下文中。 主线程在创建其他线程的时候,会把这个加载器传递下去。 所以DriverManager中的源码可以看到是取得了取得了这个加载器来加载驱动类的。 这样就实现了jdk中的类加载了用户的类。

Tomcat 的类加载器

tomcat基于自己的特性, 自定义了自己的类加载器。
问: 一个tomcat可以允许多个webApp应用,他们都是在一个jvm上运行的, 那么这个多个webApp是否可以互相调用呢?
答: 不行, 每个webApp都有自己的唯一的类加载器并且只加载本应用的目录中的类, 多个应用的类加载器是平行的, 如果A应用加载B应用的类,会委托到common类加载器,最后到bootstrap加载器,然后在原路返回。 整个过程都不会找到B类的class文件, 此时就会报ClassNotFundException .

为什么Catania.lib目录中的类优先级最高, 并且里面的类会覆盖所有应用的同名类?
因为common加载器是所有webApp加载器的上级加载器, 如果webapp要加载其中的同名类,就会委派到common加载器来加载,它会加载Catania.lib目录下的同名类,自然就屏蔽了各个应用的类了, 所以优先级很高。

JVM的内存区域

jvm的内存分为线程共享的和线程私有的, 线程私有的区域: 方法栈,本地栈, 程序计数器。
线程共享的: 堆, 方法区。
Java8中取消了方法区,将常量池放入堆中,方法区的其他信息(主要一些类的信息)放入堆外空间中了

程序计数器

记录字节码中指令运行的行号,程序根据这个行号确定下一步的指令。

虚拟机栈

栈是先进先出的单向队列的形式,方法执行的时候会创建一个栈帧, 并将栈帧放入栈中。 方法中的局部变量都会在栈中开辟空间(方法中的变量其实都是应用)。随着方法的结束,栈帧会移除栈并销毁。 如果栈中持有堆中对象的引用,这个对象不会被回收。

本地方法栈

是jdk中一些native方法运行的栈。

程序中创建的对象和数组都会存放在这里, 垃圾回收机制的主要作用区域, 为了方便垃圾回收对堆进行了分代处理和回收

方法区/永久代

存放每个类的信息, 存放静态常量,存放字符串常量等。 这里也会被垃圾回收机制回收,但是回收的频率较小。

堆的分代

Hotspot将堆分为老年代和新生代, 新生代和老年代的大小比例默认在1:2。
Xms:堆初始初始大小 Xmx:堆的最大值 Xmn: 新生代大小

新生代:

使用复制清除的方式回收对象, 新生代的回收称为Minor GC, GC时候会暂停程序
由于新生代的存活对象少,回收效率高, 实际暂停事件很少。
如果新生代中对象持有老年代中应用, 对象不会被回收
默认分成1:8:1的三个区域, 其中两个Eden区, 一个Servivor
始终有一个Eden是空闲的。 当Servivor内存快满的时候,会将Servivor和Eden1中的存活对象复制到空闲Eden2中, 新的对象继续在Servivor中创建
单空闲Eden放不下存活的对象的时候, 会将对象转入到老年代中。
java中的大部分对象都是存活的周期很短的, 所以分成1:8:1的比例完全没有问题。 同样由于大部分对象的存活时间不长, 使用复制清除的方法回收十分高效。
一个对象默认存活过15次回收,会转入老年代。 大对象会直接存入老年代

老年代:

存放生命周期长的对象和大对象。
当快要满的时候, 会触发MajorGC。 老年代使用标记整理的算法(不是标记清除)
老年代的回收比较耗时, 应该尽量避免。
标记整理的算法会清理内存碎片,这样保证了老年代的内存的连续
老年代是新生代的兜底内存区域,所以一般出现OOM的都是老年代

永久代:

随着类的加载而扩大
不会进行垃圾回收, 所以易出现OOM
java8已经把方法区移除, 方法区中的字符串常量和静态常量放入堆中,其他部分放入堆外内存中

垃圾回收

Java之所以能够长盛下去, 主要有三方面原因:
完全的面向对象的语言
跨平台的特性
垃圾回收机制
java为了垃圾回收放弃了灵活强大而又难用的指针。 不断的优化垃圾回收算法,现在java已经摆脱运行慢的帽子了, 同时java的内存使用效率程序的健壮性也随着垃圾回收算法的完善二提高。

要进行垃圾回收就要解决两个问题,

  • 什么样的对象算垃圾,
  • 垃圾如何高效回收?
什么样的对象算垃圾?

引用计数的算法
垃圾回收中经典经典的算法, 但是由于无法处理循环引用的问题,jdk没有采用。
可达性算法:
程序中有一些对象我们已经确定必定不会是垃圾, 比如栈中的数据和持有的引用, 元空间中类持有的引用, 常量池中持有的引用。这些引用指向的对象肯定不能是垃圾。所以栈中对象,常量池中对象可以作为GC roots对象, 沿着GC roots对象持有的引用往下遍历, 能够遍历到的对象就是可达对象, 不可达对象就是垃圾对象,GC就会回收这个垃圾。
可达性分析解决了循环引用问题。
不用维护计数器,减少了开销。
Jdk正是采用的这个算法

垃圾如何高效回收?

复制整理算法:
将内存分成两部分,每次只使用其中一部分。
回收只需要把使用的区域A中的存活对象copy到空闲区域B中, 在全部清空A区域
复制算法回收的空间是连续的, 没有碎片,并且回收简单高效
缺点是浪费空间
新生代正是使用这个算法,结合大部分对象活不久的特点,新生代把空闲区的大小比例设计成1/10 有效的减少了空间的浪费。

标记清除算法:
就是简单的把垃圾对象占用的空间回收掉,
但是垃圾对象可能在内存中各个部分零星分布的,如果直接回收会产生很多的内存碎片空间
标记整理算法: 这个是标记清除的优化版本
前期也是和标记清除算法一样回收垃圾,产生碎片
产生碎片后, 这个算法再把对象进行整理一下, 比如移动下存活的对象的位置, 使的内存连续,消灭碎片
标记整理的算法由于需要一个个碎片的整理,效率不高,但是老年代中回收的频率也不大, 使用这个算法没有问题
老年代使用的是这个算法, 由于老年代的对象存活周期长,每次也回收不了几个对象, 如果采用复制算法, 从A复制到B, 可能需要复制90%的存活对象, 效率低下。 复制对象又会浪费空间,老年代占用的空间本来就大,这样浪费的空间也会大。

常见的垃圾回收器

新生代垃圾收集器

Serial垃圾收集器: 单线程的使用复制算法的新生代垃圾, 收集的时候会暂停程序
ParNew: Serial的多线程版本
Parallel Scavenge收集器: 多线程收集器, 关注吞吐量。会根据设置的吞吐量来自动调整收集效率

老年代收集器

Serial Old收集器(单线程标记整理算法 )老年代, 可作为CMS的后备垃圾收集器, 能配合所有的新生代垃圾收集器
Parallel Old收集器(多线程标记整理算法): 只能配合新生代Parallel Scavenge使用。

CMS也是老年代的垃圾收集器,使用并发标记整理算法, 他分四步执行。

只有在初始标记的时候需要暂停程序

G1垃圾收集器

已经在java11中应用了。 G1收集器弱化了老年代和新生代概念。
将内存分成小的区块, 会标记每个区块的垃圾对象占比,并维护一个每个区块的垃圾率等信息的表
根据用户配置的回收比率参数, 来确定收集多少的区块的垃圾才符合配置
如果一个区块垃圾率很高,会直接采用复制清除算法, 复制这个区块存活对象到新的区块, 并回收它
如果区块垃圾少, 就会使用标记整理算法。
会合并使用率少的区块

java的四种引用类型
强引用

在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引
用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即
使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之
一。

软引用

软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它
不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中

弱引用

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象
来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

虚引用

虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚
引用的主要作用是跟踪对象被垃圾回收的状态。

Java的BIO, NIO 和AIO

阻塞IO

比如从一个输入流中读取数据, 用户进程调用read方法会发送命令到系统内核进程中,并且用户进程会阻塞等待, 系统内核进程会去查看流中是否有可读取数据,有的话返回数据到用户进程,用户进程阻塞结束,如果流中没有可读取数据,系统进程和用户进程都会阻塞直到可以读取数据为止。
从io的阻塞流程可以看到,程序是十分低效的,需要用户线程需要长期堵塞的。

非阻塞IO /NIO
 用户进程调用系统内核进程读取数据的时候, 如果流中没有数据,内核进程会直接返回信息告知用户进程没有数据,用户进程也会立即返回不阻塞。

从非阻塞的问题可以看到,他会立即返回流的状态, 如果我们要持续读取数据,就需要使用循环读取了。 我们可以使用一个线程循环读取每一个流的状态, 如果某个流可以读取了, 就把这个流交给新的线程去读取。 这个方式就是epoll的工作原理。 主流的linux和win操作系统都实现了这个机制,我们不用自己去实现这个循环读取流的操作,只要调用java的api就可以, 这个API正是java中的Selecter类。

多路复用技术:

java的NIO实际上就是对操作系统的epoll机制的调用调用实现的。

NIO: 用户进程开一个线程去监听各个流的状态, 当某个流可读取了,系统会通知用户进程, 用户进程再调用系统的方法从系统内核中复制流数据到用户进程中,这样就实现了数据的读取
AIO: 用户进程通知系统要读取某个流的数据, 之后用户进程就不管了去做其它的事情, 系统内核发觉某个流可以读取了,会自己把流的数据复制到用户进程中,并触发用户进程的回调通知用户进程。

可以看到从BIO到AIO 实际上是把更多的操作放到系统进程中, 由于系统是直接与硬件打交道的程序, 由它来监控流状态,读取流数据是十分高效的。 因此IO的效率也是随着提升的。

为何IO有这种发展趋势呢?

由于刚开始硬件性能低, 操作系统不能完成太复杂的操作, 操作系统就把更多的硬件操作权限交给上级应用程序来控制, 这样上级应用程序可以最大化的开发硬件的性能。 随着硬件的发展和业务复杂性的增加, 用户更希望能使用更简单的api来应对复杂的业务, 同时也希望操作系统做更多的事情,于是就发展成这样了。

为何NIO比BIO高效?

高效是相对的, 如果在一个性能低下的硬件中, BIO比NIO高效的多, 但是在性能更好的硬件中NIO万宝BIO. 实际上是硬件的发展带动了不同的IO实现方式。 相信随着科技的发展,在未来AIO也会成为一种低效的IO

java的流的设计大量使用了装饰器模式, 可以很方便的把自己流转换成字符流等。
NIO中的Channel 和各种Buffer的设计也是未来高效的使用NIO.
AIO使用不高, 著名的netty框架放弃了AIO的实现, 因为据说是性能没有比NIO更好, AIO确实是比NIO更先进的一种设计理念,但是由于Linux并没有实现好,导致netty没有采用。

java的序列化机制?

可以参考我的博客

java在序列化的前会调用对象中的writeReplace方法, 并把这个方法的返回值作为真正的序列化类对象。
反序列化的时候, 在之前会调用readResolve方法, 并把这个方法的返回值作为真正的反序列化对象。
所以重写这两个方法就可以控制类的序列化和反序列化的过程
单例模式大家都知道加锁或者私有的构造方法等可以保证唯一,但是如果使用序列化的方式就可以破坏这个唯一性, 如果要真正的保证唯一可以在单例中增加这两个方法来确保唯一。
其实序列化和反序列化 还有很多的方法可以控制这个过程 , 这里就不详细说了
lambda表达式可以通过序列化和反序列化的方式获取到传入的表达式的方法名。 具体可以参考我上面的博客。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值