一.基础知识
- JVM vs JDK vs JRE
- JDK:JRE+编译器(javac),工具,它能够创建和编译程序。
- JRE:JVM+核心类库,如果想要运行一个已经开发好的Java程序,使用JRE即可,但是它不能用于创建新程序。
- JVM:Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
- Java程序从源码到运行一般有以下三步:
- .java(源文件)
- .class文件 经过JDK中的javac编译后得到(JVM可以理解的Java字节码,它不面向任何特定的处理器,只面向虚拟机)
- 机器可以执行的二进制机器码 经过JVM后得到
- Java 中的几种基本数据类型是什么?对应的包装类型是什么?各自占用多少字节呢?
4. 自动装箱与拆箱
- 装箱:将基本类型用它们对应的引用类型包装起来,Integer i = 10 等价于 Integer i = Integer.valueOf(10)
- 拆箱:将包装类型转换为基本数据类型, int n = i 等价于 int n = i.intValue()
- String 、 StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?
- String 类中使用 final 关键字修饰字符数组来保存字符串,所以对象不可变,可以理解为常量,故线程安全
- StringBuffer内容可变;对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
- StringBuilder内容可变;没有对方法进行加同步锁,所以是非线程安全的。
- == 与 equals()?hashCode() 与 equals() ?
- ==比较的时基本类型的值,引用类型的内存地址
- equals()不能用于基本类型的判断;对于引用类型:没有重写该方法时,等价于==,重写该方法后会比较两个对象的属性是否相等
- hashCode()获取哈希码,作用是确定该对象在哈希表中的索引位置,该方法通常用来将对象的 内存地址 转换为整数之后返回。
- final关键字
-
- 类:该类不能有子类;final类中的所有成员方法都会被隐式的指定为final方法;
-
- 对象:该对象的引用地址不能改变,值可以改变;
-
- 方法:该方法不能被重写;
-
- 变量:该变量会变成常量,值不能被改变。
- static关键字
-
- 修饰成员变量和成员方法: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。随着类的加载而加载。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:类名.静态变量名 类名.静态方法名()
-
- 静态代码块: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
-
- 静态内部类(static修饰类的话只能修饰内部类): 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。
- 注意: a.在静态方法中是没有this关键字的 b.静态方法只能访问静态的成员变量和静态的成员方法
- Java 反射?反射有什么缺点?你是怎么理解反射的(为什么框架需要反射)?
- 反射:就是通过class文件对象,去使用该文件中的成员变量,构造方法,成员方法。
- 谈谈对 Java 注解的理解,解决了什么问题?
- Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符?
- 泛型提供了编译时类型安全检测机制,允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
- 类型擦除:Java 在编译期间,所有的泛型信息都会被擦掉
- 通配符:写在<>中的内容。? 表示不确定,T (type) 表示具体的一个 java 类型,K V (key value) 分别代表 java 键值中的 Key Value,E (element) 代表 Element
- 内部类了解吗?匿名内部类了解吗?
- 静态方法:
- 静态方法中是没有this关键字的
- 静态方法只能访问静态的成员变量和静态的成员方法(原因:静态方法随着类的加载而加载,而非静态成员只有在对象实例化之后才存在。因此,在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。)
- 重载和重写
区别 | 重载 | 重写 |
---|---|---|
发生范围 | 同一个类 | 子类 |
参数列表 | 必须不同 | 必须相同 |
返回类型 | 可修改 | 基本类型不可修改;引用类型:子类方法返回值类型应比父类方法返回值类型更小或相等 |
异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等 |
访问修饰符 | 可修改 | 一定不能更严格 |
发生阶段 | 编译器 | 运行期 |
构造函数 | 可重载 | 不可重写 |
- BIO,NIO,AIO 有什么区别?
-
同步:发起一个调用后,被调用者未处理完请求之前,调用不返回。
-
异步:发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
-
阻塞:就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
-
非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。
-
BIO(Blocking I/O):同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。是一请求一应答通信模型
-
NIO(New I/O):同步非阻塞的I/O模型。核心组件: Channel(通道) Buffer(缓冲区)Selector(选择器)。
与IO的区别:
1.IO流是阻塞的,NIO流是不阻塞的。
2.IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。在NIO厍中,所有
数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。
3.NIO 通过Channel(通道) 进行读写。
4.NIO有选择器,而IO没有。选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来
处理这些通道。
- AIO (Asynchronous I/O):异步非阻塞IO流。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
- 红黑树与AVL(自平衡二叉树)树之间的差别
- 二者都是最常用的平衡二叉搜索树,它们的查找、删除、修改都是O(logN) time
- AVL树更加严格平衡,因此可以提供更快的查找效果,适用于查找密集型任务。但这是以更多旋转操作导致更慢的插入和删除为代价的
- 红黑树在查找,删除和添加方面表现相对较好,适合插入密集型任务
jvm运行时数据区介绍
7.java中如何直接访问内存
8.类加载器,双亲委派机制
9.java线程状态,之间如何转换
解过垃圾回收机制吗?
一般选谁作GC roots?
聊聊各个回收算法的区别
12. 类加载的双亲委派机制,这样做的原因
13 jvm相关,jvm的作用,jvm内存结构,jvm各部分的功能
~~14.jvm垃圾回收机制,堆内存分代,new出来的对象一定在堆中吗,什么时候进行gc,可达性分析法,复制算法,标记清除算法,标记整理算法,三色标记法,垃圾回收器有哪些,垃圾回收的过程(Stop world),CMS和G1的区别。 ~~
3.synchronized 和 Reentrylock 的区别
4. synchronized 的底层实现
5. voliate
6. ThreadLocal 的原理
7. java线程创建方式
8. java线程池创建方式,线程池的主要组成部分,线程池处理任务流程,拒绝策略,有哪几种线程池,newCachedThreadPool的缺点
9. 写个多线程同步的例子 (腾讯)
10. 写个单例模式(线程安全,双重检测的缺点)
11.锁的种类,共享锁互斥锁,读写锁,可重入锁,公平非公平锁,悲观锁乐观锁,无锁算法CAS,ABA问题如何解决ABA
线程创建的方式(runnable和callable的区别,callable返回的是什么)
四. JVM
- Java内存区域(运行时数据区)
- jdk1.8之前
- jdk1.8之后
- 程序计数器(线程私有):记住下一条jvm指令的执行地址。物理上是基于寄存器编写的
- 虚拟机栈(线程私有):每个线程运行需要的内存空间,称为虚拟机栈。每个栈由多个栈帧组成,对应每个方法在执行的同时都会创建一个栈帧 ,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 本地方法栈(线程私有):一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法。本地方法栈是为本地方法提供的
- 堆(线程公有):此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
- 方法区:方法区用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。运行时常量池是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。在老版jdk,方法区也被称为永久代。在1.8之后,由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError,所以在1.8之后废弃永久代,引入元空间的概念。元空间是方法区的在HotSpot jvm 中的实现,元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。理论上取决于32位/64位系统可虚拟的内存大小。可见也不是无限制的,需要配置参数。
- 垃圾回收——作为GC root的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
- 垃圾回收机制
- 引用计数法:引用计数法是一种简单但速度很慢的垃圾回收技术。每个对象都含有一个引用计数器,当有引用连接至对象时,引用计数加1。当引用离开作用域或被置为null时,引用计数减1。虽然管理引用计数的开销不大,但这项开销在整个程序生命周期中将持续发生。垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象引用计数为0时,就释放其占用的空间。
引用种类:
强引用:只有GC Root都不引用该对象时,才会回收强引用对象
软引用:在内存不足时,会回收软引用对象所引用的对象
弱引用:不管内存足不足,都会回收弱引用所引用的对象(注意与软引用的区别)。(使用少)
虚引用:在任何时候都可能被垃圾回收。引用与软引用和弱引用的一个区别在于:
虚引用必须和引用队列(ReferenceQueue)联合使用。(使用少)
- 可达性分析算法:这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
- 垃圾回收算法:
- 停止-复制:先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有被复制的对象全部都是垃圾。 当对象被复制到新堆时,它们是一个挨着一个的,所以新堆保持紧凑排列,然后就可以按前述方法简单,直接的分配了。缺点是一浪费空间,两个堆之间要来回倒腾,二是当程序进入稳定态时,可能只会产生极少的垃圾,甚至不产生垃圾,尽管如此,复制式回收器仍会将所有内存自一处复制到另一处。
- 标记-清除:同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象。每当它找到一个存活的对象,就会给对象一个标记, 这个过程中不会回收任何对象。只有全部标记工作完成的时候,清理动作才会开始。在清理过程中,没有标记的对象会被释放,不会发生任何复制动作。所以剩下的堆空间是不连续的,垃圾回收器如果要希望得到连续空间的话,就得重新整理剩下的对象。
- 标记-整理:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
- 分代收集算法:把Java堆分为新生代和老年代,然后根据各个年代的特点采用最合适的收集算法。新生代中,对象的存活率比较低,所以选用复制算法,老年代中对象存活率高且没有额外空间对它进行分配担保,所以使用“标记-清除”或“标记-整理”算法进行回收。
- 堆的分区:新生代(Eden 空间、From Survivor、To Survivor 空间),老年代
- Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆
- 堆内存分配策略:
- 对象优先在 eden 区分配
- 大对象直接进入老年代:大对象就是需要大量连续内存空间的对象(比如:字符串、数组),为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。
- 长期存活的对象将进入老年代
- 动态对象年龄判定:大部分情况,对象都会首先在 Eden 区域分配,当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。
- GC(垃圾回收)的准确分类为两大类:
- 部分收集 (Partial GC):
1.新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
2.老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是
3.Major GC 在有的语境中也用于指代整堆收集;
混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
- 整堆收集 (Full GC):收集整个 Java 堆和方法区。
- 如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
- 垃圾收集器——CMS执行过程,“标记-清除”算法
- 初始标记(STW initial mark):标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
- 并发标记(Concurrent marking):这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。
- 重新标记(STW remark):这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从"跟对象"开始向下追溯,并处理对象关联。
- 并发清理(Concurrent sweeping):清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
9. 垃圾收集器——G1执行过程
- 标记阶段:首先是初始标记(Initial-Mark),这个阶段也是停顿的(stop-the-word),并且会稍带触发一次yong GC。
- 并发标记:这个过程在整个堆中进行,并且和应用程序并发运行。并发标记过程可能被yong GC中断。在并发标记阶段,如果发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,每个区域的对象活性(区域中存活对象的比例)被计算。
- 再标记:这个阶段是用来补充收集并发标记阶段产新的新垃圾。与之不同的是,G1中采用了更快的算法:SATB。
- 清理阶段:选择活性低的区域(同时考虑停顿时间),等待下次yong GC一起收集,这个过程也会有停顿(STW)。
- 回收/完成:新的yong GC清理被计算好的区域。但是有一些区域还是可能存在垃圾对象,可能是这些区域中对象活性较高,回收不划算,也肯能是为了迎合用户设置的时间,不得不舍弃一些区域的收集。
- Minor GC和Full GC触发条件
- Minor GC触发条件:当Eden区满时,触发Minor GC。
- Full GC触发条件:
1.调用System.gc时,系统建议执行Full GC,但是不必然执行
2.老年代空间不足
3.方法区空间不足
4.通过Minor GC后进入老年代的平均大小大于老年代的可用内存
5.由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
- 对象的创建步骤
Step1:类加载检查:检查指令的参数;检查这个符号是否已经被加载过、解析和初始化过
Step2:分配内存:虚拟机将为新生对象分配内存。分配方式有 “指针碰撞” 和 “空闲列表” 两种,
选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带
有压缩整理功能决定。
Step3:初始化零值:虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头)
保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用
Step4:设置对象头:虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。
这些信息存放在对象头中。
Step5:执行 init 方法:把对象按照程序员的意愿进行初始化
- JVM类加载过程
- 加载:通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的Class对象,作为方法去这个类的各种数据的访问入口
- 验证:验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟自身的安全。
- 准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法去中进行分配。这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
- 解析:解析阶段是虚拟机将常量池内的符号(Class文件内的符号)引用替换为直接引用(指针)的过程。
- 初始化:初始化阶段是类加载过程的最后一步,开始执行类中定义的Java程序代码(字节码)。
- 类加载器——在加载阶段将 .class文件加载到内存
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由C++实现,负责加载 %JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。
- ExtensionClassLoader(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext 目录下的jar包和类,或被 java.ext.dirs 系统变量所指定的路径下的jar包。
- AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。
- 双亲委派机制——系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型
- 如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。
- 好处:双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。
- 堆和栈的区别
- 1.栈内存存储的是局部变量而堆内存存储的是实体;
- 2.栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
- 3.栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。