java源代码经过编译 ---> java字节码 ---> JVM 创建 main 主线程,使用的内存由虚拟机栈分配 --->
类加载子系统将类的原始信息加载到方法区 ---> 实例对象存储在堆中 ---> 局部变量和方法的参数存到虚拟机栈 --->
普通java方法的调用存在虚拟机栈 ---> 本地方法的调用存在本地方法栈 ---> 当前线程执行到第几行代码存在程序计数器中
---> 当对象不再使用,当内存不足时,会被垃圾回收回收
线程私有:程序计数器、虚拟机栈、本地方法栈
线程共享:方法区、堆
只有程序计数器不会出现内存溢出
(1)OutOfMemoryError的情况:
堆内存耗尽 -- 对象越来越多,又一直在使用,不能被垃圾回收
方法区内存耗尽 -- 加载的类越来越多,很多框架都会在运行期间动态产生新的类
虚拟机栈累积 -- 每个线程最多会占用1M内存,线程个数越来越多,而又长时间运行不销毁
(2)StackOverflowError
虚拟机栈内部 -- 方法调用次数过多
(1)方法区是JVM规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
(2)永久代是Hotspot虚拟机对JVM规范的实现(JDK1.8之前)
(3)元空间是Hotspot虚拟机对JVM规范的实现(JDK1.8之后),使用本地内存作为这些信息的存储空间
标记:
局部变量引用的对象和静态变量引用的对象都可以作为跟对象(GC Root),沿着跟对象的引用链,如果找到了某个对象,就加标记,
在垃圾回收时不会回收
清除:
没有加标记的对象直接释放内存
问题:被释放的内存不连续,会存在过多的内存碎片
标记:
局部变量引用的对象和静态变量引用的对象都可以作为跟对象(GC Root),沿着跟对象的引用链,如果找到了某个对象,就加标记,
在垃圾回收时不会回收
整理:
没有加标记的对象直接释放内存,将标记的对象移动到一端
问题:效率低,解决了内存碎片问题
常用于老年代(存活对象较多的区域)的垃圾回收
标记:
局部变量引用的对象和静态变量引用的对象都可以作为跟对象(GC Root),沿着跟对象的引用链,如果找到了某个对象,就加标记,
在垃圾回收时不会回收
复制:
将被标记的对象复制到另一个区域,将其他清除
问题:相比于标记整理效率提高了,但是占用了额外的内存
常用于新生代(存活对象较少的区域)的垃圾回收,不适用于老年代(存活对象多的区域)的垃圾回收
(1)GC
GC的目的是在于实现无用对象的内存自动释放,减少内存碎片、加快分配速度
GC要点:
(1)回收区域是堆内存,不包括虚拟机栈(在方法调用结束会自动释放方法占用的内存)
(2)判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
(3)GC的具体实现称为垃圾回收器
(4)GC大都采用了分代回收思想,理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难
回收,根据这两类对象的特性将回收区域分为新生代和老年代,不同区域应用不同的回收策略
(5)根据GC的规模可以分成 Minor GC(回收新生代),Mixed GC,Full GC(新生代和老年代都回收)
(2)分代回收
(1)三色标记
(2)并发漏标
用户线程对垃圾回收线程的影响
解决漏标问题:
(1)Incremental Update
只要赋值发生,被赋值的对象就会被记录。最后对这些被记录的对象进行重新标记
(2)Snapshot At The Beginning,SATB
新加对象会被记录,
被删除引用关系的对象也会记录。最后对这些被记录的对象进行重新标记
(1)Parallel GC
伊甸园区内存不足发生 Minor GC,标记赋值 STW
老年代内存不足发生 Full GC,标记整理 STW
注重吞吐量
(2)ConcurrentMarkSweep GC
老年代并发标记,重新标记时需要STW,并发清除
Failback Full GC(清除速度不及需要创建新的速度时,开始老年代和新生代都释放)
注重响应时间
(3)G1 GC
响应时间与吞吐量兼顾
把堆内存划分成多个区域,每个区域都可以充当伊甸园区、幸存区、老年区
新生代回收:伊甸园区内存不足时,标记复制 STW,复制到幸存区
并发标记:老年代达到堆内存的45%会触发并发标记,老年代并发标记,重新标记时需要 STW
混合收集:并发标记完成,开始混合收集,参与复制的有伊甸园区、幸存区、老年代,其中老年代会根据暂停时间目标,选择部分
回收价值高的区域,复制时 STW
Failback Full GC
(1)误用固定大小线程池
它的工作队列可以放整数最大值数量的任务对象,任务对象占用的内存会导致内存溢出
使用线程池的时候,根据实际情况,定义有大小限制的任务队列
(2)误用带缓冲线程池
对最大救急线程数量没有限制,因此会造成内存溢出
自己控制最大救急线程数量
(3)查询数据量太大导致
注意不要一次性查询所有数据
(4)动态生成类过多导致内存溢出
(1)加载
将类的字节码载入方法区,并创建类.class对象(在堆内存)
如果此类的父类没有加载,先加载父类
加载时是懒惰执行
(2)链接
验证--验证类是否符合Class规范,合法性、安全性检查
准备--为static变量分配空间,设置默认值
解析--将常量池的符号引用解析为直接引用
(3)初始化
执行静态代码块与非final静态变量的赋值
初始化是懒惰执行
final修饰的基本类型变量使用时不会进行类的加载,会将变量直接复制到自己的类中
final修饰的引用类型变量使用时会进行类的加载
class Student{
static final int age=1;
static final String name="lisi";
}
class test{
System.out.printlf(student.age);//不会加载student类信息
System.out.printlf(student.name);//会加载student类信息
}
类加载器加载时的工作方式:优先委派上级类加载器进行加载,如果上级类加载器
能找到这个类,由上级加载,加载后该类也对下级加载器可见
找不到这个类,则下级类加载器才能有资格执行加载
目的:
让上级类加载器中的类对下级共享(反之不行),即能让你的类能依赖到jdk提供的核心类
让类的加载有优先次序,保证核心类先加载
普通变量赋值即为强引用,如:Dog dog = new Dog();
通过 GC Root 的引用链,如果强引用找不到该对象,该对象才能被回收
例如:SoftReference a = new SoftReference(new A());
如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象
软引用自身需要配合引用队列来释放
典型例子是反射数据
例如:WeakReference a = new WeakReference(new A());
如果仅有弱引用引用该对象,只要发生垃圾回收,就会释放该对象
弱引用自身需要配合引用队列来释放
典型例子是 ThreadLocalMap 中的 Entry 对象
例如:PhantomReference a = new PhantomReference(new A());
必须配合引用队列一起使用,当虚引用引用的对象被回收时,会将虚引用对象入队,由 Reference Handler 线程释放其关联的外部资源
典型例子:Cleaner 释放 DirectByteBuffer 占用的直接内存
(1)它是Object中的一个方法,子类重写它,垃圾回收时此方法会被调用,可以在其中进行一些资源释放和清理工作
(2)但是将资源释放和清理放在finalize方法中不太好,非常影响性能,严重时甚至会引起OOM(内存溢出),从java9
开始就被标注为@Deprecated,不建议使用了
非常不好:
(1)FinalizerThread是守护线程,代码很有可能没来得及执行完,线程就结束了,造成资源没有正确释放
(2)会吞掉代码里的异常,导致不能判断在释放资源时是否出现错误
影响性能:
(1)重写了finalize方法的对象在第一次 GC 时,并不能及时释放它占用的内存,因为要等着FinalizerThread调用完
finalize,把它从第一个unfinalized队列移除后,第二次 GC 时才能真正的释放内存
(2)finalize的调用很慢,当内存不足时不能及时的释放内存,对象释放不及就会逐渐移入老年代,老年代对象积累过多就
会容易 full GC ,导致速度很慢。甚至 full GC 后如果释放的速度仍然跟不上创建新对象的速度,就会OOM(内存溢出)