Java内存区域与内存溢出异常
运行时数据区域
- 程序计数器:当前线程执行字节码的行号指示器,可以通过改变计数器的值来选取下一条需要执行的字节码指令--线程私有
- Java虚拟机栈:方法运行栈,用于存储方法运行的局部变量表,操作栈,动态链接,方法出口等信息;-生命周期和线程相同,StackOverflowError和OutOfMemoryError---线程私有
- 局部变量表:存放编译期可知的各种基本数据类型,对象引用(指向对象起始地址的引用指针)
- 本地方法栈:Native方法运行时栈--线程私有
- Java堆:线程共享内存,存放对象实例,OutOfMemoryError--线程共享
- 方法区:存储已经被虚拟机加载的来信息,常量,静态常量,即时编译器编译后的代码OutOfMemoryError;
- 运行时常量池:存放编译期生成的各种字面量和符号引用,也可以将运行期间产生的新的常量放入常量池中,比如String的intern()方法;
对象访问
Object obj = new Object();
Object obj会反映到虚拟机栈的本地变量表中,new Object会反映到Java堆中,形成一块存储Object类型实例数据值的结构化内存;长度会根据类型不同;此外,在Java堆中还包含能查找到此对象类型数据(对象类型,父类型,接口信息,方法等)的地址信息,这些类型数据存放在方法区中;
对象访问方式有两种:使用句柄和直接指针;HotSpot使用的是直接指针访问方式;
垃圾收集器与内存分配策略
对象存活判断方法:
- 引用计数法:很难解决对象之间相互引用的问题
- 根搜索算法:通过GCroot对象作为起点,从根节点向下搜索,如果到根对象没哟引用链则说明对象不可用的;可以作为GCroot的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中的静态属性引用的对象;
- 方法区中的常量引用的对象
- 本地方法栈中的Native方法引用的对象;
引用的几种类型
- 强引用:强引用存在时对象不会被回收
- 软引用:系统将要发生内存溢出之前,会对软引用进行回收,如果回收之后还是没有足够的内存才会抛出内存溢出异常;也就是说,软引用只有在即将内存不足时才会被回收,否则不会被回收
- 弱引用:弱引用只能生存到第二次对象回收之前,即弱引用对象可以经历一次对象回收而不被回收
- 虚引用:虚引用对回收没有影响,只会在回收该对象之前向系统发出一个通知;
对象死亡判断
对象废弃至少经历两次标记过程:没有到根对象的引用链进行第一次标记;并进行一次筛选,如果该对象还需要执行finalize()方法,该对象会被放置在F-Queue队列中等待Finalizer线程去执行finalize()方法;之后才会被第二次标记,如果在finalize()方法中重新建立引用链则不会被标记回收;fainalize()方法只会被虚拟机执行一次,下次回收不会被执行该方法;防止一个对象永远不会被回收;(使用try()finally()方法可以完成fianlize()方法的功能);
垃圾回收算法
- 标记-清除算法:首先标记需要回收的对象,标记完成后统一进行回收;缺点:效率问题;产生空间碎片;
- 复制算法:将内存分为大小相等的两块,当一块内存用完时,将其中活着的对象复制到另一块内存中,对用完的内存进行统一回收;改进:不按1:1划分
- 标记-整理算法:让所有活着的对象向一端移动,然后清理掉边界以外的内存;
- 分代收集算法:根据对象的存活周期将内存划分为几块,根据各个年代的特点采用最适合的回收算法;比如;新生代中如果回收时存活的对象较少,则使用复制算法,老年代中使用标记-清除算法
垃圾收集器
HotSpot中常用的几种垃圾收集器:
- Serial收集器:一种单线程收集器,且回收过程中工作线程必须停止;简单高效,对于运行在Client模式下的虚拟机是很好的选择
- SerialOld收集器:回收老年代的版本,使用标记-清除算法;被Client模式下的虚拟机使用;
- ParNew收集器:Serial收集器的多线程版本,可以与CMS收集器共同工作,所以是运行在Server模式下的虚拟机首选的新生代收集器;
- Parallel收集器:一个新生代收集器,使用复制算法,并行多线程执行;关注CPU吞吐量;
- ParallelOld收集器:
- CMS收集器:获取最短回收停顿时间为目标的收集器;标记-清除算法;
- G1收集器:标记-整理算法
内存分配与回收策略
Java的自动内存管理--给对象分配内存及回收分配给对象的内存;
对象内存分配就是在堆上为新建的内存进行分配;对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配;几个普遍的内存分配原则:
- 对象优先在Eden分配
新生对象大多数情况下在新声代Eden区中分配,当内存不足时,虚拟机将发起一次MonitorGC;
- 大对象直接进入老年代
为了避免在新生代和老年代之间发生大量内存复制,默认超过3M的对象将直接在老年代上进行分配
- 长期存活的对象将进入老年代
使用对象年龄计数器对对象年龄进行计数,每经过一次MonitorGC,年龄就加1,默认为15岁的时候对象将被复制到老年代中;
- 动态对象年龄判定
如果在Survivor空间中相同年龄所有对象大小超过了Survivor空间的一半,则可以直接在下次GC时被复制到老年代中
- 空间分配担保
类加载机制
- 加载
- 使用一个类的全限定名来获取定义此类的二进制字节流
- 将字节流代表的静态存储结构转化为运行时数据结构
- 在Java堆中实例化一个对应的Class对象,作为方法区访问这些数据的访问入口
- 验证
- 准备
为变量分配内存并设置类变量初始值(类型零值),内存中在方法区中进行分配(仅限静态类变量static)
- 解析
将常量池中的符号引用替换为直接引用的过程;符号引用是一个能定位到变量的符号,直接引用可以是指针,偏移量,直接指针存在时引用目标必定已经在内存中存在;
- 初始化
执行构造方法
有且只有四种情况类才被初始化
- 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,
- 使用java.lang.reflect对类方法进行反射调用时,
- 当初始化一个类时,首先对其父类进行初始化,
- 虚拟机启东时必须先初始化主类
除此之外的被动引用不会触发类的初始化;比如通过子类引用父类的静态域,只会触发对父类的初始化而不会触发子类的初始化;
- 使用
- 卸载
双亲委派模型
几种类加载器:
- 启动类加载器--加载lib文件和类路径定义下的类库文件
- 扩展类加载器--加载lib/ext目录下的扩展类库
- 应用程序类加载器--加载用户类路径上的类库
双亲委派模型要求除了顶层的启动类加载器之外,所有的类加载器都应当有自己的父类加载器;如果一个类加载器收到了加载请求,首先将请求委托给父类加载器来完成
虚拟机字节码执行引擎
运行时帧栈结构
栈帧用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中虚拟机栈的栈元素,存储了局部变量表,操作数栈,动态链接和方法返回地址等信息,局部变量表,操作数栈的空间大小已经确定了;因此一个栈帧需要分配多少内存不会受到程序运行期间数据的影响;而仅仅取决于虚拟机的实现;
局部变量表
存储变量值的空间;用于存放方法参数和方法内部定义的局部变量;局部变量表的容量以变量槽为最小单位,虚拟机规范中没有明确指明一个slot应该占用的内存大小;但是一个slot应该可以存放一个32位以内的数据类型(boolean,byte,char,short,int,float,reference,和returnAddress八种类型),其中,reference应该直接或者间接的指明对象在Java堆中的起始地址索引和方法区中的对象类型数据;而returnAddress指向方法返回的字节码指令地址;
局部变量表建立在线程私有内存空间中,因此不会引起数据线程安全问题;
操作数栈
操作数栈是方法执行时需要的,例如算数运算以及调用其他方法时通过操作数栈来传参;
动态链接
字节码中的方法调用指令以常量池中指向方法的符号引用作为参数,这些符号引用一部分会在类加载或第一次使用的时候转化为直接引用,另外一部将在每一次运行期间转化为直接引用,这部分称为动态链接;
可以在解析阶段确定方法直接引用的有四种方法:
- 静态方法
- 私有方法
- 实例构造器
- 父类方法
分派
- 静态单分派--静态重载
- 静态多分派--
- 动态单分派--重写
- 动态多分派
重写方法匹配过程:
invokevirtual指令的多态查找:
- 周到操作数栈顶的第一个元素所指的对象的实际类型,
- 如果在类型中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,
- 否则,按照继承关系从下往上依次对对象的各个父类进行第二步的搜索和验证
- 如果始终没有找到合适的方法,则抛出AbstractMethodError异常;
invokevirtual方法第一步是确定的接受者的实际类型----这种在运行期根据实际类型确定方法执行版本的分派称为动态分派;
Java是静态多分派(接受者类型和参数类型),动态单分派(接受者类型)
动态分派实现_itable和vtable
方法返回地址
方法有两种退出方式:遇到方法返回的字节码指令(正常返回),遇到异常返回,异常返回是不会给上层调用者返回任何返回值的;
基于栈的字节码解释执行引擎
程序编译与代码优化
几种编译过程:
- 前端编译器:把.java文件编译为.class文件的过程;
- 即时编译器JIT: 把字节码转化为机器码的过程;
- 静态提前编译:AOT编译器:直接把.java文件编译为本地机器码;
javac的编译过程大概分为三个过程:
- 解析与符号表填充过程
- 插入式注解处理器的注解处理过程
- 分析与字节码生成过程
解析与填充符号表
- 词法分析与语法分析
高效并发
TPS:一秒内服务端平均能响应的请求总数;
解决缓存一致性的协议:MSI,MESI,MOSI;等
为了使得处理器内部的运算单元能够尽量被充分利用,处理器可能会对代码进行乱序执行优化,_JVM会进行指令重排优化;
Java内存模型
Java内存模型主要是为了定义程序中各个变量的访问规则;即将虚拟机中的变量存储到主内存中以及将变量从主内存中取出来;
线程的工作内存保存了该线程使用到的变量的主内存副本拷贝;线程所有对变量的操作必须在工作内存中完成,而不能直接读写主内存;不同的线程之间也无法直接访问对方线程工作内存中的变量;线程之间的数据交互通过主内存来完成;
内存之间的交互操作:一个变量如何从主内存拷贝到工作内存以及如何从工作内存中写入主内存中,Java定义了8中操作_
- lock
- unlock
- read
- load
- use
- assign
- store
- write
其中,read和load store和write必须是顺序执行的操作(可以不连续)
volatile变量的特殊规则:
自增操作等非原子运算操作对于volatile变量仍然是线程不安全的;因为volatile变量只保证每次线程读取的时候强制从主内存中加载到工作内存,而不是直接使用工作内存中的值,但是对于非原子性的运算操作,某一线程从主内存中读取之后在运算的时候有可能该变量被其他线程已经修改了,所以在除了以下两种情境中,仍然要通过加锁来保证操作的线程安全性:(换句话说,volatile只在以下两种场景中保证线程安全)
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值;
- 变量不需要与其他的状态变量共同参与不变约束
volatile变量的第二个语义是阻止指令重排,
volatile是Java虚拟机提供的最轻量级的同步机制,
性能比较:由于JVM对锁的消除和优化,使得volatile未必比锁更具有性能优势,一般只在适合使用volatile的情况下使用;同时,volatile的读操作与普通变量相比差不多,但是写操作会慢上一些;
volatile的作用:
- 在工作内存中,每次使用volatile变量的时候必须先从主内存中刷新最新的值;
- 在工作内存中,每次对volatile变量修改以后必须立刻同步到主内存中;
- volatile修饰的变量不会被指令重排优化;
long和double类型变量的特殊规则( long和double的非原子性协议):虚拟机允许将64位的long和double的读写操作分为两次32位的操作来进行,不过一般的虚拟机实现都将这个64位的读写操作实现为一个原子性操作;
先行发生原则:定义两个操作的顺序关系:如果A先行发生于B,则A中对共享变量的操作是可以被B观察到的;
线程实现:Java中主流的线程实现是一对一模型,即一条用户线程映射到一条内核线程中;
Java线程调度:
主要有两种线程调度方式:抢占式线程调度和协同式线程调度;
Java中使用的是抢占式线程调度的方式;
Java中的线程状态转换:Java中定义了五种线程状态:
- 新建
- 运行
- 无限期等待Waiting
- 限期等待Timed Waiting
- 阻塞
- 结束
线程安全与锁优化
Java中线程安全的五个级别:
- 不可变
- 绝对线程安全
- 相对线程安全
- 线程兼容
- 线程对立
线程安全的实现方法:
- 互斥同步
synchronized是Java中最基本的互斥同步操作;synchronized关键字经过编译以后会在同步代码块的前后分别形成monitorenter和monitorexit两个字节码指令;这两个字节码指令都需要一个reference类型的参数赖志明要锁定和解锁的对象,如果代码中指明了对象参数,则该参数就是synchronized指定的对象,否则会根据synchronized修饰的是实例方法还是类方法来获取该实例对象或者Class对象;同时synchronized同步是可重入的;
同时,重入锁ReentrantLock也可以实现互斥锁,不过,ReentrantLock可以实现等待可中断,公平锁,锁可以绑定多个条件等高级功能:
- 等待可中断:持有锁的线程长期不释放锁的时候,正在等待的线程可以放弃等待,选择处理其他事情;
- 公平锁:按照申请锁的时间先后顺序来依次获得锁,而不是严格按照线程优先级来获得锁;
- 锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个Condition对象,但是高版本的JDK中ReentrantLock和Synchronized的性能在该功能实现上差别不大,所以更推荐使用Synchronized进行同步操作;
- 非阻塞同步
互斥同步最主要的问题是进行线程阻塞带来的性能问题;因此这种同步也被称为阻塞同步;属于悲观锁的一种实现方式;
非阻塞同步是一种乐观锁的实现方式---先进行操作,如果没有其他线程争用共享数据,操作就成功了,如果共享数据有竞争,就进行补偿措施---最常见的补偿措施是不断重试,直到重试成功为止;这种乐观锁不需要将线程挂起;
CAS的ABA问题解决方法----通过控制变量的版本值来保证CAS的正确性;大部分情况下ABA问题不会影响程序执行的结果的正确性;
- 无同步方案
无同步方案的线程安全是指如非必要,将变量使用同一个线程进行操作而不是用多线程进行共享;
ThreadLocal--每一个Thread对象中都有一个ThreadLocalMap对象,这个对象以ThreadLocalHashCode为键,以本地线程变量为值的键值对;TheadLocal对象就是该Map对象的访问入口;
锁优化方案
- 自旋和适应性自旋
自旋是指让线程遇到锁被占用的时候在一段时间内进行忙等待,而不是直接被阻塞,以消除频繁线程调度引起的开销
适应性自旋是指自旋的时间会根据上一次自旋等待时间发生动态变化,而不是一直使用固定的自旋时间;比如一个线程如果一直自旋失败的话有可能会被虚拟机取消自旋机会儿直接被阻塞;
- 锁消除
锁消除是指对于一串代码中,如果在对上访问的数据都不会逃逸出去被其他线程访问到,就可以将其当做线程私有数据对待,取消对其加锁操作;直接同步运行;
- 锁粗化
如果虚拟机探测到有一串零碎的操作都对同一对象加锁,为了避免频繁进行锁的释放和加锁操作带来的性能问题,会将这段代码作为一个整体进行加锁,
- 轻量级锁
- 偏向锁