1.内存区域划分(六大区域)
1.1 线程私有内存
线程私有:指这三块内存区域生命周期与线程的生命周期是相关的,随着线程的创建而创建,随着线程的销毁而回收。不同线程之间这三块内存彼此隔离。
(1)程序计数器
当前线程正在执行的字节码行号指示器,唯一一块不会产生OOM(OutOfMemoryError内存溢出)异常的区域。
若执行的是native方法,计数器值为0。
(2)Java虚拟机栈
**栈帧:**每个方法调用和结束都是一个栈帧,每个方法从调用到执行完成的过程,就对应一个栈帧在JVM中入栈出栈过程。
产生两种异常
- StackOverFlowError:栈溢出
- OOM:内存溢出
(3)本地方法栈(native方法的内存模型)
1.2 线程共享内存
线程共享:所有线程共享此三块区域内存,彼此不隔离。
(1)Java堆(GC堆:垃圾回收主要负责的区域)
存放所有对象实例以及数组空间。
- Xmx:设置堆的最大值
- Xms:设置堆的最小值
(2)方法区(永久代)
存放JVM加载的类信息、常量、静态变量。JDK8之前称为“永久代”
(3)运行时常量池
是方法区的一部分,存放字面量与符号引用。
字面量:指的是直接写出来的值
符号引用:符号引用–>找到指定的类,再通过引用变量找到堆空间
Test test = new Test();此处的test就是符号引用。
1.3 OOM产生原因
**内存泄漏:**产生的对象永远无法被垃圾回收
**内存溢出:**当前堆的空间过小,没有足够的空间容纳新的对象。适当的将堆的大小扩大基本就可以解决
2.垃圾回收策略
垃圾回收针对的是线程共享的内存,即GC堆、方法区、运行时常量池。
2.1 回收对象(判断对象是否存活)
不再存活的对象。
(1)引用计数法
给每个对象附加一个引用计数器,每当有一个引用指向当前对象,计数器+1,每当有引用不再指向当前对象,计数器值-1,任意时刻,引用计数器值为0的对象就被标记为不再“存活”。
**缺点:**无法解决循环引用问题,即B引用是A引用的属性,A引用也是B引用的属性。
(2)可达性分析算法
通过GC Roots的对象开始向下搜寻,若到指定对象有路可走(“可达”),认为此对象存活;若从任意一个GC Roots对象到目标对象均不可达,认为目标对象不再存活。
可以作为GC Roots的对象:
- 虚拟机栈与本地方法栈中的临时变量指向的对象
- 类中静态变量引用的对象
- 类中常量引用的对象
2.2 JDK1.2之后的引用
强引用、软引用、弱引用、虚引用(引用强度依次递减)
(1)强引用
程序中普遍存在,GC Roots对象指向的引用都属于强引用。
只要当前对象被任意一个强引用指向,即便内存不够用也不能回收掉此对象。
(2)软引用
用来描述有用但不必须对象(例如缓存对象)。
当前系统内存够用时,仅被软引用指向的对象还存活;若当前内存不够用,则下次GC(垃圾回收)时,会将所有仅被软引用指向的对象进行GC。
JDK1.2之后,SoftReference类表示软引用。
(3)弱引用
弱引用也是来描述非必须对象,但是强度要弱于软引用。
仅被弱引用指向的对象,只能存活到下次GC之前,当GC开始时,不管内存是否够用,都会回收仅被弱引用指向的对象。
JDK1.2之后,WeakReference类表示弱引用。
(4)虚引用
是四大引用中最弱的一种引用关系。
虚引用完全对对象的生存周期不造成任何影响,并且也无法通过虚引用取得一个对象。
为一个对象设置虚引用的唯一目的就在于该对象被GC之前由系统发回回收通知。
JDK1.2之后,PhantomReference类表示虚引用。
2.3 对象的自我拯救
当一个对象到GC Roots不可达时,JVM在进行GC之前,需要判断即将回收的对象所在的类是否覆写了finalize()方法。
- 若没覆写,此对象直接被回收
- 若对象所在的类覆写了finalize()
①若finalize()已被JVM调用,此对象直接被回收。
②若finalize()未被JVM调用,会调用finalize(),若对象在此次调用过程中与GC Roots有路可走,此对象不再被回收。
final、finally、finalize区别:
(1)final是关键字,是终结器。final 表示最终的、不可改变的。用于修饰类、方法和变量。
(2)finally是异常处理的一部分,它只能用在try/catch语句中,表示希望finally语句块中的代码最后一定被执行(但是不一定会被执行)。
(3)finalize是在java.lang.Object里定义的方法,Object的finalize方法什么都不做,对象被回收时finalized方法会被调用。
特殊情况下,可重写finalize方法,当对象被回收的时候释放一些资源。但注意,要调用super.finalize()。
2.4 垃圾回收算法
堆(所有对象和数组对象)
- 新生代:大部分对象在此区域存放,该区域特点“对象朝生夕死”(存活率很低)
- 老年代:存活率高。
(1)标记清除算法
算法分为标记与清除两个阶段:
- 标记阶段:首先将需要回收的对象打上回收标记
- 清除阶段:一次性回收所有被打上标记的对象空间。
缺点:
- 效率问题:标记和清除这两个过程的效率都不高;
- 空间问题(最大缺点):标记清除会产生大量的不连续空间碎片,导致gc频繁发生。
(2)复制算法(新生代垃圾的回收算法)
复制算法:
将新生代内存分为一块较大的Eden区(伊甸园)与两块大小相等的Survivor区(幸存者),默认比例为8:1:1,每次使用Eden与其中一块Survivor区(Survivor区一个叫From区,另一个叫To区)
复制算法的核心流程:
- 对象默认都在Eden区产生,当Eden空间即将满时,触发第一次Minor GC(新生代GC),将Eden区所有存活对象复制到From区,然后一次性清理掉Eden区的所有空间;
- 当Eden区再次即将满的时候,触发Minor Gc,此时需要将Eden与From区的所有存活对象复制到To区,然后一次清理掉Eden与From的所有空间。之后的新生代GC不断重复Step2,只是From与To来回作为备用区域。
备注:某些来回在From与To区交换若干次以上(默认是15次),将其置入老年代空间。
(3)标记整理算法(老年代的垃圾回收算法)
**核心思想:**首先将需要回收的对象打上回收标记,整理阶段先让存活对象向一端移动,而后清理掉存活对象边界之外的所有空间。
为何老年代不采用复制算法?
因为老年代存活率太高,每次复制的内容很多,开销大。
(4)分代收集策略(JavaGC)
将堆空间分为新生代与老年代空间,其中新生代采用复制算法,老年代采用标记整理算法。
对象的分配策略
- 对象默认在新生代的Eden区产生
- 大对象(占用内存比较多:例如集合类、数组)直接进入老年代。可以设置一个参数作为上限(超过该参数直接进入老年代):-XX:pretenureSizeThreshold
- 长期存活对象(默认是15)进入老年代
- 动态年龄判定:若from或to区的相同年龄对象总和超过Survior空间的一半,将所有此年龄对象直接晋升老年代。
Minor GC和Full GC有什么不⼀样?
- Minor GC⼜称为新⽣代GC : 指的是发⽣在新⽣代的垃圾收集。因为Java对象⼤多都具备朝⽣夕灭的特性,因此Minor GC(采⽤复制算法)⾮常频繁,⼀般回收速度也⽐较快。
- Full GC ⼜称为 ⽼年代GC或者Major GC : 指发⽣在⽼年代的垃圾收集。出现了Major GC,经常会伴随⾄少⼀次的Minor GC(并⾮绝对,在Parallel Scavenge收集器中就有直接进⾏FullGC的策略选择过程)。Major GC的速度⼀般会⽐Minor GC慢10倍以上
(5)对象的分配策略
- 对象默认在新生代的Eden区产生
- 大对象(占用内存比较多:例如集合类、数组)直接进入老年代。
可以设置一个参数作为上限(超过该参数直接进入老年代):-XX:pretenureSizeThreshold - 长期存活对象(默认是15)进入老年代
- 动态年龄判定:若from或to区的相同年龄对象总和超过Survior空间的一半,将所有此年龄对象直接晋升老年代。
3. JVM内置监测工具(JVM简单性能调优)
- jps:返回当前操作系统中的所有JVM进程ID,使用:jps -l(输出包名.类名)
- jmap:查看当前JVM的内存情况,使用:jmap -heap PID(查看PID的JVM的堆情况)
- jstack:查看当前JVM的线程栈情况,常用于解决线程卡死问题。
4. Java内存模型(描述并发程序的逻辑模型)
- Java的内存模型主要是定义JVM如何将变量存储到内存中,又如何将内存中的变量取回等细节。
- 内存模型中变量是指(线程共享):类中的实例属性、静态属性以及数组元素
4.1 要求
- 规定所有变量必须都存储在主内存中。
- 每个线程的内存叫工作内存,工作内存中保存了使用到主内存的变量的副本。
- 线程对于变量的所有操作必须在工作内存中进行,进而不能直接操作主内存。
不同线程间也无法访问彼此的工作内存,变量间的值传递均通过主内存来进行。
4.2 Java内存模型三大特性(线程安全条件)
并发程序同时满足以下三个特性才是线程安全的,任意一个不满足都不是线程安全。
- 原子性
概念:一个或一组操作要么全都发生,要么全都不发生。
基本数据类型的访问读写属于原子性操作。如若需要更大范围的原子性操作(运算等),需要使用synchronized或lock来保证原子性。 - 可见性
概念:当一个线程修改了共享变量的值,其他线程能够立即得知此修改。
synchronized/final/volatile可以保证可见性。 - 有序性
概念:程序执行的顺序按照代码的先后顺序执行,禁止进行指令重排序。
happens-before(先行发生原则,JVM层面保证的有序性)
4.3 volatile变量的特殊规则
- volatile变量具备可见性,当一个线程修改了这个变量的值,其他线程能够立即得知此修改。
应用场景:
a)运算结果不依赖当前变量的值(没有自增自减等非原子性操作);
b)变量不需要与其他变量共同参与运算。(eg:int j = i+1) - 使用volatile变量可以禁止CPU进行指令重排。(双重加锁单例模式)
a)当程序执行到volatile的读或写时,在其前面的操作一定全部进行完毕,并且结果对后面操作可见,后面的操作也还没有发生。
b)指令重排时,不能将volatile的语句提前或滞后。volatile变量可以相当于内存屏障。