JVM学习笔记
一、JVM堆、栈及方法区
1、概念
1.1 栈区
(1)在java中,每个线程都会包含一个栈区,且在栈中它只会保存方法的基础数据类型及自定义对象的引用,栈的内存管理不会存在内存回收问题,但堆会,因为堆是随机分配内容的。
(2)Java中的JVM是基于堆栈的虚拟机,在创建每个新线程时都会分配一个堆栈.就是说,对Java程序而言,它的运行就只是通过一些堆栈操作来完成的。
(3)每当线程执行一个方法时,它就会跟着创建一个对应栈帧,并把建立的栈帧压栈。方法执行完毕后,再把栈帧出栈。
(4)由此我们可知,线程与当前所执行的方法对应的栈帧是必定位于Java栈顶部的。因此对于所有的程序设计语言而言,栈这部分空间对开发者来说是不透明的。
1.2 堆区
在java中,堆数据区是用来存放对象和数组的,每个对象都包含一个与之对应的class的信息,堆内存是被多个线程共享的。在JVM启动后堆内存会随之创建。堆中只会存放对象本身,不会存放基本类型和对象引用,几乎所有的对象实例和数组都在堆中分配。
1.3 方法区
(1)我们一般也叫静态区,它与堆一样是被所有的线程共享的。方法区用于存储已经被虚拟机加载的常量、静态变量、类信息、即时编译器编译后的代码等数据。
(2)方法区实际上来说是一个各个线程共享的内存区域,就是用来存储已经被虚拟机加载完成的各种即时编译器编译后的代码等一些数据。
2、图解JVM内存结构
(1)总体结构
(2)字节码及堆结构
(3)栈结构
(4)GC对应的堆结构
(5)堆中对象头结构
3、知识点
3.1 指针压缩
系统分为32和64位。32位可以寻址2^32,也就是4g的内存大小。所以32位的机,你就算给8g内存,也没有多大用处。64位的机,可想寻址会很大。
(1)压缩
对于jvm,默认是按8字节对齐的。
对于虚拟机,不开起指针压缩,一个对象,定义一个成员变量int类型,对象头中的markword是8个字节,类型指针是8个字节,没有数组的话,数组长度0个字节。实例数据int 4个字节,padding补4个字节,总24,是8的倍数。
开起指针压缩,类型指针是4个字节,总16字节,不用对齐padding。
(2)原理
对于32位机,地址0x1,0x2,..可以寻址到2^32
对于64位,如果8个字节为寻址宽度呢0x1000,0x2000。会发现是补了000。
那么2^32是在堆中的存储,在寄存器中可以补位000,变为2^35。大概32g
所以超过了32,其实大约31g,指针压缩会失效
3.2 对象逃逸分析
在jdk1.7之前,对象的创建都是在堆空间创建的,但在1.7的版本之后,HotSpot中默认开启了逃逸分析,所以对象还可能存在栈上。
3.2.1 什么是逃逸分析
逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。 逃逸分析(Escape Analysis)算是目前Java虚拟机中比较前沿的优化技术了。
3.2.2 逃逸分析的原理
Java本身的限制(对象只能分配到堆中),我可以这么理解了,为了减少临时对象在堆内分配的数量,我会在一个方法体内定义一个局部变量,并且该变量在方法执行过程中未发生逃逸,按照JVM调优机制,首先会在堆内存创建类的实例,然后将此对象的引用压入调用栈,继续执行,这是JVM优化前的方式。然后,我采用逃逸分析对JVM进行优化。即针对栈的重新分配方式,首先找出未逃逸的变量,将该变量直接存到栈里,无需进入堆,分配完成后,继续调用栈内执行,最后线程执行结束,栈空间被回收,局部变量也被回收了。如此操作,是优化前在堆中,优化后在栈中,从而减少了堆中对象的分配和销毁,从而优化性能。
3.2.3 逃逸的方式
(1)方法逃逸:在一个方法体内,定义一个局部变量,而它可能被外部方法引用,比如作为调用参数传递给方法,或作为对象直接返回。或者,可以理解成对象跳出了方法。
(2)线程逃逸:这个对象被其他线程访问到,比如赋值给了实例变量,并被其他线程访问到了。对象逃出了当前线程。
3.2.4 如何判断是否发生逃逸分析
(1)发生逃逸分析的情况:也就是该user对象有可能会被外部使用
public User test2(){
User user=new User();
user.setId(1);
user.setName("xiaoming")
return user;
}
(2)没有发生逃逸分析的情况:该user对象没有被外部使用
public void test1(){
User user=new User();
user.setId(1);
user.setName("xiaoming")
}
(3)这种没有被外部访问,且如果在堆内存上频繁创建,当方法结束,需要被gc,浪费性能,所以1.7的版本之后,默认开启了逃逸分析,对于这样的对象直接在栈上创建。
3.2.5 逃逸分析的好处
(1)栈上分配:
一般情况下,不会逃逸的对象所占空间比较大,如果能使用栈上的空间,那么大量的对象将随方法的结束而销毁,减轻了GC压力
(2)同步消除:
如果你定义的类的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。
(3)标量替换
Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。
二、 对象内存分配
1、对象内存分配流程
2、对象内存分配方式
2.1 指针碰撞
(1)适用于堆内存完整的情况,已分配的内存和空闲内存分表在不同的一侧,
(2)通过一个指针指向分界点,当需要分配内存时,把指针往空闲的一端移动与对象大小相等的距离即可,
(3)用于Serial和ParNew等不会产生内存碎片的垃圾收集器。
2.2 空闲列表
(1)适用于堆内存不完整的情况,已分配的内存和空闲内存相互交错,JVM通过维护一张内存列表记录可用的内存块信息,
(2)当分配内存时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录,
(3)最常见的使用此方案的垃圾收集器就是CMS。
2.3 并发解决方案
(1)同步锁定,JVM是采用CAS配上失败重试的方式保证更新操作的原子性;
(2)线程隔离,把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB), 哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的空间。
3、对象在Eden区分配
(1) 大多数情况下,对象在新生代中Eden区分配,当Eden区没有足够的空间进行分配时,JVM将发起一次minorGC
minorGC: 指发生在新生代的垃圾收集动作,minorGC非常频繁,回收速度一般也比较快.
FullGC: 一般会回收老年代,年轻代,方法区的垃圾,FullGC的速度一般会比minorGC的慢10倍以上.
(2)Eden与Survivor区默认8:1:1
(3)大量的对象被分配在Eden区,Eden区满了以后会触发minorGC,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到空闲的Survivor区,下次Eden区满了后又会触发minorGC,把Eden区和非空闲的Survivor区垃圾对象回收,把剩余的存活的对象一次性挪到另一块空闲的Survivor区,因为新生代的对象都是朝生夕死的,存活的时间很短,所以JVM默认的8:1:1的比例还是很合适的,让Eden区尽量的大,Survivor区够用即可
(4) JVM默认有这个参数
-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1的比例自动变化,如果不想这个比例有变化可以设置参数 -XX:-UseAdaptiveSizePolicy
4、对象进入老年代空间的方式
4.1 大对象直接进入老年代
(1)大对象就是需要大量连续内存空间的对象,例如:字符串,数组。
(2)JVM参数 -XX:PretenureSizeThreshold可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代。
(3)这个参数只在Serial和ParNew两个收集器下有效.例如:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC,如果对象大小超过了1000000字节,就会直接进入老年代。
(4)大对象直接进入老年代的优点: 避免大对象分配内存时的复制操作而降低效率.
4.2 长期存活的对象将进入老年代
(1)既然JVM采用了分代收集的思想来管理内存,那么内存在回收时就必须能识别哪些对象应放在新生代,那些对象应放在老年代中。
(2)为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器.如果对象在Eden出生并经过第一次minorGC后仍然能够存活,并且能够被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在Survivor每经历一次minorGC,年龄就+1。
(3)当它的年龄增加到一定的成都(默认15岁,CMS收集器默认6岁,不同的收集器略微会有点不同),就会晋升到老年代中。
(4)对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold来设置.
4.3 对象动态年龄判断
(1)当前非空的Survivor区域里,一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了,
(2)例如: 非空的Survivor区域里现在有一批对象,年林1+年龄2+年龄n+年龄等多个年龄对象总和超过了Survivor区域的50%,此时就会把年龄大于等于n的对象都放入老年代
(3)这个机制其实是希望那些可能是长期存活的对象,尽早的进入老年代.对象动态年龄机制一般是在minorGC之后触发的.
4.4 老年代空间分配担保机制
(1)年轻代每次minor GC之前JVM都会计算下老年代剩余可用空间
(2)如果这个可用空间小于年轻代现有的所有对象大小之和 (包括垃圾对象)
(3)就会看是否设置 -XX:-HandlePromotionFailure(jdk1.8默认设置)
(4)如果有设置,就会查看老年代的可用内存大小,是否大于之前每一次minor GC后进入老年代的对象的平均大小
(5)如果没有设置或者老年代可用内存大小小于之前每一次minor GC后进入老年代的对象的平均大小,那么就会触发一次Full GC,对老年代和年轻代一起回收一次垃圾,如果回收还是没有足够空间释放新的对象就会发生OOM
(6)如果有设置并且老年代可用内存大小小于之前每一次minor GC后进入老年代的对象的平均大小,那么就会触发minor GC,当然如果minor GC之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发Full GC,Full GC完之后如果还是没有空间放置minor GC之后的存活对象,则也会发生OOM
5、对象回收
堆中几乎存放着所有的对象实例,对垃圾回收前的第一步就是要判断那些对象已经死亡;即不能再被任何途径使用的对象
5.1 引用计数法
(1)对每个对象的引用进行计数,每当有一个地方引用它时计数器+1,引用失效则-1.引用计数放到对象头中,大于0的对象被认为是存活对象.
public class ReferenceCounting {
Object eg = null;
public static void main(String[] args) {
ReferenceCounting referenceCounting1 = new ReferenceCounting();
ReferenceCounting referenceCounting2 = new ReferenceCounting();
referenceCounting1.eg = referenceCounting2;
referenceCounting2.eg = referenceCounting1;
referenceCounting1 = null;
referenceCounting2 = null;
}
}
(2)如上面代码所示:除了对象referenceCounting1和referenceCounting2相互引用着对方以外,这两个对象之间再无任何引用,但是他们因为互相引用对方,导致它们的引用计数器都不为0;出现相互循环引用的问题,会导致GC回收器无法进行回收,引用计数法是可以解决循环引用问题的,主要是通过Recycler算法进行解决,但是再多线程环境下,引用计数变更也需要进行昂贵的同步操作,性能较低.目前主流的虚拟机并没有选择这个算法来管理内存
5.2 可达性分析算法
(1)将GCRoots对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象标记为非垃圾对象,其余未标记的对象都是垃圾对象
(2)GC Roots根节点: 线程栈的本地变量,静态变量,本地方法栈的变量等等
5.3 如何判断一个类是无用的类?
(1)类需要满足下面三个条件才能算是无用的类
(2)该类的所有实例都被回收,Java堆中没有存在该类是任何实例
(3)加载该类的ClassLoader被回收(只有自定义的类加载器才能被回收)
(4)该类的对应的Java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
三、与GC相关的常用命令
1、jmap
(1)jmap(Java Virtual Machine Memory Map)是JDK提供的一个可以生成Java虚拟机的堆转储快照dump文件的命令行工具。除此以外,jmap命令还可以查看finalize执行队列、Java堆和方法区的详细信息,比如空间使用率、当前使用的什么垃圾回收器、分代情况等等。
(2)参数说明
#使用格式
jmap [options] pid
# -heap 显示Java堆的信息:
jmap -heap pid
# -histo[:live]显示Java堆中对象的统计信息,包括:对象数量、占用内存大小(单位:字节)和类的完全限定名
jmap -histo pid
# -clstats 显示Java堆中元空间的类加载器的统计信息
jmap -clstats pid
# -finalizerinfo 显示在F-Queue中等待Finalizer线程执行finalize方法的对象。
jmap -finalizerinfo pid
# -help 显示jinfo命令的帮助信息。
2、jvisualvm
(1)VisualVM 是Netbeans的profile子项目,已在JDK6.0 update 7 中自带,能够监控线程,内存情况,查看方法的CPU时间和内存中的对 象,已被GC的对象,反向查看分配的堆栈(如100个String对象分别由哪几个对象分配出来的)。在JDK_HOME/bin(默认是C:\Program Files\Java\jdk1.6.0_13\bin)目录下面,有一个jvisualvm.exe文件,双击打开,从UI上来看,这个软件是基于NetBeans开发的了。
(2)VisualVM 提供了一个可视界面,用于查看 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的基于 Java 技术的应用程序的详细信息。VisualVM 对 Java Development Kit (JDK) 工具所检索的 JVM 软件相关数据进行组织,并通过一种使您可以快速查看有关多个 Java 应用程序的数据的方式提供该信息。您可以查看本地应用程序或远程主机上运行的应用程序的相关数据。此外,还可以捕获有关 JVM 软件实例的数据,并将该数据保存到本地系统,以供后期查看或与其他用户共享。
3、jstack
(1)命令参数
(2)死循环导致cpu飙高
步骤:查找进程-》查找线程-》分析threadDump日志-》找出问题代码
1)top找进程id,
2) top -p 进程id,按H(大写),查找该进程下所有线程
3)将线程id转换为16进制
4)jstack 16进制线程号(第3步转换后的 ) > lwp.txt
5)分析lwp.txt
4、jinfo
jinfo(Configuration Info for Java) 查看虚拟机配置参数信思,也可用于调整虚拟机的配置参数
(1)jinfo -sysprops pid :查看该进程的全部配置信息
(2)jinfo -flags pid: 查看曾经赋过值的参数值
(3)jinfo -flag <具体参数> pid: 查看具体参数的值
5、jstat
jstat(JVMStatisticsMonitoringTool): 虚拟机统计信息监视工具。
所有命令都可以先 -help一下,-help 是软件的通用命令,可以看一下有什么参数
(1)命令格式
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
参数 时间 每隔多少行 进程号 时间间隔 打印次数
(2)jstat -options:查看具体的参数命令
jstat -options
-class 类加载统计
-compiler 编译统计
-gc 垃圾回收统计
-gccapacity 堆内存统计
-gccause 近一次GC统计和原因
-gcmetacapacity 元数据空间统计
-gcnew 新生代垃圾回收统计
-gcnewcapacity 新生代内存统计
-gcold 老年代垃圾回收统计
-gcoldcapacity 老年代内存统计
-gcutil 垃圾回收统计(百分比)
-printcompilation JVM编译方法统计
(3)常用的gc命令
# 打印gc信息 每隔1000ms 打印40次
jstat -gc 12141 1000 40
字段具体含义
S0C:第一个幸存区的大小
S1C:第二个幸存区的大小
S0U:第一个幸存区的使用大小
S1U:第二个幸存区的使用大小
EC:伊甸园区的大小
EU:伊甸园区的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法区大小
MU:方法区使用大小
CCSC:压缩类空间大小
CCSU:压缩类空间使用大小
YGC:年轻代垃圾回收次数
YGCT:年轻代垃圾回收消耗时间
FGC:老年代垃圾回收次数
FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间
6、arthas工具
(1)下载测试用脚本
# 运行一个demo:
wget https://arthas.aliyun.com/math-game.jar
java -jar math-game.jar
(2)如何获得arthas,并启动
wget https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
(3)内部的常用命令
# The dashboard command allows you to view the real-time data panel of the current system.
Dashboard
# The thread 1 command prints the stack of thread ID 1.
Thread
#The sc command can be used to find the loaded classes in the JVM:
sc
#The jad command can be used to decompile the byte code:反编译
jad --source-only com.example.demo.arthas.user.UserController
#The watch command can view the parameter/return value/exception of the method,
# 查看函数的参数/返回值/异常信息
watch demo.MathGame primeFactors returnObj
#The vmtool command can search object in JVM
vmtool --action getInstances --className java.lang.String --limit 10
7、常量池
7.1 常量池分类
Java中的常量池分为:Class文件常量池、运行时常量池、全局字符串常量池、基本类型包装对象常量池
7.1.1Class文件常量池
(1)class文件是一组以字节为单位的二进制数据流,在java代码的编译期间,我们编写的Java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量池,class文件常量池在在编译阶段就已经确定。
(2)既然是常量池,那么里面存放的肯定是常量,那么什么是《常量》呢?class文件常量池主要存放两大常量:字面量和符号引用
(3)字面量
文本字符串,月就是我们经常声明的:public String s = "abc";中的"abc"
用final修饰的成员变量,包括静态变量、实例变量和局部变量
而对于基本类型数据(甚至是方法中的局部变量),也就是private int value = 1;基本类型(或字符)常量池只保留了他的字段描述I和字段的名称value,他们的字面量不会存在基本类型(或字符)常量池。
(4)符号引用:
类和接口的全限定名
字段的名称和描述符,字段也就是类或者接口中声明的变量,包括类级别变量和实例级的变量
方法中的名称和描述符
7.1.2 运行时常量池
(1)运行时常量池的作用是存储java class文件常量池中的符号信息,运行时常量池中保存着一些class文件中描述的符号引用,同时在类的解析阶段还会将这些符号引用翻译成直接引用(直接指向实例对象的指针,内存地址),翻译出来的直接引用也是存储在运行时常量池中。
(2)运行时常量池相对于class常量池一大特征就是具有动态性,java规范并不要求常量只能在运行时才能产生,也就是说运行时常量池的内容并不全部来自class常量池,在运行时可以通过代码生成常量并且将其放入到运行时常量池中,这种特性备用的最多的就是String.intern()。
7.1.3 全局字符串常量池
(1)在JDK1.6及之前的版本,字符串常量池存放在方法区中间,在JDK1.7版本以后,字符串常量池就被移到堆中了。HotSpot VM中,记录interned string的一个全局表叫StringTable,它本质上就是个HashSet;这个StringTable在每个HotSpot VM的实例只有一份,被所有类共享。
(2)注意:它只存储对java.lang.String实例的引用,而不存储String对象的内容
字符串常量池和上面的基本类型包装类常量池有些不用,字符串常量池没有事先的缓存一下数据,而是如果要创建的字符串在常量池内存中就返回对象的引用,如果不存在就创建一个放在常量池中。
(3)在Java中,有两种创建字符串对象的方法,一种是字面量直接创建,另一种是new一个String对象,这两种方法创建字符串对象的过程是不一样的。
String str1 = "abc"; //(1)
String str2 = new String("abc") //(2)
(4)如果是第一种方式创建对象,因为在编译时确定的,如果该字符串不在常量池中则会将该字符串放入常量池中并返回字符串对象的引用,如果在常量池中则直接返回字符串对象的引用;如果是第二种方式创建对象,因为要创建String类型的对象,而String对象是在运行时才加载到内存的堆中的,属于运行时创建,所以要先在堆中创建一个String对象,再去常量池中寻找是否有相同的字符串,如果有就返回堆中的String对象引用,如果没有则在将该字符串加入常量池中。
7.1.4 基本类型常量池
ava中基本类型的包装类大部分都实现了常量池技术,这些类Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面这五种整型的包装类也只是在对应值小于等于127时才可使用对象池,也就是对象不负责创建和管理大于127的这些类的对象。
7.1.5 不同字符定义在源空间和heap中存储方式
四、垃圾收集
1、垃圾收集算法
1.1 标记复制算法
会把内存分为相同的2个部分,每次回收,会把存活的对象移动到另一边,回收当前使用的空间。分配的内存被分成2份,实际使用空间变成正常的一半。但是不会出现垃圾碎片。
1.2 标记清除算法
标记存活的对象,把未标记的回收。回收后内存不是连续的,会产生大量的不连续的碎片,标记对象的时候效率低。
1.3 标记整理(压缩)算法
会把存活的对象移动到一起,清除边间外的垃圾对象,效率低
2、垃圾收集器
2.1 Serial和SerialOld
(1)Serial是新生代,SerialOld是老年代的回收期,串行化执行,最简单的单线程的收集器,会STW。
(2)CMS收集器如果空间不够无法进行FULL-GC,就会用SerialOld进行回收。
(3)Serial使用标记复制,SerialOld使用标记整理算法
2.2 Parallel Scavenge收集器 Parallel Old
(1)使用多线程,其他的和Serial相同,关心吞吐量,会自动调整参数设置吞吐量的大小,停顿的时间,提供最优的吞吐量。JDK8默认的收集器
(2)新生代采用复制算法,老年代使用标记整理算法。
2.3 parNew收集器
(1)使用多线程,其他的和Serial相同。使用复制算法
(2)新生代的收集器,可以搭配CMS收集器使用,
2.4 CMS收集器
为了提高用户的体验,提供最短的STW,并发收集,可以让垃圾回收线程和用户线程同时使用,标记清除算法,默认情况下是老年代内存达到92%会执行FullGC。
2.4.1 CMS回收垃圾过程
(1)初始标记:会STW,但是速度非常快,标记GCROOT能引用的对象,可以用-XX:+CMSParallellnitialMarkEnabled参数开启多线程执行。
(2)并发标记:根据GCROOT遍历所有对象,过程比较慢,不会STW,会和用户线程一起执行。
(3)重新标记:因为并发标记中垃圾回收线程会和用户线程一同执行,可能会出现,被标记为垃圾对象的现在不是垃圾对象了会对产生变动的重新进行标记。会STW,比初始标记时间长,并发标记时间短。可以用 -XX:+CMSParallelRemarkEnabled 参数开启多线程重新标记。(存活的对象现在是垃圾对象,可达变不可达,是不会被重新标记的,这个是浮动垃圾)
(4)并发清理:和用户线程一同执行,对未标记的对象清理,这个阶段如果有新添加的对象,会被标记为黑色。
(5)并发重置:重置本次标记的对象,与用户线程一同运行。
2.4.2 优缺点
(1)优点:并发执行,低停顿,用户体验较好
(2)缺点:
1)会产生浮动垃圾只能等待下一次回收
2)占用CPU资源
3)标记清除会产生空间碎片,可以通过开启参数,做完发FullGC自动整理碎片( -XX:+UseCMSCompactAtFullCollection),可以通过参数设置多少次FullGC整理一次内存碎片( -XX:CMSFullGCsBeforeCompaction)
2.4.3 CMS图解
在FullGC执行前,可以先进行一次YGC来减少内存对象的引用,加快CMS在FullGC时标记的速度(-XX:+CMSScavengeBeforeRemark)
2.5 G1收集器
2.5.1 实现方式:
(1)G1收集器把java的堆分为多个大小相等的区域Region,最多可以有2048个Region。一个Region = 堆容量/2048。适合大内存。
(2)年轻代(eden,survivor),占总容量的5%,但是在JVM运行的时候,会不断地给年轻代调整Region最多占60%可以通过参数调整-XX:G1MaxNewSizePercent。
(3)老年代:
(4)Humongous区:专门用来存储大对象,如果这个对象超过Region的50%就是一个大对象
(5)在进行GC的时候年轻代、老年代、大对象区都会被回收。
2.5.2 G1垃圾回收过程
(1)初始标记:会STW,记录GCROOT可以引用的对象,速度快
(2)并发标记:根据GCROOT遍历所有对象,过程比较慢,不会STW,会和用户线程一起执行。
(3)最终标记:和CMS的重新标记相同。
(4)筛选回收:根据设置的GC停顿时间(-XX:MaxGCPauseMillis)来设置回收计划,会对各个Region的进行回收的时间计算,优先选择回收价值高的Region来进行回收。默认的回收次数是8次,可以通过(-XX:G1MixedGCCountTarget)控制回收几次。会STW。
2.5.3 G1垃圾回收过程说明
(1)年轻代和老年代都是使用的复制回收算法,把一个Region复制到另一个Region中,不会出现空间碎片。
(2)YGC:YCG并不是Eden区满了就去回收,会先计算回收Eden需要多长时间,如果回收时间小于设置的回收时间,就继续给Eden分配Region,直到下一次满了在判断回收时间,如果接近设置的回收时间,就触发YGC.
(3)MixedGC:老年代的占有率达到回收的默认45%,就会执行MixedGC,回收所有的年轻代、部分老年代和大对象,在这个过程中,如果剩下的空Region放不下存活的对象,就会触发FGC。
(4)FGC:停止系统程序,采用单线程进行标记、清理、压缩,这个过程非常耗时。
2.5.4 G1图解
3、三色标记算法
3.1 黑色
代表这个对象所有的引用都被扫描完成。
3.2 灰色
这个对象所有的引用可能还有至少1个对象引用没有被扫描到
3.3 白色
(1)还没有扫描的会被标记为白色,如果在GC并发标记完成后,还是白色,就代表这个对象不可达,是个垃圾对象
(2)在并发标记的过程中,用户线程和标记线程是同时运行的,这个时候就会出现已经是垃圾对象的被重新引用,但是它已经被标记为白色,或者某个对象由存活对象变成垃圾对象,对象的引用发生了改变,这个时候就会出现多标或者漏标
3.4 漏标
漏标会把引用的对象当成垃圾回收掉,可以用增量更新或者原始快照实现
(1)增量更新-CMS
黑色对象指向白色对象的引用时,就把这个新插入的引用记录下来,等并发扫描结束后,在把这些记录过引用关系的跟节点重新扫描。
(2)原始快照(SATB)-G1
灰色对象要删除指向白色对象的引用,把这个删除的引用记录下来,在并发扫描结束后,在把这个灰色的对象重新扫描,这样就能扫描到这个对象,然后把这个白色的对象标记为黑色,等待下一次GC,这个白色的对象有可能是浮动垃圾。
(3)增量更新和原始快照都需要和写屏障来实现
写前操作,写后操作
写屏障实现增量更新: 把新的应用对象记录下来
写屏障实现SATB: 把删除的引用存放到队列中
3.5 多标-浮动垃圾
执行并发标记的时候有些被扫描过得存活对象,已经销毁,它已经被标记为黑色,这些对象就是浮动垃圾,还有就是并发清理开始后产生的对象会被当成黑色,但是他有可能是垃圾对象,这些都是浮动垃圾。
4、记忆集及卡表
五、双亲委派机制
1、类加载过程
2、JVM中提供了三层的ClassLoader
(1) Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
(2)ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
(3)AppClassLoader:主要负责加载应用程序的主函数类
3、双亲委派机制核心代码
(1)打开了我的AndroidStudio,搜索了下“ClassLoader”,然后打开“java.lang”包下的ClassLoader类。然后将代码翻到loadClass方法:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// -----??-----
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 首先,检查是否已经被类加载器加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 存在父加载器,递归的交由父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 直到最上面的Bootstrap类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
(2)其实这段代码已经很好的解释了双亲委派机制,为了大家更容易理解,我做了一张图来描述一下上面这段代码的流程:
(3)从上图中我们就更容易理解了,当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
4、为什么要设计这种机制
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。