JVM内存结构
JVM内存分为线程私有区和线程共享区
线程私有区
1、程序计数器
(记录当前线程执⾏到哪⼀条字节码指令位置)
当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,不免发生线程切换。这时,每个线程就需要一个属于自己的计数器来记录下一条要运行的指令。如果执行的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空。
2、虚拟机栈
(线程执⾏⽅法的时候内部存局部变量会存堆中对象的地址等等数据)
线程私有的,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一个桢栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss参数可以设置虚拟机栈大小)。栈的大小可以是固定的,或者是动态扩展的。如果请求的栈深度大于最大可用深度,则抛出stackOverflowError;如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutofMemoryError。
下图为栈帧结构图:
3、本地方法栈
(存放各种native⽅法的局部变量表之类的信息)
与虚拟机栈作用相似。但它不是为Java方法服务的,而是本地方法(C语言)。由于规范对这块没有强制要求,不同虚拟机实现方法不同。
线程共享区
1、方法区
<存放类信息,常量池,共享>(java8移除了永久代(PermGen),替换为元空间(Metaspace)>
线程共享的,用于存放被虚拟机加载的类的元数据信息,如常量、静态变量和即时编译器编译后的代码。若要分代,算是永久代(老年代),以前类大多“static”的,很少被卸载或收集,现回收废弃常量和无用的类。其中运行时常量池存放编译生成的各种常量。(如果hotspot虚拟机确定一个类的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收)
2、堆
<对象,静态变量,共享>
存放对象实例和数组,是垃圾回收的主要区域,分为新生代和老年代。刚创建的对象在新生代的Eden区中,经过GC后进入新生代的S0区中,再经过GC进入新生代的S1区中,15次GC后仍存在就进入老年代。这是按照一种回收机制进行划分的,不是固定的。若堆的空间不够实例分配,则OutOfMemoryError。
类加载机制
加载顺序
(1)加载 :获取类的⼆进制字节流,将其静态存储结构转化为⽅法区的运⾏时数据结构,通过类的完全限定名,查找此类字节码文件,利用字节码文件创建Class对象.
(2)验证校验:确保Class文件符合当前虚拟机的要求,不会危害到虚拟机自身安全
(3)准备: 进行内存分配,在⽅法区中对类的static变量分配内存并设置类变量数据类型默认的初始值,不包括实例变量,实例变量将会在对象实例化的时候随着对象⼀起分配在Java堆中
(4)解析: 将常量池内的符号引⽤替换为直接引⽤的过程
(5)初始化: 为类的静态变量赋予正确的初始值(Java代码中被显式地赋予的值)
主要完成静态块执行以及静态变量的赋值.先初始化父类,再初始化当前类.只有对类主动使用时才会初始化.
触发条件包括,创建类的实例时,访问类的静态方法或静态变量的时候,使用Class.forName反射类的时候,或者某个子类初始化的时候.
Java自带的加载器加载的类,在虚拟机的生命周期中是不会被卸载的,只有用户自定义的加载器加载的类才可以被卸.
描述一下JVM加载class文件的原理机制?
JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。 由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。 类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明:
Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量
classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。
加载机制-双亲委派模式
双亲委派模式,即加载器加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器.父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载.*
JVM三种类加载器
(1):启动类加载器(home) 加载jvm核⼼类库,如java.lang.*等
(2):扩展类加载器(ext), ⽗加载器为启动类加载器,从jre/lib/ext下加载类库
(3):应⽤程序类加载器(⽤户classpath路径) ⽗加载器为扩展类加载器,从环境变量中加载类
双亲委派机制
(1):类加载器收到类加载的请求
(2):把这个请求委托给⽗加载器去完成,⼀直向上委托,直到启动类加载器
(3):启动器加载器检查能不能加载,能就加载(结束);否则,抛出异常,通知⼦加载器进⾏加载
(4):保障类的唯⼀性和安全性以及保证JDK核⼼类的优先加载
双亲委派模型有啥作⽤:
保证java基础类在不同的环境还是同⼀个Class对象,避免出现了⾃定义类覆盖基础类的情况,导致出现安全问题。还可以避免类的重复加载。
如何打破双亲委派模型?
(1):⾃定义类加载器,继承ClassLoader类重写loadClass⽅法;
(2):SPI
tomcat是如何打破双亲委派模型:
tomcat有着特殊性,它需要容纳多个应⽤,需要做到应⽤级别的隔离,⽽且需要减少重复性加载,所以划分为:/common 容器和应⽤共享的类信息,/server容器本身的类信息,/share应⽤通⽤的类信息,/WEB-INF/lib应⽤级别的类信息。整体可以分为:boostrapClassLoader->ExtensionClassLoader-
ApplicationClassLoader->CommonClassLoader->CatalinaClassLoader(容器本身的加载器)/ShareClassLoader(共享的)->WebAppClassLoader。虽然第⼀眼是满⾜双亲委派模型的,但是
不是的,因为双亲委派模型是要先提交给⽗类装载,⽽tomcat是优先判断是否是⾃⼰负责的⽂件位置,进⾏加载的。
SPI: (Service Provider interface)
(1):服务提供接⼝(服务发现机制):
(2):通过加载ClassPath下META_INF/services,⾃动加载⽂件⾥所定义的类
(3):通过ServiceLoader.load/Service.providers⽅法通过反射拿到实现类的实例
SPI应⽤?
(1):应⽤于JDBC获取数据库驱动连接过程就是应⽤这⼀机制
(2):apache最早提供的common-logging只有接⼝.没有实现…发现⽇志的提供商通过SPI来具体找到⽇志提供商实现类
双亲委派机制缺陷?
(1):双亲委派核⼼是越基础的类由越上层的加载器进⾏加载, 基础的类总是作为被调⽤代码调⽤的
API,⽆法实现基础类调⽤⽤户的代码….
(2):JNDI服务它的代码由启动类加载器去加载,但是他需要调独⽴⼚商实现的应⽤程序,如何解决?
线程上下⽂件类加载器(Thread Context ClassLoader), JNDI服务使⽤这个线程上下⽂类加载器去加载所需要的SPI代码,也就是⽗类加载器请求⼦类加载器去完成类加载动作Java中所有涉及SPI的加载动作基本上都采⽤这种⽅式,例如JNDI,JDBC
Java对象
对象的引用(强软弱虚)
●强引用
Object strongReference = new Object();
当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象!记住是存活着,不可能是你new一个对象就永远不会被GC回收。
当一个普通对象没有其他引用关系,只要超过了引用的作用域或者显示的将引用赋值为null时,你的对象就表明不是存活着,这样就会可以被GC回收了。当然回收的时间是不一定的具体得看GC回收策略。
●软引用
软引用的生命周期比强引用短一些。软引用是通过SoftReference类实现的
SoftReference softReference = new SoftReference(str);
软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用所引用对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。
当内存不足时,JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收,也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的“较新的”软对象会被虚拟机尽可能保留,这就是引入引用队列ReferenceQueue的原因
●弱引用
弱引用是通过WeakReference类实现的,它的生命周期比软引用还要短,也是通过get()方法获取对象,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
ThreadLocal中的key就用到了弱引用。
●虚引用
用于跟踪垃圾回收器的回收过程,用于管理堆外内存
Get不到一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。虚引用和弱引用对关联对象的回收都不会产生影响,如果只有虚引用活着弱引用关联着对象,那么这个对象就会被回收。它们的不同之处在于弱引用的get方法,虚引用的get方法始终返回null,弱引用可以使用ReferenceQueue,虚引用必须配合ReferenceQueue使用。
jdk中直接内存的回收就用到虚引用,由于jvm自动内存管理的范围是堆内存,而直接内存是在堆内存之外(其实是内存映射文件,自行去理解虚拟内存空间的相关概念),所以直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,相应的用户线程会收到通知并对直接内存进行清理工作
对象的构成
Java对象由三个部分组成:对象头、实例数据、对齐填充。
对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID(一般占32/64 bit)。第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)
对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)
对象的创建过程
1.JVM遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在后边讲)
2.为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,最终常用的办法“本地线程缓冲分配(TLAB)”
3.将除对象头外的对象内存空间初始化为0
4.对对象头进行必要设置
对象是否存活/被回收
判断对象是否存活一般有两种方式:
引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。
●根据GC ROOTS。GC ROOTS可以的对象有:虚拟机栈中的引⽤对象,⽅法区的类变量的引⽤,⽅法区中的常量引⽤,本地⽅法栈中的对象引⽤。
常见垃圾收集算法
●标记清除->位置不连续,产生碎片
●拷贝算法(复制)->没有碎片,浪费空间,效率较高
●标记压缩->没用碎片,效率偏低
JVM内存分代模型(用于分代垃圾回收算法)
在新生代 每次垃圾回收时 都有大批量的对象死去,只有少量存活那就采用–复制算法。而老年代因为存活率较高 采用标记清除 或者标记整理算法来分配空间
●新生代=Eden(伊甸园)+2个suvivor(幸存者)区
1.YGC回收之后,大部分对象会被回收,活着进入s0
2.再次YGC,活着的对象Eden+s0->s1
3.再次YGC,活着的对象Eden+s1->s0(循环)
4.年龄足够(一般阈值为15)->老年代
5.S区装不下(或者new对象超过suvivor内存50%)->老年代
●老年代:
1.玩顾分子
2.new对象超过suvivor内存50%
3.满了->FGC Full GC
对象分配规则
对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次
Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
导致fullGC的原因
(1):⽼年代空间不⾜
(2):永久代(⽅法区)空间不⾜
(3):显式调⽤system.gc()
JVM七种垃圾收集器
(1): Serial 收集器 复制算法,单线程,新⽣代)
(2): ParNew 收集器(复制算法,多线程,新⽣代)
(3): Parallel Scavenge 收集器(多线程,复制算法,新⽣代,⾼吞吐量)
(4):Serial Old 收集器(标记-整理算法,⽼年代)
(5):Parallel Old 收集器(标记-整理算法,⽼年代,注重吞吐量的场景下,jdk8默认采⽤ Parallel
Scavenge + Parallel Old 的组合)
(6):CMS 收集器(标记-清除算法,⽼年代,垃圾回收线程⼏乎能做到与⽤户线程同时⼯作,吞吐量低,内存碎⽚)以牺牲吞吐量为代价来获得最短回收停顿时间-XX:+UseConcMarkSweepGC jdk1.8
默认垃圾收集器Parallel Scavenge(新⽣代)+Parallel Old(⽼年代) jdk1.9 默认垃圾收集器G1
使⽤场景:
(1):应⽤程序对停顿⽐较敏感
(2):在JVM中,有相对较多存活时间较⻓的对象(⽼年代⽐较⼤)会更适合使⽤CMS
CMS
CMS垃圾回收过程:
初始化标记(CMS-initial-mark) ,标记root,会导致stw;
并发标记(CMS-concurrent-mark),与用户线程同时运行;
预清理(CMS-concurrent-preclean),与用户线程同时运行;
重新标记(CMS-remark) ,会导致stw;
并发清除(CMS-concurrent-sweep),与用户线程同时运行;
调整堆大小,设置CMS在清理之后进行内存压缩,目的是清理内存中的碎片;
并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;
STW问题?
Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。
并发漏标问题
CMS解决方案:三色标记+incremental update+Remark
并发标记(三⾊标记算法) 三⾊标记算法处理并发标记出现对象引⽤变化情况:
⿊:⾃⼰+⼦对象标记完成
灰:⾃⼰完成,⼦对象未完成
⽩:未标记;
并发标记 ⿊->灰->⽩ 重新标记 灰->⽩引⽤消失,⿊引⽤指向->⽩,导致⽩漏标 cms处理办法是incremental update⽅案 (增量更新)把⿊⾊变成灰⾊,但多线程下并发标记依旧会产⽣漏标问题,所以cms必须remark⼀遍(jdk1.9以后不⽤cms了)
G1方案:三色标记+SATB
ZGC方案:Colored pounters(颜色指针 着色指针)
G1回收器
回收过程 (1):young gc(年轻代回收)–当年轻代的Eden区⽤尽时–stw 第⼀阶段,扫描根。 根是指static变量指向的对象,正在执⾏的⽅法调⽤链条上的局部变量等 第⼆阶段,更新RS(RememberedSets)。 处理dirty card queue中的card,更新RS。此阶段完成后,RS可以准确的反映⽼年代对所在的内存分段中对象的引⽤ 第三阶段,处理RS。 识别被⽼年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。 第四阶段,复制对象。 此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段 第五阶段,处理引⽤。 处理Soft,Weak,Phantom,Final,JNI Weak 等引⽤。
(2):concrruent marking(⽼年代并发标记) 当堆内存使⽤达到⼀定值(默认45%)时,不需要Stop-The-World,在并发标记前先进⾏⼀次young gc
(3):混合回收(mixed gc) 并发标记过程结束以后,紧跟着就会开始混合回收过程。混合回收的意思是年轻代和⽼年代会同时被回收
(4):Full GC? Full GC是指上述⽅式不能正常⼯作,G1会停⽌应⽤程序的执⾏,使⽤单线程的内存回收算法进⾏垃圾回收,性能会⾮常差,应⽤程序停顿时间会很⻓。要避免Full GC的发⽣,⼀旦发⽣需要进⾏调整。
什么时候发⽣Full GC呢?
⽐如堆内存太⼩,当G1在复制存活对象的时候没有空的内存分段可⽤,则会回退到full gc,这种情况可以通过增⼤内存解决
尽管G1堆内存仍然是分代的,但是同⼀个代的内存不再采⽤连续的内存结构
年轻代分为Eden和Survivor两个区,⽼年代分为Old和Humongous两个区新分配的对象会被分配到Eden区的内存分段上
Humongous区⽤于保存⼤对象,如果⼀个对象占⽤的空间超过内存分段Region的⼀半;
如果对象的⼤⼩超过⼀个甚⾄⼏个分段的⼤⼩,则对象会分配在物理连续的多个Humongous分段上。
Humongous对象因为占⽤内存较⼤并且连续会被优先回收
为了在回收单个内存分段的时候不必对整个堆内存的对象进⾏扫描(单个内存分段中的对象可能被其他内存分段中的对象引⽤)引⼊了RS数据结构。RS使得G1可以在年轻代回收的时候不必去扫描⽼年代的对象,从⽽提⾼了性能。每⼀个内存分段都对应⼀个RS,RS保存了来⾃其他分段内的对象对于此分段的引⽤
JVM会对应⽤程序的每⼀个引⽤赋值语句object.field=object进⾏记录和处理,把引⽤关系更新到RS中。但是这个RS的更新并不是实时的。G1维护了⼀个Dirty Card Queue
那为什么不在引⽤赋值语句处直接更新RS呢?
这是为了性能的需要,使⽤队列性能会好很多。
线程本地分配缓冲区(TLAB: Thread Local Allocation Buffer)?
栈上分配->tlab->堆上分配 由于堆内存是应⽤程序共享的,应⽤程序的多个线程在分配内存的时候需要加锁以进⾏同步。为了避免加锁,提⾼性能每⼀个应⽤程序的线程会被分配⼀个TLAB。TLAB中的内存来⾃于G1年轻代中的内存分段。当对象不是Humongous对象,TLAB也能装的下的时候,对象会被优先分配于创建此对象的线程的TLAB中。这样分配会很快,因为TLAB⾪属于线程,所以不需要加锁
PLAB: Promotion Thread Local Allocation Buffer
G1会在年轻代回收过程中把Eden区中的对象复制(“提升”)到Survivor区中,Survivor区中的对象复制到Old区中。G1的回收过程是多线程执⾏的,为了避免多个线程往同⼀个内存分段进⾏复制,那么复制的过程也需要加锁。为了避免加锁,G1的每个线程都关联了⼀个PLAB,这样就不需要进⾏加锁了
OOM问题定位⽅法
(1):jmap -heap 10765如上图,可以查看新⽣代,⽼⽣代堆内存的分配⼤⼩以及使⽤情况;
(2):jstat 查看GC收集情况
(3):jmap -dump:live,format=b,file=到本地
(4):通过MAT⼯具打开分析