jvm
内存结构:
image.png
方法区:
用于储存已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,是线程共享的
异常:当方法区无法满足内存分配需求(-XX:MaxPermSize)时,将会抛出OutOfMemoryError:PermGen space;异常.
JDK1.8 移除了方法区,增加元空间,元空间内存只受本地内存限制,元空间可以在运行时动态调整,可以使用-XX:MaxMatespaceSize 设置本地内存分配给元空间的最大内存,元空间与堆不相连,但是与堆共享物理内存
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:-UseCompressedClassPointers -XX:MetaspaceSize=20M -XX:MaxMetaspaceSize=20m
第一个参数用于打印GC日志;
第二个参数用于打印对应的时间戳;
第三个参数-XX:-UseCompressedClassPointer表示在Metaspace中不要开辟出一块新的空间(Compressed Class Space),如果开辟这块空间的话,该空间默认大小是1G,所以我们关闭该功能,此时再设置Metaspace的大小;
image.png
堆内存:
堆是JVM管理的最大的一块内存,是线程共享的,用于存放对象实例以及数据.堆也是垃圾回收器主要管理的区域,也就GC堆.
虚拟机启动时就会创建堆,可通过-Xmx、-Xms调节堆大小
image.png
异常:如果在堆中没有内存完成实例分配,并且堆也无法进行再扩展(-Xmx,-Xms)了,将会抛出java.lang.OutOfMemoryError: Java heap space异常
1.指针碰撞法
假设Java堆中内存时完整的,已分配的内存和空闲内存分别在不同的一侧,通过一个指针作为分界点,需要分配内存时,仅仅需要把指针往空闲的一端移动与对象大小相等的距离。使用的GC收集器:Serial、ParNew,适用堆内存规整(即没有内存碎片)的情况下。
2.空闲列表法
事实上,Java堆的内存并不是完整的,已分配的内存和空闲内存相互交错,JVM通过维护一个列表,记录可用的内存块信息,当分配操作发生时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录。使用的GC收集器:CMS,适用堆内存不规整的情况下。
内存分配并发问题
在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:
CAS: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
TLAB: 为每一个线程预先分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。
内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头。
执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成;
对象的内存布局
在 HotSpot 虚拟机中,分为 3 块区域:对象头(Header)、实例数据(Instance Data) 和 对齐填充(Padding)
对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 ‘Mark Word’;
第二部分是类型指针,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例,另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以
image.png
实例数据(Instance Data):程序代码中所定义的各种成员变量类型的字段内容(包含父类继承下来的和子类中定义的);
对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍;
对象的访问
使用对象时,我们是通过栈上的 reference 引用来操作堆上的具体对象;
Sun Hotspot虚拟机使用直接指针访问具体对象;
image.png
java 虚拟机栈:
是java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态链接,方法出口灯信息.每一个方法从调用直至执行完成的过程,就是对应着这个栈帧在虚拟机栈中的入栈和出栈的过程,该区域是线程私有的,她的生命周期 跟线程的生命周期相同.
image.png
局部变量表:包含方法中的参数和方法内定义的局部变量,如果的局部变量是一个对象的引用, 那么这个引用指向堆中的对象
操作数栈:也叫做操作站,他是一个先进后出的栈,当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行,会从局部变量表或者实例对象的字段中复制常量或者变量写入操作数栈,再随着计算的进行将栈中元素出栈道局部变量表或返回方法调用者,也就是入展和出栈操作,一个完整的方法执行期间往往包含多个这样的入展/出栈的过程
动态链接:一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池,所以需要在运行时动态将这些符号引用转化为直接引用.
返回地址:方法不管是正常执行结束还是异常退出,需要放回方法被调用的位置.
本地方法栈:与虚拟机栈类似,只不过本地方法栈是为虚拟机使用到的native方法服务的
程序计数器:是一块较小的内存空间,它可以看做当前线程所执行的字节码的行号指示器.字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖于这个计数器.
程序计数器是线程私有的,每个线程依赖于每个线程中的程序计数器来确保线程切换后能恢复到正确的执行位置.
如果,在执行的是Java方法,则这个计数器记录的是正在执行的虚拟机字节码指令的地址.如果是本地native方法,则该计数器记录的为空(Undefined).
垃圾回收
垃圾回收机制:
不定时的去堆内存中清理不可达对象.垃圾回收器执行是自动的,程序员只能通过System.gc去建议垃圾回收器进行垃圾回收,但是是否执行,什么时候执行都是不可控的.
finalize方法:Java中使用finalize()方法在垃圾回收器将对象从内存中清除出去前,做必要的清理工作.这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在Object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
GC线程是守护线程.
Java堆内存可化为分新生代,老年代. 新生代,老年代内存比例默认1:2(该值可以通过参数 -XX:NewRatio来指定),而新生代可细分为Eden区,from Survivor, to Survivor区,比例8:1:1(可通过-XX:SurvivorRatio来指定)
新生代(Young):刚出生不久的对象,存放在新生代里,存放不是经常使用的对象.
老年代(Old):存放比较活跃的对象,或者说是存在较久的年老的对象.
判断对象是否已死:
1.引用计数法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的.
引用计数法弊端:很难解决对象之间的互相循环引用的问题,导致,本已经没有再引用的对象,因为相互引用着而导致计数器不为0,而无法回收,所以,现在Java虚拟机不采用引用计数法.
2.根搜索算法(可达性分析算法):通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的.如下图所示:
图片.png
可作为GC Roots对象包括下面几种:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象.
2)方法区中类静态属性引用的对象
3)方法区中常量引用的对象
4)本地方法栈中JNI(native 方法)引用的对象.
不可达对象被回收的过程(两次标记):
当一个对象被垃圾收集器认为不可达时,此时这个对象会被第一次标记,并进行一次筛选,筛选的条件就是该对象有没有必要执行finalize()方法.当对象没有覆盖finalize()方法,或着已经被虚拟机执行过,则认为没有必要执行(一个对象的finalize方法只会被调用一次).
虚拟机会将这些有必要执行的对象放置到一个叫做F-Queue的队列中,并在稍后由虚拟机自动建立的,低优先级的Finalizer线程中取执行它的finalize()方法,但是虚拟机只去触发调用,不保证会等待finalize()执行结束.
执行finalize()是对象逃离死亡的最后的机会,只要在finalize()方法中重新与引用链连接上即可.重新建立上连接的对象,会在回收器在队列里做第二次标记时,将之移除队列,使其逃出升天.而,没有重新建立上连接的对象在第二次被标记后,就真的被回收了.
垃圾收集算法:
image.png
1.标记-清除算法:
算法分为"标记"和"清除"两个阶段,首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象.
缺陷:1.效率问题,标记 和 清除 两个过程效率都不高.
2.空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作.
2.复制算法:
复制算法将内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清除掉.
因为IBM公司的研究表明,98%的对象都是"朝生夕死",所以,一般不会按1:1去划分空间,而是分为一个Eden区和两个survivor区,每次都使用一个Eden区和一个survivor区的空间.当执行回收时,将Eden和from survivor区的还存活的对象一次性的复制到to survivor区中,然后清理Eden和from survivor区.HotSpot 默认Eden:survivor=8:1.
但是,当survivor区内存不够用时,需要依赖老年代进行分配担保(Handle Promotion),大概意思是,在垃圾回收时,如果to survivor的空间不足以存放Eden和 from survivor空间存活的对象时,这些对象将直接通过分配器担保机制进入老年代(一般对象在from , to之间来回15此才会被移进老年代,15次这个是由JVM参数MaxTenuringThreshold决定的,默认是15).
优点:在存活对象不多的时候,效率比较高,并解决碎片化问题.
缺点:会造成一部分内存的浪费,并且如果存活的对象比较大,复制的效率会比较低.
3.标记-整理算法(标记-压缩算法):
标记-整理算法 与标记-清除算法基本一样,只是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后再直接清理掉端边意外的内存.
优点:解决了内存碎片化的问题.
缺点:压缩阶段,由于移动了可用对象,则需要更新对象的引用.
4.分代收集算法
分代收集算法将堆空间分为新生代,老年代.根据各个年代的特点采用最适用的手机算法.新生代中,每次垃圾收集(young GC)时都会有大批对象死去,只有少量存活,一般采用复制算法.老年代(Full GC)中因为对象对象存活率高,没有额外的空间进行分配担保,就必须使用"标记-清除"或者"标记-整理"算法.
young GC(Minor GC):
当Eden空间满的时候会触发,survivor满的时候不会触发.一般young GC 较为频繁,回收速度也比较快.
Full GC(Major GC):
当老年代满了的时候会引发Full GC,Full GC 将会同时回收年轻代和老年代,即一般Full GC 会伴随至少一次的young GC.当永久代(方法区)满的时候也会引发Full GC,会导致Class,Method 元信息的卸载(Java 8中,移除了永久代,新加了一个元数据区的native内存区).Full GC速度比较慢,一般会比young GC慢10倍以上.
JVM 参数配置:
-Xmx20M -Xms20M -Xmn1M -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
1>在实际工作中,我们可以直接将初始的堆大小与最大堆大小相等,这样的好处是可以减少程序运行时垃圾回收次数,从而提高效率
图片.png
Java 堆溢出:
Java堆用于存储对象实例,只要不断创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾收集器清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出(Java.lang.OutOfMemoryError: Java heap space).
要解决这个问题,一般需要通过内存映像分析工具(idea JVM Debugger Memory View)对Dump出来的堆转存储快照进行分析,确认内存中的对象是否是必要的.
对象是必要的则是内存溢出,对象不必要的话就是内存泄露.
如果是内存泄露,进一步查看泄露对象到GC Roots的引用链,找到泄露对象为什么与GC Roots相关联,定位问题,解决问题.
如果是内存溢出,查看代码是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期间的内存消耗.另外,确认-Xms,-Xmx参数,与物理内存相比较,确认是否可以对堆内存进行增加.
垃圾收集器:
image.png
1>serial收集器:
serial收集器是最基本发展历史最久的收集器,也是JDK1.3之前,唯一的收集器.serial收集器是一个单线程的收集器,它只会使用一个CPU或一条收集线程去完成垃圾收集工作,并且更重要的是它在进行垃圾收集时,必须暂停其他所有的线程工作,直到它收集结束.
它是虚拟机运行在Client模式下的默认新生代收集器.
优点:与其他收集器相比,serial收集器简单高效,对于限定单个CPU环境,serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率.
image.png
2>ParNew 收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了在收集垃圾时,采用多线程之外,与Serial基本一样.
但是ParNew是运行在server模式下的虚拟机中首选的新生代收集器.因为在单CPU下效率随没有serial高,但是在多CPU环境下效率显著,一般它默然开启的线程数与CPU数量相同.另外一个原因就是,目前只有它可以与CMS(老年代)收集器配合工作.
3>Parallel Scavenge收集器
Parallel Scavenge收集器与ParNew收集器类似,不同的是Parallel Scavenge收集器提供了两个参数
-XX:MaxGCPauseMillis :该参数允许设定一个大于0的毫秒数,表示收集器尽量在垃圾收集时不超过这个时间,但是这个是以牺牲吞吐量和新生代空间内存为代价的.设置的时间越短,那收集的就越频繁.
-XX:GCTimeRatio :参数允许设置一个大于0且小于100的整数.也就是垃圾收集时间占总时间的比率,默认值是99,即允许最大的垃圾回收时间占比为1/100%
由于它与吞吐量关系密切,所以也称作 吞吐量优先收集器.
4>CMS收集器 -XX:+UseConcMarkSweepGC
CMS收集器是一种以获取最短回收停顿时间为目标的收集器.应用于互联网或者B/S系统的服务端上,采用 标记-清除 算法,收集老年代空间的垃圾.
CMS收集器收集分为四个步骤:
1>初始标记,标记GC Roots能够直接关联到的对象,速度很快.
2>并发标记,根据GC Roots 进行tracing追踪
3>重新标记,修正在并发标记过程中,因用户程序继续运作而导致的标记变动的那一部分对象的标记记录.
4>并发清除,将所有标记的对象清除.
CMS收集器的四个过程中,只有初始标记和重新标记的时候需要 stop the world,而耗时最久的并发标记和并发清除,均是并发进行,所以效率极高.
缺点:
1>CMS收集器对CPU资源非常敏感.因为面向并发设计,所以对CPU资源比较敏感,它虽不会让用户应用程序停止,但是它会占用一部分线程资源,而导致用户应用程序变慢.默认收集线程为(CPU数+3)/4.
2>CMS收集器无法收集浮动资源(由于CMS并发清理过程用户线程还在运行着,所以还有新的垃圾产生,这一部分垃圾出现在标记过程之后,CMS无法再档次收集中处理他们,只好留待下一次,这样的垃圾叫做浮动垃圾),可能导致出现Concurrent Mode Failure 失败而导致一次serial Old 收集器的 Full GC.
因为在垃圾清除的过程中,用户程序还在进行,那么就需要保留一定的内存来供用户使用,可以通过-XX:CMSInitiatingOccupancyFraction 来设置,JDK1.5中默认当老年代使用了68%时就会触发CMS收集器回收垃圾.而JDK1.6中默认为92%.
如果在CMS运行期间,预留的内存无法满足用户程序的需求时,就会出现 Concurrent Mode Failure失败,这时会启动备案,临时启动Serial Old收集器重新进行老年代的垃圾收集.这样停顿时间就会很长.
3>因为采用的是标记清除法,所以会有碎片化的问题
5>G1收集器
G1收集器是当今收集器最前沿的技术,G1是一款面向服务端应用的垃圾收集器.
G1对内存的划分与传统的不同,G1把内存划分为多个大小相同的Region(默认512K),Region逻辑上连续,物理内存地址不连续.同时每个Region被标记成E、S、O、H,分别表示Eden、Survivor、Old、Humongous。其中E、S属于年轻代,O与H属于老年代。如下图
H表示Humongous。从字面上就可以理解表示大的对象(下面简称H对象)。当分配的对象大于等于Region大小的一半的时候就会被认为是巨型对象。H对象默认分配在老年代,可以防止GC的时候大对象的内存拷贝
图片.png
G1收集器过程如下:
1>初始标记:标记GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一个阶段用户程序并发运行时,能在正确的可用的region中创建对象,这阶段需要停顿线程,但耗时很短.
2>并发标记:是从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时时间较长,但可用与用户程序并发进行.
3>最终标记:是为了修正在并发标记期间因用户程序继续进行而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs中,最终标记阶段需要把 Remembered Set Logs中数据合并到 Remembered Set中,这段时间需要停顿线程,但是可并行进行.
4>筛选回收:最后在筛选回收阶段首先对各个region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段是可以和用户线程一起并发执行的,但是因为一次只回收一部分region,时间是可以根据用户控制的,而且停顿用户线程将大幅度提高收集效率.
G1收集器特点:
1>G1可以回收年轻代和老年大的空间,年轻代采用复制算法,老年代采用标记整理算法,解决碎片化问题
2>可预测停顿,能够让用户设定在M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒.