JAVA复习笔记(面试向)

JVM 内存管理

内存区域

JVM内存分为线程私有的和公共区域,

线程私有的由程序计数器,JAVA虚拟机栈和本地方法栈构成

公共区域分为JAVA堆和方法区,1.8之前方法区叫做永生代,之后改为元空间

程序计数器

程序计数器:可以看作当前线程执行的字节码的行号指示器,字节码解释器就是通过改变程序计数器,选取要运行的下一条字节码。

程序计数器的两个功能:

  • 记录线程运行状态,在发生上下文切换的的时候记录程序的状态,以便恢复到原来的状态

  • 控制程序的执行

程序计数器,线程私有,并且不会发生OOM

JAVA虚拟机栈

虚拟机栈:虚拟机栈描述方法执行的内存模型,存放局部变量表,操作数栈,动态链接和方法出口,一个方法调用到执行完成,就是栈帧在虚拟机栈中从入栈到出栈。

局部变量表:存在局部变量和对象引用

存在StackOverFlowError和OOM

本地方法栈

本地方法栈,和JAVA虚拟机栈基本相同,但是执行的是本地方法。

虚拟机规范中未定义具体实现,HotSpot中和JAVA虚拟机栈合二为一

存在StackOverFlowError和OOM

JAVA堆

JAVA堆用来存放实例对象的内存区域,几乎所有对象都分配在堆

基于垃圾分代回收假说,为了更好的回收垃圾,所以需要对JAVA堆分代。

经典分代:

  • 新生代(Eden,from survivor 和 to survivor)

  • 老年代

其他分代:

将内存区域分为多个region,每个region既可以作为老年代也可以作为新生代

方法区

方法区存放已经加载的类信息、常量和静态变量。

类的信息包括:版本、字段、方法、接口等 还有常量池表

1.7以后将常量和静态变量移出了

1.8以后改作元空间

方法区也存在垃圾回收:常量回收和类卸载

运行时常量池

方法区的一部分,当类被加载后,类的常量信息存放在运行时常量池表

常量池包括:

  • 字面量:字符串值、基本数据类型、final的常量,其他

  • 符号引用:类和结构的名字,字段和描述符,方法名称和描述符

字面量就是值,比如int i = 1;String s =“abc”;1和“abc”就是字面量,s就是符号引用

静态常量池的概念:静态常量池就是在类没被加载的时候,.class文件中的常量池

但是常量池不是必须是编译时期产生的,比如string.intern()方法就可以将字符串对象的字面量放入字符串常量池。

字符串常量池

存放字符串常量的地方,1.8以后在元空间

直接内存

native的方法直接在JVM以外的内存区域分配内存

内存中的对象

一个对象创建

对象创建分五步,

类加载检查,内存分配、初始化、设置对象头、执行init方法

  • 类加载检查:检查对象所属的类是否已经被加载了,如果被加载了,就通过常量池的符号引用在方法区找到这个类。

  • 内存分配:为对象在JAVA堆上分配空间。指针碰撞和空闲列表,对象整理复制和清除。同步性:CAS操作失败重试或者本地线程分配缓冲

  • 初始化:赋0值

  • 设置对象头:对对象必要的设置,比如属于哪个类、hash码、分代年龄还有是否启用偏向锁等

  • 执行init,按照程序猿构造的方法,对对象初始化

对象内存布局

对象在堆内存中,主要分为三个部分:对象头、实例数据、对齐填充

对象头:

  • 对象自身运行数据:hashacode、分代、锁状态(Mark word记录)

  • 对象指针:指向所属类

对象访问方式

句柄和直接访问

句柄:JAVA栈的本地变量表中指向JAVA堆中一个句柄池中的指针,句柄池中指针指向对象实例数据和类型类型数据的指针

直接方法:JAVA栈的本地变量表中指向JAVA堆中实例对象,对象实例数据有指针指向对象类型数据

好处:直接指针,快,节省一次开销,句柄,对象移动,只需要改变句柄。hotspot用直接访问

一个String对象

new一个string对象会产生两个string对象,一个字符串常量在常量池一个堆中string对象

不用new直接拼接,会在堆生成一个对象,然后引用指向常量池,实际上是调用了stringbuilder拼接,然后返回一个对象。

不用new直接写,在常量池,然后引用这个对象

string.intern:第一次在常量池生成一个对象的拷贝,第二次不操作

垃圾回收

引用计数算法和可达性分析算法

判断一个对象是否存活。

引用计数:有一个引用就+1,消失就-1,为0就死亡。hotspot没用,用的是可达性分析算法

可达性分析:从一系列GC Roots出发,按照引用关系向下搜索,完成引用链,不可达的对象就是死亡对象。

GR roots:

  • 虚拟机栈中引用

  • 方法区中静态属性应用的对象

  • 常量区引用的对象

  • 本地方法区native 方法引用的对象

  • 锁持有的对象

  • 虚拟机内部的对象还有类加载器

引用种类

强引用:不会被回收

弱引用:不够了被回收

软引用:下一次被回收

虚引用:不影响回收

finalize()方法

GC Roots不可达之后,进行一次标记,然后检查是否有finalize方法,有就执行一次,没有或者执行过了就可以执行回收。

finalize方法可以让对象自救,比如在对象内引用上自己

方法区回收

同时满足:

  • 所有实例被回收

  • 加载器被回收

  • java.lang.Class对象没有被引用,不可以反射

跨代引用

不同年龄段的对象之间存在指向对方的引用。

垃圾回收的时候如何处理?

使用一个记忆集:一种从非回收区域,指向回收区域的的指针的抽象集合

回收算法

标记清除:效率不稳定、碎片化

标记整理:开销很大

标记复制:浪费一部分内存空间,如果按照1:1分配就会浪费50。hotspot也用的是这种,hotspot中新生代分为8:1:1,Eden和survivor,将一个Eden和survivor复制到另一个survivor。分配不下就内存担保机制。

移动对象开销大,不移动对象分配大。从整个程序上看,移动比较划算。

碎片化问题可以通过执行一段时间之后,标记整理一次。

Hotspot实现细节

GC roots枚举

枚举出GC roots。如何找到引用对象?使用的是Oopmap(ordinary object pointer)普通对象指针。使用OopMap可以快速枚举gc roots,但是导致OopMap变化的指令很多,开销很大。

所以只在特定的位置,生成OopMap,这些位置叫做安全点。

安全点和安全区域

安全点:所以程序只有执行到安全点的时候,才可以垃圾回收。

安全点选择:不至于等待时间太长,也不至于太频繁。所以在循环跳转,方法调用等情况下设置安全点

中断方式:抢断式和主动中断,要准备垃圾收集了就中断所有线程,不在安全点的就恢复执行到安全点,主动式设置标志位,发现标志位就主动执行到安全点中断。

安全区域:安全点选择结局了执行中线程的引用变化问题,但是对于不执行的程序,使用安全区域。线程执行到安全区域就标识一下,出区域的时候检查垃圾回收中gcroot枚举是否完成。

记忆集和卡表

记忆集:一种从非回收区域,指向回收区域的的指针的抽象集合

卡表:记忆集的一种实现,定义记录进度和内存映射关系,就是记录了某一块内存中是否有对象被另一块内存引用

字长精度和对象精度,这俩精度更高

写屏障

维护卡表,保证引用关系变化的时候,卡表可以随之变化。

并发的可达性分析

由于和程序是并发执行的,防止引用关系变化,使用增量更新或者初始快照。

变化导致的错误:

A引用B,然后扫描A的时候,引用被删除,所以B不在引用链,然后之后扫描过的C引用了B

增量更新:当扫过对象增加新的引用的时候,记录下来,结束后,以这些对象为root再扫一遍

初始快照:删除正在扫描对象引用的时候,记下这些对象,结束后重新扫描一遍。

CMS使用增量,G1使用快照

经典垃圾收集器

serial

新生代串行收集器

标记复制

ParNew

新生代并行收集器,serial并行版本

标记复制

CMS默认的新生代收集器

parallel scanvge

新生代并行收集器

标记复制

注重于可预测的停顿时间,可控制的吞吐量,对于暂停时间可以预测,吞吐量优先

通过设置最大停顿时间和和吞吐量大小的参数,调节收集新生代大小,从而决定停顿时间

还可以开启自适应调节

serial old

老年代序列收集器

标记整理

parallel old

老年代并行收集器

标记整理

CMS

四个阶段:初始阶段、并发标记、重新标记、并发清除

初始阶段:GC roots枚举,停顿

并发标记:可达性分析,查找引用链

重新标记:并发标记的时候变化的引用关系重新标记,停顿

并发清除:清除

浮动垃圾和重新标记?

重新标记的是引用链里已经有的对象,但是对象的位置发生了改变,而浮动垃圾是标记过程以后产生的垃圾,不在引用链里了。

标记清除算法

缺点:浮动垃圾、资源敏感、碎片化

G1

Garbage First 是面向局部的垃圾回收器,将内存区域分为不同的region,1.8后支持对类的卸载。

四个阶段:初始阶段、并发标记、最终标记、筛选回收

初始阶段:GC roots枚举,借用minor gc时间完成

并发标记:可达性分析,查找引用链

最终标记:SATB中的记录,再检查一遍

筛选回收:按照能回收的空间回报回收region,直接将一个region中存活对象复制到另一个空region,不要求一次清理干净,不影响执行就可以

SATB 原始快照算法

TAMS指针,用于并发回收的时候分配对象

缺点:内存占用太高,因为需要记忆集记录跨代引用

局部上看标记复制,总体看是标记整理

Hotspot虚拟机实战

-Xms20M -Xmx20M -Xmn10M

-printGCDetials等参数运行虚拟机

证明:

大对象直接进入老年代

内存担保

动态年龄判断:相同年龄所有对象大雨survivor一半

垃圾回收触发条件

minor gc:Eden满了

full gc:至少伴随一次minor

  • 老年代满了

  • 调用system.gc()可能会清理

  • 空间担保失败

  • 1.7之前永久代不足

  • CMS GC过程中,分配新对象内存失败,直接全部暂停full gc一次

CLASS文件结构

魔术和版本

常量池

访问标志

类、父类、接口索引

字段表

方法集合

属性表:属性表记录,上面三个多一些特定场景,例如字段为常量

类加载

类加载机制:虚拟机从class文件,从读取、数据检验、转换分析、初始化,最终形成可以直接使用的JAVA类的过程

类的生存周期:

加载、验证、准备、解析、初始化、使用、卸载

类加载器

将.class文件加载到内存中,变成二进制流,一个class对应一个类加载器

分类:

  • 启动类加载器,C++实现,虚拟机一部分

  • 其他所有类加载器,java.lang.classloader子类

分类2:

  • 启动类加载器

  • 扩展类加载器:加载类库

  • 应用程序类加载器:加载应用程序

双亲委派模型

一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。

好处:避免重新加载和核心api被篡改

加载

加载阶段完成三件事:

  • 将二进制流加载入内存,并且可以通过一个类全名访问

  • 将字节流表示的静态数据结构,转化为方法区内运行时数据结构

  • 内存中生成一个java.lang.Class对象,作为方法区中类的各种数据访问入口。

验证

确保字节流包含的信息符合规范,不会危害虚拟机,因为二进制流不一定都是编译出来的。

准备

为类中变量也就是静态变量分配内存。不会赋值,初始化的时候才会赋值,现在为0

1.8以后随着Class对象一起存放在JAVA堆

解析

将JAVA虚拟机中常量池中符号引用,替换为直接引用的过程。

初始化

按照程序猿的编写的代码,初始化变量和其他资源。

一定要立即初始化

  • new

  • 反射调用reflect

  • 子类初始化时,如果父类没有,先初始化父类

  • 虚拟机启动,用户指定执行的主类

  • default

虚拟机字节码执行

虚拟机字节码执行,是在线程中进行的,方法是执行的基本单元

栈帧是支持调用和执行的数据结构,执行一个方法,就将这个方法的栈帧压入虚拟机栈

每一个栈帧,都包括:本地方法表、操作数栈、动态链接和方法返回地址

只有栈顶的栈帧被执行。

操作数栈

方法执行的过程,就是各种字节码指令往操作数栈中写入和提取的内容

动态链接

栈帧中指向常量池中方法引用的调用过程。

有些类加载的时候就已经转换成直接引用了,有些是运行时才变成直接引用的,这个过程叫做动态链接。

方法调用

确定方法的版本,因为存在多态嘛,和方法执行的调用不是一个含义

解析和分派

前端编译器、即时编译器、提前编译器

前端编译器:编程成字节码,.java编程.class。

即时编译器:一边执行一遍编译热点代码成机器码,运行时编译器JIT just in time

提前编译器:直接编译成机器码,AOT ahead of time complier

优缺点:

  • 即时编译消耗资源

  • 提前编译不能跨平台

JAVA内存模型

内存模型:对内存进行读写访问过程的抽象

主要目的:定义程序中各种变量的访问规则,关注虚拟机如何吧变量值存储到内存,又如何从内存中取出。

JAVA中分为主内存和工作内存,

线程在工作内存执行,变量存储在主内存,通过SAVE和LOAD操作从主内存获取数据。

内存之间操作

lock 作用于主内存,锁定,线程独占

unlock 作用于主内存,解锁

read 作用于主内存,读出数据

load 作用于工作内存,read出来的变量放入工作内存

use 作用于工作内存,给执行引擎

assign 作用于工作内存,执行引擎操作过的值给工作内存

store 作用于工作内存,从工作内存读出,准备给主内存

write 作用于主内存,放入主内存

JAVA线程实现

系统线程实现,用户线程实现和混合实现

线程安全和锁优化

线程安全有互斥同步和非阻塞同步

互斥同步

临界区、互斥量和信号量

synchronized和重入锁

区别:

  • 重入锁可以等待可中断:得不到可以申请放弃

  • 公平锁:按照申请顺序获得,sychronized不公平

  • 绑定多个条件:condition条件控制阻塞

没有特殊需求,推荐synchronized,原因:

  • sychronized代码易读

  • lock需要finally中释放,不释放永久持有

  • 性能差不多

非阻塞同步

CAS指令的支持,完成比较和交换动作,原子性,比较变了吗,没有才执行,会有ABA问题。

乐观锁:就是不断的尝试,可以就修改,不可以就不断尝试

自旋锁和自适应自旋锁

请求不到就等待,但是不切换,不释放资源,因为可能很快就可以获得资源了,减少切换的开销

锁消除

编译的时候,没有发现访问冲突,直接消除锁

锁粗化

同步代码块总是太小,频繁切换会很浪费资源,所以让同步代码块的范围更大

轻量锁

将资源标记与属于一个线程,其他资源访问的时候,先看看有没有人占用,有的话变成传统重量级锁。

基于CAS操作,每次使用的时候CAS检查自己是否拥有。

偏向锁

CAS操作都不做了,获得偏向锁以后,每次都不申请了,直接使用,直到其他进程也要使用,升级成轻量或者重量级锁。

原文地址:https://www.cnblogs.com/CooperXia-847550730/p/12913734.html