JVM 内存管理
内存区域
JVM内存分为线程私有的和公共区域,
线程私有的由程序计数器,JAVA虚拟机栈和本地方法栈构成
公共区域分为JAVA堆和方法区,1.8之前方法区叫做永生代,之后改为元空间
程序计数器
程序计数器:可以看作当前线程执行的字节码的行号指示器,字节码解释器就是通过改变程序计数器,选取要运行的下一条字节码。
程序计数器的两个功能:
-
记录线程运行状态,在发生上下文切换的的时候记录程序的状态,以便恢复到原来的状态
-
控制程序的执行
程序计数器,线程私有,并且不会发生OOM
JAVA虚拟机栈
虚拟机栈:虚拟机栈描述方法执行的内存模型,存放局部变量表,操作数栈,动态链接和方法出口,一个方法调用到执行完成,就是栈帧在虚拟机栈中从入栈到出栈。
局部变量表:存在局部变量和对象引用
存在StackOverFlowError和OOM
本地方法栈
本地方法栈,和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检查自己是否拥有。
偏向锁