JAVA内存区域
线程私有的包括:
程序计数器
- 若正在执行的是java方法,则计数器记录的是正在执行的字节码指令的地址
- 若正在执行的是native方法,则计数器为空
- 该区域是唯一一个不会导致OutOfMemoryError的区域
虚拟机栈
- 描述的是Java方法执行的内存模型:每个方法都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息
- 局部变量表存放了编译期可知的基本数据类型,对象引用,和returnAddress类型(指向一条字节码指令地址),局部变量表的内存空间在编译器确定,在运行期不变
- 可导致两种异常:线程请求的栈深度大于虚拟机允许的深度-StackOverFlowError;虚拟机无法申请到足够的内存-OutOfMemoryError
本地方法栈
- 和虚拟机栈类似,但它是为Native方法服务的
线程共享的包括:
堆
- java堆是被所有线程共享的内存区域,在虚拟机启动时创建,用来分配对象实例和数组
- 堆是垃圾回收器主要管理的区域,可分为新生代和老年代,新生代分为有Eden 空间、From Survivor空间、To Survivor空间
- 大小可通过 -Xmx 和 -Xms 控制
方法区
- 用来存放虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等信息
- GC会回收该区域的常量池和进行类型的卸载
运行时常量池
- Class文件的常量池用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放在运行时常量池中
- 还把翻译出来的直接引用也放在运行时常量池中,运行时产生的常量也放在里面
直接内存
- 并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,是NIO使用Native函数库直接分配堆外内存
HotSpot虚拟机在Java堆中对象分配、 布局和访问的全过程
对象创建
- 虚拟机遇到一条new指令时, 首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程
- 内存分配
指针碰撞
如果JVM的垃圾收集器采用复制算法或标记-整理算法,那么堆中空闲内存是完整的区域,并且空闲内存和已使用内存之间由一个指针标记。那么当为一个对象分配内存时,只需移动指针即可。因此,这种在完整空闲区域上通过移动指针来分配内存的方式就叫做“指针碰撞”。
空闲列表
如果JVM的垃圾收集器采用标记-清除算法,那么堆中空闲区域和已使用区域交错,因此需要用一张“空闲列表”来记录堆中哪些区域是空闲区域,从而在创建对象的时候根据这张“空闲列表”找到空闲区域,并分配内存。
安全性
同步
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
本地线程分配缓冲(TLAB)
把内存分配的动作按照线程划分为在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。
- 内存空间初始化
- 对对象进行必要的设置(例如这个对象是哪个类的实例、如何才能找到 类的元数据信息、对象的哈希码、对象的GC分代年龄等信息)
- 执行init,完成
对象的内存布局
- 对象头
第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向 线程ID、偏向时间戳等
另外一部分是类型指针,即对象指向它的类元数据的指针, 虚拟机通过这个指针来确定这个对象是哪个类的实例实例数据
- 实例数据
是对象真正存储的有效信息,也是在程序代码中所定义的各种 类型的字段内容。 无论是从父类继承下来的,还是在子类中 定义的,都需要记录起来对齐填充
- 对齐补充
对象的访问定位
使用句柄
直接指针(Sun HotSpot)
垃圾收集和内存分配
引用计数法
- 思想:给对象设置引用计数器,每引用该对象一次,计数器就+1,引用失效时,计数器就-1,当任意时候引用计数器的值都为0时,则该对象可被回收
- Java不适用原因:无法解决对象互相循环引用的问题
可达性分析法
以GC Roots为起点,从这些起点开始向下搜索,经过的路径称为引用链。若一个对象到GC Roots之间没有任何引用链,则该对象是不可达的。
可作为GC Roots的对象有
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
在可达性分析过程中,对象引用类型会对对象的生命周期产生影响
JAVA中有这几种类型的引用:
- 强引用:只要该引用还有效,GC就不会回收
- 软引用:内存空间足够时不进行回收,在内存溢出发生前进行回收、用SoftReference类实现
- 弱引用:弱引用关联的对象只能存活到下一次Gc收集、用WeakReference类实现
- 虚引用:无法通过虚引用获得对象实例,也不会对对象的生存时间产生影响、唯一目的:当该对象被Gc收集时,收到一个系统通知。用PhantomReference类实现
一个对象真正不可用,要经历两次标记过程:
- 首先进行可达性分析,筛选出与GC Roots没用引用链的对象,进行第一次标记和筛选,筛选条件是是否有必要执行finalize()方法。若对象有没有重写finalize()方法,或者finalize()是否已被jvm调用过,则没必要执行,GC会回收该对象,若有必要执行,则该对象会被放入F-Queue中,由jvm开启一个低优先级的线程去执行它(但不一定等待finalize执行完毕)
- 第一次标记后,GC将对F-Queue中的对象进行第二次标记,Finalize()是对象最后一次自救的机会,若对象在finalize()中重新加入到引用链中,则它会被移出要回收的对象的集合,其他对象则会被第二次标记,进行回收
JAVA中的垃圾回收算法有:
标记-清除(Mark-Sweep)
两个阶段:标记, 清除
缺点:两个阶段的效率都不高;容易产生大量的内存碎片
复制(Copying)
把内存分成大小相同的两块,当一块的内存用完了,就把可用对象复制到另一块上,将使用过的一块一次性清理掉
缺点:浪费了一半内存
标记-整理(Mark-Compact)
标记后,让所有存活的对象移到一端,然后直接清理掉端边界以外的内存
分代收集
把堆分为新生代和老年代
新生代使用复制算法
将新生代内存分为一块大的Eden区和两块小的Survivor;每次使用Eden和一个Survivor,回收时将Eden和Survivor存活的对象复制到另一个Survivor(HotSpot的比例Eden:Survivor = 8:1)
老年代使用标记-清理或者标记-整理
HotSpot的算法实现
枚举根节点
OopMap数据结构记录哪些位置是引用
安全点
哪些位置可以生产OopMap
以是否具有让程序长时间执行的特征为标准进行选定
抢先式中断(不采用)和主动式中断
安全区域
一段代码片段中,引用不会发生变化
垃圾收集器:
Serial(串行收集器)
特性:单线程,stop the world,采用复制算法,简单高效
应用场景:在Client模式下默认的新生代收集器
ParNew
特点:是Serial的多线程版本,采用复制算法
应用场景:在Server模式下常用的新生代收集器,可与CMS配合工作
Parallel Scavenge
特点:并行的多线程收集器,采用复制算法,吞吐量优先,有自适应调节策略
应用场景:需要吞吐量大的时候
SerialOld
特点:Serial的老年代版本,单线程,使用标记-整理算法
Parallel Old
Parallel Scavenge的老年代版本,多线程,标记-整理算法
CMS
处理过程:
- 初始标记:stop the world 标记GC Roots能直接关联到的对象
- 并发标记:进行GC Roots Tracing
- 重新标记:stop the world;修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
- 并发清除:清除对象
特点:以最短回收停顿时间为目标,使用标记-清除算法
优点:并发收集,低停顿
缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾(并发清除时,用户线程仍在运行,此时产生的垃圾为浮动垃圾)
- 产生大量的空间碎片
G1
面向服务端应用,将整个堆划分为大小相同的region。
处理过程:
- 初始标记:stop the world 标记GC Roots能直接关联到的对象
- 并发标记:可达性分析
- 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的一部分标记记录
- 筛选回收:筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划
特点
- 并行与并发
- 分代收集
- 空间整合:从整体看是基于“标记-整理”的,从局部(两个region之间)看是基于“复制”的。
- 可预测的停顿:使用者可明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
内存分配规则
触发GC又涉及到了内存分配规则:(对象主要分配在Eden,若启动了本地线程分配缓冲,将优先在TLAB上分配)
- 对象优先在Eden分配
当Eden区没有足够的空间时就会发起一次Minor GC
- 大对象直接进入老年代
典型的大对象是很长的字符串和数组
- 长期存活的对象进入老年代
每个对象有年龄计数器,每经过一次GC,计数器值加一,当到达一定程度时(默认15),就会进入老年代,年龄的阈值可通过参数 -XX:MaxTenuringThreshold设置
对象年龄的判定
Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于等于该年龄的对象就可直接进入老年代,无须等到MaxTenuringThreshold要求的年龄
- 空间分配担保
发生Minor GC前,jvm会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,若大于,则Minor GC是安全的,若不大于,jvm会查看HandlePromotionFailure是否允许担保失败,若不允许,则改为一次Full GC,若允许担保失败,则检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,则尝试进行Minor GC;若小于,则要改为Full GC
最后提一下也会回收方法区:
永久代中主要回收两部分内容:废弃常量和无用的类
废弃常量回收和对象的回收类似
无用的类需满足3个条件
- 该类的所有实例对象已被回收
- 加载该类的ClassLoader已被回收
- 该类的Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
JVM常用命令
JDK命令行工具
Jps
查看虚拟机进程
Jstat
监视虚拟机各种运行状态信息
Jinfo
查看和调整虚拟机各项参数
Jmap
生产堆转储快照(一般称为heapdump或dump文件)
查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器
Jhat
分析堆转储快照
Jstack
生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)
举例:
高内存分析:
top 得到pid
jmap -histo:live pid
jmap -dump:live,format=b,file=xxx.xxx [pid]
高CPU分析:
top 得到pid
ps -mp pid -o THREAD,tid,ttime 得到线程id
printf "%x\n" 线程id 得到转换后线程id
jstack pid| grep 转换后线程id -A n
可视化工具
Jconsole
VisualVM
目前为止JDK发布的功能最强大的运行监视和故障处理工具
对应用程序的实际性能影响很小,可以直接应用在生产环境中
插件形式扩展
类文件结构
无关性
- 平台无关性
- 语言无关性
Class类文件结构
Class文件是一组以8位字节为基础单位的二进制流,采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构有两种数据类型:
- 无符号数
基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节
- 表
由多个无符号数或者其他表作为数据项构成的复合数据类型
具体构成:
魔数:前4个字节,0xCAFEBABE
次版本号:5~6字节
主版本号:7~8字节
常量池:
- 入口有一个u2类型的数据,代表常量池的容量
- 主要存放字面量和符号引用
- 字面量:文本字符串、申明为final的常量值等
- 符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
- 每一个常量都是一个表,开始的第一位是一个u1类型的标志位,代表属于哪种常量类型,共有14中结构各不相同的表结构数据
- Javap –verbose *.class
访问标志:常量池后的两个字节,用于识别一些类或者接口层次的访问信息,包括:这个 Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话, 是否被声明为final等
类索引:u2类型的数据
父类索引:u2类型的数据
接口索引集合:第一项为u2类型的数据,代表索引表的容量
字段表集合:
方法表集合:
属性表集合:
字节码指令
虚拟机操作码为一个字节,即最多操作码总数不可能超过256条
类加载机制
类加载的时机
什么时候对类进行初始化?
1、遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new 关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
2、使用java.lang. reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父 类的初始化。
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
5、当使用JDK 1. 7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_ getStatic、REF_ putStatic、REF_ invokeStatic的方法句柄,并且这个方法 句柄所对应的类没有进行过初始化,则需要先触发其初始化。
特殊:
1、通过子类引用父类的静态字段,不会导致子类初始化
2、通过数组定义来引用类,不会触发此类的初始化
3、 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
接口初始化和类的区别点:
当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。
类加载的过程
加载
1、通过一个类的全限定名来获取定义此类的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时运行时数据结构。
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
解析
是虚拟机将常量池内的符号引用替换为直接引用的过程
初始化
- 执行类构造器<clinit>()方法的过程
- <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
- 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
- <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
- 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,从而保证只执行一次
类加载器
- 两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。
- 双亲委派模型
启动类加载器:负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
扩展类加载器:负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库
应用程序类加载器:负责加载用户类路径(ClassPath)上所指定的类库
工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
程序编译与代码优化
编译期的优化
完成了从程序到抽象语法树或中间字节码的生成
编译器种类:
前端编译器 如:javac
JIT编译器 如:HotSpot VM的C1、C2
AOT编译器
javac
把*.java文件转变为*.class文件的过程,对代码运行效率几乎没有优化,相当多新生的Java语法特性都是靠编译器的语法糖来实现的,由java语言编写的程序
实现过程:
解析与填充符号表->注解处理器->语义分析与字节码生成
语法糖
- 泛型与类型擦除
- 自动装箱、拆箱和遍历循环
- 条件编译
其它:变长参数、内部类、枚举类、断言语句、对枚举和字符串(在JDK1.7中支持)的switch支持、try语句中定义和关闭资源(在JDK1.7中支持)等
注解处理器:
继承AbstractProcessor
类似处理:Hibernate Validator、Lombok
运行期的优化
Hotspot JVM:
解释器和编译器并存
有两个即时编译器:Cilent Compiler、Server Compiler
java -version mixed mode 混合模式;java -Xint -version 解释模式;java -Xcomp version 编译模式(已作废)
分层编译策略
第0层,程序解释执行,解释器不开启性能监控功能(Profiling),可触发第1层编译。
第1层,也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。
第2层(或2层以上),也称为C2编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
如何触发即时编译:
热点代码
被多次调用的方法、被多次执行的循环体
热点探测
基于采样的热点探测:简单、高效,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
基于计数器的热点探测:精确、严谨,但相对复杂,需要建立并维护计数器(采用)
Hotspot采用基于计数器的热点探测
方法调用计数器
这个计数器就用于统计方法被调用的次数,它的默认阈值在Client模式下是1500次,在Server模式下是10000次,这个阈值可以通过虚拟机参数-XX:CompileThreshold来人为设定;有热度衰减,可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减,可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。
回边计数器
统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(BackEdge)。
编译过程(执行各种优化)
方法内联
冗余访问消除(公共子表达式消除)
复写传播
无用代码消除
数组边界检查消除
逃逸分析:方法逃逸、线程逃逸
栈上分配、同步消除、标量替换
并发高效
Java内存模型与线程
内存模型
volatile变量
保证变量对所有线程的可见性
禁止指令重排序优化
线程内表现为串行的语义
内存屏障:指令重排序时不能把后面的指令重排序到内存屏障之前
不是线程安全的
long和double型变量的非原子协定,但商业虚拟机都保证了原子性
原子性、可见性、有序性
先行发生原则
Java与线程
线程的实现:使用内核线程实现(采用),使用用户线程实现,使用用户线程加轻量级进程混合实现
线程调度:协同式(简单但不稳定)、抢占式(采用)
线程状态转换:
线程安全与锁优化
线程安全
不可变、绝对线程安全、相对线程安全、线程兼容、线程对立
线程安全的实现方法
互斥同步
synchronized:可重入
j.u.c下:
Reentrantlock:可重入、等待可中断、可实现公平锁、可绑定多条件
Condition:
CountDownLatch:
非阻塞同步
CAS:指令需要有3个操作数,分别是内存位置(在Java中可以简单理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新,但是无论是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作。
锁优化
自旋锁与自适应自旋
默认开启自旋
-XX:PreBlockSpin自旋次数
锁消除
锁粗化
轻量级锁:是在无竞争的情况下使用CAS操作去消除同步使用的互斥量
偏向锁:是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了
转载于:https://my.oschina.net/jzgycq/blog/1921951