这些知识整理都是自己查阅帅丙资料(当然还有其他渠道)加以总结滴~ 每周都会更新知识进去。
如有不全或错误还请大家在评论中指出~
以Sun HotSpot虚拟机为例。Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
其中方法区和堆是所有线程共享的,栈,本地方法栈和程序虚拟机则为线程私有的(编译时确定所需内存大小)。
如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError
的区域,不需要进行 GC
。
每个方法被执行的时候都会创建一个栈帧
用于存储局部变量表,操作栈,动态链接,方法出口
等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
两种error:
StackOverflowError
。OutOfMemory
异常。虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务。
在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。会抛出StackOverflowError+OutOfMemory
堆是java虚拟机管理内存最大
的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。它的目的是存放对象实例。同时它也是GC所管理的主要区域,因此常被称为GC堆,又由于现在收集器常使用分代算法,Java堆中还可以细分为新生代和老年代。
OutOfMemoryError
)为了保证对象的内存分配过程中的线程安全性,HotSpot虚拟机提供了一种叫做TLAB(Thread Local Allocation Buffer)的技术。堆是线程共享的内存区域”这句话并不完全正确,因为TLAB是堆内存的一部分,他在读取上确实是线程共享的,但是在内存分配上,是线程独享的。
用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。在JDK1.8中,使用元空间
代替永久代来实现方法区(元空间并不在虚拟机中,而是使用本地内存),元数据空间并不在虚拟机中,而是使用本地内存(不需要进行 GC)
。但会出现OutOfMemoryError
-help
-version -showversion
-Xint:解释执行
-Xcomp:第一次使用就编译成本地代码
-Xmixed:混合模式,JVM自己来决定是否编译成本地代码,默认使用的就是混合模式
1、Boolean类型
格式:-XX[+-]<name>其中+-表示启用或者禁用name属性
比如:-XX:+UseConcMarkSweepGC表示启用CMS垃圾收集器,-XX:+UseG1GC表示启用G1垃圾收集器
2、非Boolean类型
格式:-XX:<name>=<value>表示name属性的值是value
比如:-XX:MaxGCPauseMillis=500表示GC的最大停顿时间是500毫秒,-XX:GCTimeRatio=19
注意:-Xmx和-Xms表示设置JVM的最大内存和最小内存,它们不是X参数,而是XX参数,-Xmx等价于-XX:MaxHeapSize,-Xms等价于-XX:InitialHeapSize;-Xss设置堆栈,也是XX参数,等价于-XX:ThreadStackSize
包括以下 7 个阶段:
方法区
的运行时存储结构。初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
public static int value = 123;
但是加上final后就会初始化为123
构造器
。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,通过程序制定的主观计划去初始化类变量和其它资源。Bootstrap ClassLoader
)此类加载器负责将存放在 Extension ClassLoader
)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 Application ClassLoader
)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。一个类加载器首先将类加载请求转发到父类加载器
,只有当父类加载器无法完成时才尝试自己加载。
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载
,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换
。
原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现。在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-.jar中的Driver类是用户自己写的代码,那启动类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由应用程序启动类去进行类加载。于是乎,JDBC通过线程上下文件类加载器Thread.currentThread().getContextClassLoader()
得到线程上下文加载器来加载Driver实现类。)。有了这个东西之后,程序就可以把原本需要由启动类加载器进行加载的类,由应用程序类加载器去进行加载了。
class com.mysql.jdbc.Driver-----sun.misc.Launcher$AppClassLoader@18b4aac2
class com.mysql.fabric.jdbc.FabricMySQLDriver-----sun.misc.Launcher$AppClassLoader@18b4aac2
DriverManager classLoader:null
在双亲委托模型下,类加载器是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是JAVA核心库提供的,而JAVA核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供),JAVA的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。而通过给当前线程设置上下文类加载器,就可以设置的上下文类加载器来实现对于接口实现类的加载
空闲链表
” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。空闲链表
寻找空间大于等于新对象大小 size 的块 block。如果它找到的块等于 size,会直接返回这个分块;如果找到的块大于 size,会将块分割成大小为 size 与 (block - size) 的两部分,返回大小为 size 的分块,并把大小为 (block - size) 的块返回给空闲链表。复制算法
,因为在 Eden 区对象大部分在 Minor GC 后都消亡,S0,S1 区域也比较小,降低了复制算法造成的对象频繁拷贝带来的开销
。JVM优化,就是尽可能的让对象都在年轻代里分配和回收,尽量别让太多对象频繁进入老年代,避免频繁对老年代进行垃圾回收,同时给系统充足的内存大小,避免年轻代频繁的进行垃圾回收。
Java中Stop-The-World机制简称STW,在 GC(minor GC 或 Full GC)期间,只有垃圾回收器线程在工作,其他工作线程则被挂起
。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互。
以 GC Roots 为起始点进行搜索,引出它们指向的下一个节点。。。直到所有的结点都遍历完毕,如果相关对象不在任意一个以 GC Root 为起点的引用链
中,则这些对象会被判断为「垃圾」,会被 GC 回收。
对象不可达(可回收)时,当发生GC时,会先判断对象是否执行了 finalize 方法,如果未执行,则会先执行 finalize
方法。 finalize 方法只会被执行一次,如果第一次执行 finalize 方法此对象变成了可达确实不会回收,但如果对象再次被 GC,则会忽略 finalize 方法,对象会被回收
!
虚拟机栈
(栈帧中的本地变量表)中引用的对象 (Test a = new Test();)Native
) 中引用的对象方法区中类静态属性
引用的对象 (public static Test s;)方法区中的常量
引用的对象 (public static final Test s = new Test();)Java 提供了四种强度不同的引用类型。(强引用 软引用 弱引用 虚引用)
CMS是一款并发、使用标记-清除
算法、以获取最小停顿时间
为目的、对老年代
进行回收的GC。CMS虽然是老年代的gc,但仍要扫描新生代。(GC ROOT TRACING可到达)
ParNew多线程收集器(新生代
, Serial 收集器的多线程版本)配合工作。3.1 初始标记(STW) - 可达性分析,标记GC ROOT能直接关联
到的对象。
3.2 并发标记 - 由前阶段标记过的对象出发,所有可到达的对象都在本阶段中标记。(GC Roots Tracing,耗时最长 )+并发预清理 - 标记从新生代晋升的对象、新分配到老年代的对象以及在并发阶段被修改了的对象。
3.3 重标记(STW) - 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
3.4 并发清理 - 用户线程被重新激活,同时清理那些无效的对象。+ 重置 - CMS清除内部状态,为下次回收做准备。
抢占CPU资源
,即GC线程与用户线程抢占CPU。这可能会造成用户线程执行效率下降。无法处理浮动垃圾
。(不能等到老年代满了再进行GC)大量的空间碎片
。空间碎片过多,就会给大对象分配带来麻烦。在G1提出之前,经典的垃圾收集器主要有三种类型:串行收集器、并行收集器和并发标记清除收集器,这三种收集器分别可以是满足Java应用三种不同的需求:内存占用及并发开销最小化、应用吞吐量最大化和应用GC暂停时间最小化,但是,上述三种垃圾收集器都有几个共同的问题:(1)所有针对老年代的操作必须扫描整个老年代空间;(2)新生代和老年代是独立的连续的内存块,必须先决定年轻代和老年代在虚拟地址空间的位置。
G1的设计原则是"首先收集尽可能多的垃圾
(Garbage First)"。
新生代其实并不是适用于这种算法的,依然是在新生代满了的时候,对整个新生代进行回收——整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小。
针对新生代和老年代,G1提供2种GC模式,Young GC和Mixed GC,两种会导致Stop The World
全局并发标记主要是为Mixed GC计算找出回收收益较高的Region区域
4.1 初始标记(STW):暂停所有应用线程(STW),并发地进行标记从 GC Root 开始直接可达的对象(原生栈对象、全局对象、JNI 对象),当达到触发条件时,G1 并不会立即发起并发标记周期,而是等待下一次新生代收集,利用新生代收集的 STW 时间段,完成初始标记,这种方式称为借道(Piggybacking)
4.2 根区域扫描:在初始标记暂停结束后,新生代收集也完成的对象复制到 Survivor 的工作,应用线程开始活跃起来; 此时为了保证标记算法的正确性,所有新复制到 Survivor 分区的对象,需要找出哪些对象存在对老年代对象的引用,把这些对象标记成根(Root); 扫描的 Suvivor 分区也被称为根分区(Root Region);
4.3 并发标记:标记各个堆中Region的存活对象信息
4.4 重新标记(STW): 和CMS类似暂停所有应用线程(STW),以完成标记过程短暂地停止应用线程, 标记在并发标记阶段发生变化的对象,和所有未被标记的存活对象,同时完成存活数据计算。
4.5 清理(STW):整理更新每个Region各自的RSet;回收不包含存活对象的Region;统计计算回收收益高(基于释放空间和暂停目标)的老年代分区集合
标记 - 整理
”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制
”算法实现的,这意味着运行期间不会产生内存空间碎片
。可预测
的停顿:用户可以指定期望停顿时间,G1 会将停顿时间控制在用户设定的停顿时间以内。传统的收集器如果发生 Full GC 是对整个堆进行全区域的垃圾收集,而分配成各个 Region 的话,方便 G1 跟踪各个 Region 里垃圾堆积的价值大小,这样根据价值大小维护一个优先列表,根据允许的收集时间,优先收集回收价值最大的 Region,也就避免了整个老年代的回收,也就减少了 STW 造成的停顿时间。
STW 过程中,初始标记因为只标记 GC Roots,耗时较短。再标记因为对象数少,耗时也较短。清理阶段因为内存分区数量少,耗时也较短。转移阶段要处理所有存活的对象,耗时会较长。因此,G1 停顿时间的瓶颈主要是标记-复制中的转移阶段 STW。
为什么转移阶段不能和标记阶段一样并发执行呢?主要是 G1 未能解决转移过程中准确定位对象地址的问题。
CMS 新生代的 Young GC、G1 和 ZGC 都基于标记-复制
算法。标记-复制算法可以分为三个阶段:
ZGC 也采用标记-复制算法,但ZGC 在标记、转移和重定位阶段几乎都是并发的,这是 ZGC 实现停顿时间小于10ms目标的最关键原因。
ZGC 只有三个 STW 阶段:初始标记,再标记,初始转移。
其中,初始标记和初始转移分别都只需要扫描所有 GC Roots,其处理时间和 GC Roots 的数量成正比,一般情况耗时非常短;再标记阶段 STW 时间很短,最多1ms,超过1ms则再次进入并发标记阶段。即,ZGC 几乎所有暂停都只依赖于 GC Roots 集合大小,停顿时间不会随着堆的大小或者活跃对象的大小而增加。与 ZGC 对比,G1 的转移阶段完全 STW 的,且停顿时间随存活对象的大小增加而增加。
ZGC 通过着色指针和读屏障技术
,解决了转移过程中准确访问对象的问题,实现了并发转移。大致原理描述如下:并发转移中“并发”意味着 GC 线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在 ZGC 中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了,那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。
Load Barriers读屏障
(之前的GC都是采用Write Barrier)尝试读取堆中的一个对象引用obj.fieldA并赋给引用o(fieldA也是一个对象时才会加上读屏障)。如果这时候对象在GC时被移动了,接下来JVM就会加上一个读屏障,这个屏障会把读出的指针更新到对象的新地址上,并且把堆里的这个指针“修正”到原本的字段里。这样就算GC把对象移动了,读屏障也会发现并修正指针,于是应用代码就永远都会持有更新后的有效指针,而且不需要STW。ZGC 中读屏障的代码作用:在对象标记和转移过程中,用于确定对象的引用地址是否满足条件,并作出相应动作。
ZGC 的核心特点是并发,GC 过程中一直有新的对象产生。如何保证在 GC 完成之前,新产生的对象不会将堆占满,是 ZGC 参数调优的第一大目标。
开启”基于固定时间间隔“的 GC 触发机制
-XX:ZCollectionInterval:比如调整为5秒,甚至更短。
增大修正系数
-XX:ZAllocationSpikeTolerance,更早触发 GC。ZGC 采用正态分布模型预测内存分配速率,模型修正系数 ZAllocationSpikeTolerance 默认值为2,值越大,越早的触发 GC。Zeus 中所有集群设置的是5。
在 Zeus 服务不同集群中,ZGC 在低延迟(TP999 < 200ms)场景中收益较大:
超低延迟(TP999 < 20ms)和高延迟(TP999 > 200ms)服务收益不大,原因是这些服务的响应时间瓶颈不是 GC,而是外部依赖的性能。
单代垃圾回收器每次处理的对象更多,更耗费 CPU 资源
;第二,ZGC 使用读屏障,读屏障操作需耗费额外的计算资源。
可以通过下面的参数打Heap Dump信息。
-XX:HeapDumpPath
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/usr/aaa/dump/heap_trace.txt
通过下面参数可以控制OutOfMemoryError时打印堆的信息
-XX:+HeapDumpOnOutOfMemoryError
JDK的自带工具,包括jstack(查看线程)
、jmap(查看内存)
和jstat(性能分析,堆内存各部分的使用量以及加载类的数量)
等常用命令:
#查看堆内存各区域的使用率以及GC情况
jstat -gcutil -h20 pid 1000
#查看堆内存中的存活对象,并按空间排序
jmap -histo pid | head -n20
#dump堆内存文件
jmap -dump:format=b,file=heap pid
dump
下来,用可视化的堆内存分析工具:JVisualVM
、JConsole、MAT等
-XX:+HeapDumpOnOutOfMemoryError
,dump堆内存查看创建了大量线程、对象,导致垃圾回收器来不及回收,分配的堆内存被占满,产OutOfMemoryError错误。
产生栈溢出的场景:比如死循环中创建对象。
-Xss
标识来增加线程栈的大小。Java的内存结构中,栈的大小不是无限的。大量的方法调用过程,导致不断压栈最终将栈内存占满,产生StackOverflowError错误,程序直接终止运行。
产生栈溢出的场景:比如不合理(递归太深)的递归调用。
当很多对象使用之后已经没有再使用的必要而没有置为null,导致垃圾回收器无法对其回收,造成内存资源的大量浪费,给系统带来很多不稳定因素。
产生内存溢出的场景:比如使用静态的集合。
// 如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露。
public class Simple {
Object object;
void method () {
object = new Object();
}
}
top -Hp
命令找出进程中占用cpu最高的前几个线程jstack获取线程快照
,然后使用线程id搜索分析快照文件内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。Java 使用有向图的方式进行垃圾回收管理,可以消除引用循环的问题,例如有两个对象,相互引用,只要它们和CG ROOTS不可达,那么GC也是可以回收它们的。
不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。