一、线程部分
1、Java 中引用类型都有哪些?
(1)强引用。在虚拟机内存不足的情况下,也不会回收,如果我们把(强引用)对象置为null,会大大增加了垃圾回收的频率。几乎只要我们给出建议(GC),JVM就会回收。
Object o = new Object();
Object o1 = o;
(2) 软引用。如果不显式的置为null,跟强引用差不多。垃圾回收不会执行。只会等到内存不足的时候才会调用。
(3)弱引用。就算不置为null,发生垃圾回收时,会立即被回收。
(4)虚引用。相当于null
2、谈一谈java 线程模型
线程的实现可以分为两类:用户级线程(User-LevelThread, ULT)和内核级线程(Kemel-LevelThread, KLT)。用户线程由用户代码支持,内核线程由操作系统内核支持。
(1)一对一模型
每个用户线程对应一个内核线程。由于每个用户线程对应自己的内核线程,所以他们互不影响,当一个线程阻塞,也允许另外的线程继续执行,这是此模型的优点。但也存在一个缺陷,由于一对一的关系,有多少用户线程就代表有多少内核线程,由于内核线程的开销比较大,一般操作系统会都有内核线程数量的限制,所以也限制了用户线程的数量。
(2)多对一模型
一个内核线程实现若干个用户线程的并发功能,线程的管理在用户空间中进行,一般不需要切换到内核态,效率较高,而且比起一对一模型,支持的用户线程数量更多。但此模型有个致命的弱点是如果一个线程执行了阻塞调用,所有的线程都将阻塞,并且任意时刻都只能有一个线程访问内核。另外,对线程的所有操作,都将由用户自己处理。一般除了不支持多线程操作的系统被迫使用此模型外,在多线程操作系统中不会使用该模型。
(3)多对多模型
多对多模型的提出是为了解决以上两种模型的缺点,多个用户线程与多个内核线程映射形成了多路复用。前面一对一模型存在受内核线程数量限制的问题,多对一模型解决了这个问题,但它存在一个线程阻塞所有线程都阻塞的风险,而且一个内核线程只能调用一个用户线程导致并发性不强。看看多对多模型如何解决这些问题,由于多对一是多对多的子集,所以多对多具备多对一的优点,线程数不受限制。除此之外,多个内核线程可处理多个用户线程,当某个线程阻塞时,将可以调度另外一个线程执行,这从另一方面看也是增强了并发性。
二、JVM
1、JVM内存结构说一下
(一)运行时数据区:JVM在执行Java程序过程中,把所管理的内存划分为不同的区域。有
(1)程序计数器。
记录当前线程正在执行字节码指令的地址,因为CPU的时间片轮转机制,当CPU切换到其他线程执行后再切回当前线程根据程序计数器的记录继续执行。它是线程私有的,也是唯一一块不会发生OOM的区域。
(2)虚拟机栈
栈的特点是先进后出(FILO),存储当前线程运行方法所需的数据、指令、地址。它是线程私有的。里边有若干栈帧(每个java方法对应一个栈帧),栈帧有局部变量表、操作数栈、动态链接、完成出口(返回地址)组成。局部变量表中存放8大基本数据类型和对象的引用;操作数栈存放方法的执行和操作;动态链接:是由于多态,静态分派、动态分派;
完成出口(返回地址):
把符号引用转换成具体引用
正常返回,会调用程序计数器中的地址进行返回;
异常返回,会根据异常处理表来确定。
在Java中的解析执行是基于栈的,基于栈帧的执行主要说的是操作数栈。c语言是基于寄存器的。
Java基于栈:兼容性好,效率低;基于寄存器:寄存器是硬件,所以快一点,但移植性差。
可以再讲一下方法执行的流程。
(3)本地方法栈
保存的是native方法的信息。它是线程私有的。
当 JVM 创建的线程调用native方法时,JVM不会在虚拟机栈上创建栈帧,只是简单的动态链接并直接调用native方法。程序计数器也不会记录。
(4)方法区
存放着class类信息、静态变量、常量、即时编译期编译后的代码。线程间共享的。
实现方式 JDK 1.8 以后是元空间,JDK1.7是永久代。元空间可以使用硬件内存(堆以外的内存),方便扩展;永久代是受制于堆内存。
(5)堆
存放着几乎所有的对象、数组。线程间共享的。
方法区和堆为什么划分为两个区?
堆中存放着几乎所有的对象、数组,这些是频繁回收的;方法区中存放的类信息、常量、静态变量、即时编译期编译后的代码回收的难度是相当大的。动静分离的思想,便于垃圾回收的高效。
(二)直接内存(堆外内存)
不是JVM运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
如果使用NIO会频繁的使用该区域,在Java堆内使用 DirectByteBuffer 对象可以直接引用并操作。
不受堆内存大小的限制,但受本机内存大小的限制。可以通过MaxDirectMemorySize来设置,默认与堆内存最大值一样,所以也会出现OOM异常。
2、什么情况下内存栈溢出?
虚拟机栈是有深度限制的,如果加载的方法过多会发生 stackoverflowerror 造成溢出;栈内存空间不足也会发生OOM。
3、描述一下new 一个对象的过程?
首先,<u>检查加载</u>,若类信息没有加载进方法区,进行 <u>类加载</u>。若已加载,进行<u>分配内存</u>,如果内存是规整的,使用 指针碰撞 进行内存分配,若内存不规整,使用 空闲列表 进行内存分配。同时要注意并发安全问题,CAS加失败重试、本地线程分配缓存(TLAB)。本地线程分配缓存(TLAB)是线程的一块私有内存,在堆的Eden区中开辟一块区域来存放对象。对象占据的内存一定是连续的。之后是<u>内存空间初始化</u>,也就是把数据初始值置为零值,比如int类型的 a = 0; boolean类型的 b = false。然后进行<u>设置</u>,设置对象头等信息。最后是<u>对象初始化</u>,调用对象的构造函数等。
TLAB是线程的一块私有内存,它是虚拟机在堆内存的eden划分出来的,如果设置了虚拟机参数 -XX:UseTLAB,在线程初始化时,同时也会申请一块指定大小的内存, 只给当前线程使用, 这样每个线程都单独拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,这样就不存在竞争的情况,可以大大提升分配效率,当Buffer容量不够的时候,再重新从Eden区域申请一块继续使用。
4、Java对象会不会分配在栈中?
会分配在栈中。满足逃逸分析的对象会直接分配在栈中。
逃逸分析:方法逃逸、线程逃逸
对象分配的原则:
- 对象优先在Eden区分配
- 空间分配担保
- 大对象直接进入老年代
- 长期存活的对象进入老年代
- 动态判断对象的年龄
堆中的优化技术: 本地线程分配缓冲(TLAB)是线程的一块私有内存.
new出一个对象,首先判断是否满足栈中分配的条件(逃逸分析),若满足直接在栈中分配;若不满足,继续判断是否可以使用 本地线程分配缓冲,若可以在堆的Eden区单独开辟出TLAB区存放,则存放在TLAB区,若不行,则继续判断是否是大对象,如果是大对象,直接在老年代分配内存,若不是,分配在Eden区。
5、判断一个对象是否被回收,有些算法,实际虚拟机使用的最多的是什么?
(1)引用计数法
python中使用的是这种。有引用,计数+1,不在引用,计数 -1,当计数=0,就回收。存在一个问题,对象间的相互引用问题,需要额外的补偿机制回收相互引用的对象。
(2)可达性分析算法(根可达)--- 实际虚拟机使用的最多
可以成为 GC Roots 的对象有:静态变量、线程栈变量(虚拟机栈中的局部变量中的变量)、常量池、JNI指针,还有class、异常NullPointException等、类加载器、加锁的synchronized对象、jmxBean、临时性的等。
6、GC算法有哪些?它们的特点是?
(1)复制算法,把使用的对象从一块空间复制到另一块空间。特点:实现简单、运行高效;内存复制,没有内存碎片;空间利用率只有一半。
(2)标记清除算法。特点:执行效率不稳定,因为需要回收的对象多少不确定;内存碎片会导致提前GC。
(3)标记整理算法。特点:对象移动;引用更新;用户线程暂停;没有内存碎片。
7、JVM中一次完成的GC流程是怎样的?对象如何晋级老年代?
8、final、finally、finalize的区别?
final:
修饰类,表示类不可扩展。
修饰方法,一是不可修改,二是效率。
修饰变量,只能赋值一次。
finally:
try catch finally 不管有没有异常都会执行,释放资源
finalize:
Object中的方法
对象垃圾回收可以拯救一下对象,不可靠,一般不用
9、String s = new String("abc"); 创建了几个对象?
2个。
首先,在编译文件时,"abc"常量字符串会加入到常量结构中,在类加载时,"abc"会在常量池中创建;
其次,调用new 时, jvm 命令将调用String的构造函数,同时引用常量池中的"abc"字符串,在堆中创建一个对象;
最后,s引用这个对象。
常量池(方法区)分为静态常量池、运行时常量池。
静态常量池中,存放字面量、符号引用、类信息、方法的信息等
运行时常量池中,存放 类加载,加载到运行时数据区的方法区、转换成实体类,把符号引用变成直接引用(hash值)