文章目录
- 字节流如何转为字符流?
- finalize
- **CMS收集器**
- **CMS的缺点:**
- **使用场景**
- **G1收集器**
- Minor GC 和 Full GC 有什么不同呢?
- 什么是类加载?类加载的过程?
- 堆和栈的区别
- Java对象创建过程
- 简述Java的对象结构
- 调优命令有哪些?
- 调优工具
- 你知道哪些JVM性能调优
- spring[事务传播失效]
- Redis 和 Mysql 数据库数据如何保持一致性
- 一、 延时双删策略
- 二、设置缓存的过期时间
- 三、如何写完数据库后,再次删除缓存成功?
本文包含了String、包装类型、反射、泛型、序列化、异常和IO的常见面试题
JVM是如何处理异常的
serial 单线程: 必须stw 复制算法
ParNew:Serial收集器的多线程版本,也需要stop the world,复制算
Parallel Scavenge: 复制算法的收集器 并发的多线程收集器 可以控制吞吐量
Serial Old: Serial收集器的老年代版本,单线程收集器,使用标记整理算法
Parallel Old : 标记-整理算法。
CMS: 标记清楚: 初始标记 并发标记 重新标记 重新标记 并发清楚 收集结束会产生大量空间碎片;
G1: 标记整理算法+复制:面向服务器
加载: 1 通过类全限定性类名获取该类的二进制流 2 降二进制流的静态存储结构转为方法区运行时数据结构 3 在堆中为该类生成一个class文件
打破双亲委派机制
继承:classLoader 类 loadCLass方法和findclass方法
字符串常量池:
- 在jdk6中,常量池的位置在永久代(方法区)此时常量池中存储的是对象。
- 7 在堆中 此时,常量池存储的就是引用了
- jdk8中,永久代(方法区)被元空间取代了。
字节流如何转为字符流?
字节输入流转字符输入流通过 InputStreamReader 实现,该类的构造函数可以传入 InputStream 对象。
字节输出流转字符输出流通过 OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象。
tale 指针碰撞
myisam:
.frm: 存储表定义
.myd(MYData):存储数据
.MYI(MYindex):存储引擎
innodb:
.frm:存储表定义
.idb:存储数据和索引,在同一个文件中
Full GC触发条件:
事务传播失效
索引类型
finalize
finalize()是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它finalize()方法,让此对象处理它生前的最后事情(这个对象可以趁这个时机挣脱死亡的命运)。要明白这个问题,先看一下虚拟机是如何判断一个对象该死的。
- 对象覆盖写了finalize()方法(这样在被判死后才会调用此方法,才有机会做最后的救赎);
- 在finalize()方法中重新引用到"GC Roots"链上(如把当前对象的引用this赋值给某对象的类变量/成员变量,重新建立可达的引用).
finalize()只会在对象内存回收前被调用一次(The finalize method is never invoked more than once by a Java virtual machine for any given object. );
finalize()的调用具有不确定行,只保证方法会调用,但不保证方法里的任务会被执行完(比如一个对象手脚不够利索,磨磨叽叽,还在自救的过程中,被杀死回收了)
finalize()作用 被认为是做最后资源的回收
基于在自我救赎中的表现来看,此方法有很大的不确定性(不保证方法中的任务执行完)而且运行代价较高。所以用来回收资源也不会有什么好的表现。
综上:finalize()方法并没有什么鸟用。
至于为什么会存在这样一个鸡肋的方法:书中说“它不是C/C++中的析构函数,而是Java刚诞生时为了使C/C++程序员更容易接受它所做出的一个妥协”。
CMS收集器
一种以获取最短时间为目标的收集器,基于 标记清除实现的, 老年代
- 初始标记: 独占**cpu ** stop-the-world, 仅标记GCroots能直接关联的对象,速度比较快;
- 并发标记: 可以和用户线性并发执行 通过GCRoots Tracing 标记所有可达对象;
- 重新标记(CMS remark):独占CPU,stop-the-world, 对并发标记阶段用户线程运行产生的垃圾对象进行标记修正,以及更新逃逸对象;
- 并发清理(CMS concurrent sweep):可以和用户线程并发执行,清理在重复标记中被标记为可回收的对象
优点:
- 支持并发收集.
- 低停顿,因为CMS可以控制将耗时的两个stop-the-world操作保持与用户线程恰当的时机并发执行,并且能保证在短时间执行完成,这样就达到了近似并发的目的.
CMS的缺点:
- cpu卡顿
- 无法处理浮动垃圾
- 会有大量碎片
使用场景
它关注的是垃圾回收最短的停顿时间(低停顿),在老年代并不频繁GC的场景下,是比较适用的。G1收集器
标记整理 复制算法
G1从整体来看是基于 标记-整理 算法实现的回收器,但从局部(两个Region之间)上看又是基于 标记-复制 算法实现的。
G1收集器的内存结构完全区别去CMS,弱化了CMS原有的分代模型(分代可以是不连续的空间),将堆内存划分成一个个Region(1MB~32MB, 默认2048个分区)
Minor GC 和 Full GC 有什么不同呢?
Minor GC:只收集新生代的GC。
Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式。
**Minor GC触发条件:**当Eden区满时,触发Minor GC。
Full GC触发条件Full GC触发条件:
要么是老年代内存过小,要么是老年代连续内存过小。无非是这两点
- 老年待空间不足的时候 不够分配新的内存
- System.gc()方法的调用
- 元空间(Metaspace)内存达到阈值
Metaspace使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关。-XX:MetaspaceSize=21810376B(约为20.8MB)超过这个值就会引发Full GC,这个值不是固定的,是会随着JVM的运行进行动态调整的,与此相关的参数还有多个,详细情况请参考这篇文章jdk8 Metaspace 调优 - 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间
Survivor区域对象晋升到老年代有两种情况: - 一种是给每个对象定义一个对象计数器,如果对象在Eden区域出生,并且经过了第一次GC,那么就将他的年龄设置为1,在Survivor区域的对象每熬过一次GC,年龄计数器加一,等到到达默认值15时,就会被移动到老年代中,默认值可以通过-XX:MaxTenuringThreshold来设置。
另外一种情况是如果JVM发现Survivor区域中的相同年龄的对象占到所有对象的一半以上时,就会将大于这个年龄的对象移动到老年代,在这批对象在统计后发现可以晋升到老年代,但是发现老年代没有足够的空间来放置这些对象,这就会引起Full GC。 - 堆中产生大对象超过阈值
这个参数可以通过-XX:PretenureSizeThreshold进行设定,大对象或者长期存活的对象进入老年代,典型的大对象就是很长的字符串或者数组,它们在被创建后会直接进入老年代,虽然可能新生代中的Eden区域可以放置这个对象,在要放置的时候JVM如果发现老年代的空间不足时,会触发GC。 - CMS GC时出现promotion failed和concurrent mode failure**
什么是类加载?类加载的过程?
加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。
加载:
双亲委派机制
- 通过全类名 获取该类的二进制流,
- 降二进制流的静态储存转为方法区的运行时数据结构
- 在堆中为该类生产一个class对象
验证:验证该class文件中的字节流信息复合虚拟机的要求,不会威胁到jvm的安全;
- 文件格式
- 元数据校验
- 字节码
- 符号引用
准备:为class对象的静态变量分配内存,初始化其初始值;
解析:该阶段主要完成符号引用转化成直接引用;
所谓的符号引用是指以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可;而直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
符号引用和直接引用有一个重要的区别:使用符号引用时被引用的目标不一定已经加载到内存中;而使用直接引用时,引用的目标必定已经存在虚拟机的内存中了。
初始化:
初始化阶段 JVM 就正式开始执行类中编写的 Java 业务代码了。到这一步骤之后,类的加载过程就算正式完成了。
堆和栈的区别
栈是运行时单位,代表着逻辑,内含基本数据类型和堆中对象引用,所在区域连续,没有碎片;
堆是存储单位,代表着数据,可被多个栈共享(包括成员中基本数据类型、引用和引用对象),所在区域不连续,会有碎片。 堆分配的空间在逻辑地址上是连续的,但在物理地址上是不连续的
- 功能不同: 栈内存用来储存局部变量和方法的调用,堆内存用来储存Java中的对象,无论是成员变量,局部变量, 还是类变量,它们指向的对象都存储在堆内存中。
- 共享不同: 栈内存是线程私有的。堆内存是所有线程共有的
- 异常错误不同:
1.如果采用固定大小的java虚拟机栈,那每一个线程的java虚拟机栈的容量可以在线程创建的时候独立选定。如果线
程请求分配的容量超过Java虚拟机栈允许的最大容量
,java虚拟机将会抛出一个StackOverflowError异常。 那么最有可能的就是方法递归调
2.如果虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,java虚拟机会抛出OOM异常。空间大小**
栈的空间大小远远小于堆的 如果本地方法可以动态扩展,并在扩展时无法申请到足够的内存会抛出 OutOfMemoryError
- 空间大小 :栈的空间大小远远小于堆的
Java对象创建过程
- JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在 后边讲
- 为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,最终常用的办法“本地线程缓冲分配(TLAB)”
指针碰撞:假设Java堆中内存时完整的,已分配的内存和空闲内存分别在不同的一侧,通过一个指针作为分界点,需要分配内存时,仅仅需要把指针往空闲的一端移动与对象大小相等的距离。使用的GC收集器:Serial、ParNew,适用堆内存规整(即没有内存碎片)的情况下。
空闲列表法: 事实上,Java堆的内存并不是完整的,已分配的内存和空闲内存相互交错,JVM通过维护一个列表,记录可用的内存块信息,当分配操作发生时,从列表中找到一个足够大的内存块分配给对象实例, 使用CMS.适合堆内存不规则的情况下
- 将除对象头外的对象内存空间初始化为0
- 对对象头进行必要设置
- 类加载机制检查:JVM首先检查一个new指令的参数是否能在常量池中定位到一个符号引用,并且检查该符号引用代表的类是否已被加载、解析和初始化过
- 分配内存:把一块儿确定大小的内存从Java堆中划分出来
- **初始化零值:**对象的实例字段不需要赋初始值也可以直接使用其默认零值,就是这里起得作用
- **设置对象头:**存储对象自身的运行时数据,类型指针
- 执行:为对象的字段赋值
简述Java的对象结构
Java对象由三个部分组成:对象头、实例数据、对齐填充。
对象头由两部分组成,
- 第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。
- 第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的) 对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐 )
调优命令有哪些?
Sun JDK监控和故障处理命令有jps jstat jmap jhat jstack jinfo
- jps,JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
- jstat,JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
- jmap,JVM Memory Map命令用于生成heap dump文件
- jhat,JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看
- jstack,用于生成java虚拟机当前时刻的线程快照。
- jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数
调优工具
常用调优工具分为两类,jdk自带监控工具:jconsole和jvisualvm,第三方有:MAT(Memory AnalyzerTool)、GChisto。
- jconsole,Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存, 线程和类等的监控
- jvisualvm,jdk自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC变化等。
- MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Javaheap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗
- GChisto,一款专业分析gc日志的工具
你知道哪些JVM性能调优
设定堆内存大小
-Xmx:堆内存最大限制。
设定新生代大小。 新生代不宜太小,否则会有大量对象涌入老年代 ;
-XX:NewSize:新生代大小
-XX:NewRatio 新生代和老生代占比
-XX:SurvivorRatio:伊甸园空间和幸存者空间的占比
设定垃圾回收器 年轻代用 -XX:+UseParNewGC 年老代用-XX:+UseConcMarkSweepGC
spring事务传播失效
不能是同一个类中的方法
出现异常
非piblic方法
使用不当
索引类型
在spring自动注入是会发生循环依赖问题
借助 周期过程
创建原始对象
属性注入(注入时默认不能为空)
解决问题:
- 一级缓存: 实例化注入初始化完成的
- 二级缓存: 实例化.初始化完成的Bean
- 三级缓存: 实例化后的半成品 (添加后带有B的AOP)
A创建原始对象–> 缓存起来
依赖B—>创建B原始对象 缓存起来
注入A 从缓存中查找半成品A
B 初始化
A把B从缓存中拿到赋给A中的B
Redis 和 Mysql 数据库数据如何保持一致性
一、 延时双删策略
多线程下 修改后加一个状态值: 让后来的线程进行睡眠
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体步骤是:
1)先删除缓存
2)再写数据库
3)休眠500毫秒(根据具体的业务时间来定)
4)再次删除缓存
二、设置缓存的过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时
三、如何写完数据库后,再次删除缓存成功?
上述的方案有一个缺点,那就是操作完数据库后,由于种种原因删除缓存失败,这时,可能就会出现数据不一致的情况。这里,我们需要提供一个保障重试的方案。