概览:
1:源码到类文件(.java文件--->.class文件)
Xx.java--->词法分析器---->tokens流----->语法分析器----->语法树/抽象语法树----->语义分析----->注解抽象语法树------>字节码生成器----->Xx.class文件
2:类文件到虚拟机(.class文件加载到JVM)
a:装载
1)通过类的全限定名获取此类的二进制流
2)将这个字节流解析成方法区的运行时数据结构
3)在java堆中生成一个代表这个类的java.lang.class对象,作为对方法区中这些数据的访问入口
b:链接
1)验证,验证文件格式,验证元数据,验证字节码,验证符号引用
2)准备,为类的静态变量分配内存空间,并将其初始化为默认值
3)解析,将类中的符号引用转换成直接引用
c:初始化
1)对类的静态变量,静态代码块执行初始化操作
类装载器(ClassLoader)
分类:
加载原则:
检查某个类是否加载:自底向上,从Custom ClassLoader逐层检查,只要有某个ClassLoader加载,就视为已加载此类,保证所有classLoader只加载一次
加载顺序:自顶向下,由上层来逐层尝试加载此类
双亲委派机制:
如果有一个类加载器在接收到加载类的请求时候,它首先将请求委托给父类加载器去完成,依次递归,如果父类加载器可以完成加载,就成功返回,只有父类无法完成加载时候自己才去加载
优势:java类随着类加载器一起具备了带有优先级的层级关系,保证了只有一个类
破坏:继承ClassLoader类,然后重写loadClass()方法
运行时数据区:
方法区(非堆)[JDK8中Metaspace JDK6,7中Perm Space]:
各个线程共享的内存区域,在虚拟机启动时候创建,用于存放已被虚拟机加载的类信息,当方法区无法满足内存需求时候抛出OOM错误,还包含常量池(用于存放编译时期生成的各种
字面量和符号引用)
此时也就是JVM加载class文件的状态第一步(转换为二进制流)和第二步(转换成方法区的运行时数据结构)
方法区中指向堆(在类中创建对象):
堆(Heap):
java虚拟机所管理内存中最大的一块,在虚拟机启动时候创建,被所有线程共享,对象实例和数组都在堆上分配
对应装载阶段的第三步(在堆中生成代表这个类的java.lang.class对象信息,作为访问方法区中这些方法的入口)
Java对象的内存布局:分为对象头,实例数据,对齐填充
堆指向方法区
虚拟机栈(Stack):
是一个线程执行的区域,线程私有,保存着一个线程方法中的调用状态,每个被线程执行的方法都会创建一个栈帧(一个方法的运行空间),调用一个方法就会向栈中压入一个栈帧,一个方法完成就 从栈中弹出栈帧
栈帧包括:局部变量表,操作数栈,指向运行时常量池的引用,方法返回地址和附加信息
局部变量表:方法中定义的局部变量和方法的参数,不可直接使用,需要通过相关指令加载到操作数栈中使用
操作数栈:以压栈和出栈的方式存储操作数
动态连接:每个栈帧中包含一个指向运行时常量池中该栈帧所属方法的引用
方法返回地址:方法开始后只有两种方式退出,一种遇到方法返回的字节码指令,一种是遇到异常,并且这个异常没有在方法体内处理
栈中引用指向堆(方法中创建对象):
程序计数器(The PC Register):
由于java虚拟机的多线程是通过线程切换,并分配处理器执行时间的方式来实现,任意时刻一个处理器只会执行一条线程中的指令,因此为了切换后能恢复到正确位置,每个线程都有一个
程序计数器(线程私有),如果执行的是Native方法,则该值为空
本地方法栈(Native Method Stacks)
如果当前线程执行的方法是Native的,则会在本地方法栈中执行
内存模型:
1)堆区
3):Old区
4):Young区
5):Survivor区
6):Eden区,Eden:s0:s1=8:1:1
2)非堆区
一般情况下,新创建的对象都会被分配到Eden区,一些特殊的大对象才会被直接分配到Old区,当Eden区内存使用量达到一个设定的值,这时候就需要对Eden区域空间进行清理(GC)
在Young区的GC称为Minor GC,Old区的GC称为Major GC,两者同时发生称为Full GC(Full GC=Minor GC+Major GC)
survivor区域:
分为S0和S1区域(From区域和To区域),在同一个时间点上,S0和S1只有一个区域内有数据,每次GC,S0中对象年龄+1,存活对象复制到S1区域中,在S1区域中,如果对象
年龄达到设置的阈值,则分配到Old区域,未达到年龄时候分配到S0区域
Old区域:
Old区域一般是年龄比较大,或者相对超过某个阈值的对象
1:为什么需要Survivor区,只保留Eden区会怎么样?
如果没有Survivor区,新对象到了Eden区域后,在Eden进行Minor GC时候存活对象达到阈值时候会放入Old区域,这样会导致Old区域很快被填满,当Old区域满了时候触发Major GC,这样会导致频繁的
Full GC,这样会影响程序的执行和响应速度,如果增大Old区域的空间,虽然降低了Full GC频率,但一旦发生Full GC执行所需时间更长,所以Survivor存在就是为了减少被送到Old区的对象,减少Full GC
2:为什么需要两个Survivor区域
解决了碎片化,如果只有一个Survivor,当Eden将对象存放到Survivor,当下次GC时候,Survivor中回收了部分对象,Eden再次存放对象进来时候使用的空间就是不连续
GC算法(垃圾回收算法)
1:引用计数
对于某个对象,只要程序中持有该对象的引用,则说明该对象不是垃圾
缺点:无法解决循环引用
2:可达性分析
通过GC Root开始向下寻找,看某个对象是否科大
GC Root包括:栈,常量引用,static成员,Thread等
垃圾回收算法:
1:标记清除(Mark--Sweep)
找到内存中需要回收的对象,并且将他们标记出来,然后删除掉被标记的对象
缺点:产生大量不连续的内存碎片,空间碎片太多可能会导致在下次分配较大对象时候,无法找到足够的连续内存而不得不触发GC
2:复制算法(Copying)
将内存划分成两块内存区域,一次使用其中一块
缺点:空间利用率低
3:标记-整理(Mark-Compact)
找到内存中需要回收对象,进行标记,然后让所有存活对象向一端移动,然后清理掉边界外的对象
4:分代收集算法
Young区域,复制算法(对象在被分配后,生命周期较短,使用复制效率高)
Old区域,标记清除或标记整理(old区域对象存活时间长)
垃圾收集器:
新生代:
Serial:最基本,发展历史最长最久的收集器,单线程,只会使用一个CPU或者一个线程去工作,进行GC时候需要暂停其他线程,使用复制算法,适合新生代
优点:简单高效,很高的单线程收集效率
缺点:需要暂停所有线程
ParNew:Serial的多线程版本,使用复制算法,适合新生代
优点:多CPU时候,比Serial高效
缺点:暂停所有线程,单CPU比Serial效率差
Parallel Scavenge:和ParNew类似,但是更关注系统吞吐量,吞吐量=运行用户代码的时间/(运行用户代码时间+垃圾收集时间)
老年代:
Serial Old:单线程,采用标记--整理算法
Parallel Old:多线程,使用标记--整理算法
CMS:以获取最短回收停顿时间为目标的收集器,采用标记--清除算法,分为四步
1:初始标记--->标记GC Root能关联到的对象,速度很快,STW(Stop The World)
2:并发标记--->进行GC Roots Tracing
3:重新标记---->修改并发标记因用户程序变动的内容 STW
4:并发清除---->
优点:并发收集,低停顿
缺点:产生大量空间碎片,并发阶段会降低吞吐量
G1收集器:并行和并发,分代收集,空间整合(标记--整理算法,不会导致空间碎片),可预测的停顿(比CMS更先进地方在于能让使用者明确一个长度为M毫秒的时间片段内,消耗在垃圾收集 上的时间不超过N毫秒)
Java堆的内存布局和其他不同,将整个Java堆划分成大小相等的Region,新生代和老年代不再是物理隔离
分为下面几步:
1:初始标记--->GC Roots能关联到的对象 STW
2:并发标记---->GC Roots可达性分析,找出存活对象
3:最终标记---->修正并发标记阶段用户程序的并发执行导致变动的数据,STW
4:筛选回收----->对各个Region的回收价值和成本排序,根据用户所期望的GC停顿时间制定回收计划
常用命令:
jps------>查看java进程
jinfo------>实时查看和调整JVM配置参数
jinfo -flag MaxHeapSize 42368
jinfo -flag useG1GC 42368
jinfo -flag [+|-] 42368
jstate----->查看虚拟机性能统计信息
查看类状态信息
查看GC信息
jstack------>查看堆栈信息
jmap------->生成堆转储快照
打印堆内存相关信息
常用工具:jconsole(JDK自带工具),jvisualvm,Arthas,Mat,GC日志分析工具(http://gceasy.io GCViewer)
JVM性能优化:
发现问题:GC频繁,死锁,OOM,线程池不够用,CPU负载过高
排查问题:打印出GC日志,查看minor gc、major gc结合工具
jstack查看线程堆栈信息
dump出堆文件,使用工具分析
使用jvisualvm或者arthas实时查看JVM状态
使用jps,jinfo,jmap命令
解决方案:适当增大内存大小,选择合适的垃圾收集器
使用zk,redis实现分布式锁
设置本地,nginx缓存,减少服务端的访问
后台代码优化
内存泄漏:对象无法得到及时回收,持续占用内存空间,从而造成内存空间浪费
内存溢出:可能是大对象导致
直接内存:java堆外的,直接向系统申请的空间,访问速度优于java堆
不可达对象一定被回收?
在可达性分析中不可达的对象并非一定被回收,只是暂时处于被回收状态,真正被回收需要经历两次标记过程,可达性分析中不可达对象被第一次标记并且进行一次筛选,然后被判断要
执行的对象会被放在一个队列中进行二次标记,除非这个对象与引用链上任何一个对象关联,否则会被真正回收
回收无用的类:
满足下列三个条件才能算是无用的类
1:所有该类的实例被回收
2:加载该类的ClassLoader被回收
3:该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类方法