Java只支持直接使用基本数据类型和对象类型,并且是JVM进行控制管理内存
这样出现内存问题,无法像C/C++那样对所管理的内存进行合理地处理
在虚拟机运行时,内存区域如下划分:
内存区域一共分为5个区域:
JVM虚拟机目的就是实现物理机那样的程序执行,JVM中的程序计数器可以看做是当前线程所执行字节码的行号指示器,而行号正好就指的是某一条指令,字节码解释器在工作时也会改变这个值,来指定下一条即将执行的指令。
当某个cpu线程切换到下一个线程继续执行,而当前线程的执行位置会被保存到当前线程的程序计数器中,当下次轮转到此线程时,恢复上下文数据进行继续执行。
虚拟机栈是一个栈结构,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧,栈帧中包括了当前方法的一些信息,比如局部变量表、操作数栈、动态链接、方法出口等。
局部变量表、操作数栈、动态链接、方法出口:
堆是整个Java应用程序共享的区域,此区域的职责就是存放和管理对象和数组,垃圾回收机制也是主要作用于这一部分内存区域。
方法区是整个Java应用程序共享的区域,它用于存储所有的类信息、常量、静态变量、动态编译缓存等数据,可以大致分为两个部分,一个是类信息表,一个是运行时常量池。方法区也是我们要重点介绍的部分。
各个内存区域的用途:
JVM提供了一套全自动的内存管理机制,当一个Java对象不再用到时,JVM会自动将其进行回收并释放内存。
原理:
+1
;当引用失效(比如离开了局部变量的作用域或是引用被设定为null
)时,引用计数-1
0
时,表示此对象不可能再被使用,已经没有任何方法可以得到此对象的引用了public class Main {
public static void main(String[] args) {
Test a = new Test();
Test b = new Test();
a.another = b;
b.another = a;
a = b = null;
}
private static class Test{
Test another;
}
}
目前比较主流的编程语言(包括Java),一般都会使用可达性分析算法来判断对象是否存活,它采用了类似于树结构的搜索机制。每个对象的引用都有机会成为树的根节点(GC Roots)。
可以被选定作为根节点条件如下:
一旦已经存在的根节点不满足存在的条件时,那么根节点与对象之间的连接将被断开。
由于其没有任何根节点引用,所以此对象即可被判定为不再使用,也就是说如果某个对象无法到达任何GC Roots,则证明此对象是不可能再被使用的。
循环引用的情况:
经历了可达性分析算法之后基本可能判定哪些对象能够被回收,但是并不代表此对象一定会被回收,依然可以在最终判定阶段对其进行挽留。
如果子类重写了此方法,那么子类对象在被判定为可回收时,会进行二次确认,也就是执行finalize()
方法,而在此方法中,当前对象是完全有可能重新建立GC Roots的
public class Main {
private static Test a;
public static void main(String[] args) throws InterruptedException {
a = new Test();
a = null;
//手动申请执行垃圾回收操作
System.gc();
//等垃圾回收一下()
Thread.sleep(1000);
System.out.println(a);
}
private static class Test{
@Override
protected void finalize() throws Throwable {
System.out.println(this+" 开始了它的救赎之路!");
a = this;
}
}
}
finalize()
方法并不是在主线程调用的,而是虚拟机自动建立的一个低优先级的Finalizer
线程(优先级比较低)进行处理
同一个对象的finalize()
方法只会有一次调用机会,如果连续两次这样操作,那么第二次,对象必定被回收
finalize()
方法也并不是专门防止对象被回收的,主要用它来释放一些程序使用中的资源等。
对象存活判定算法可以准确地知道堆中的哪些对象可以被回收了
垃圾回收算法是用来确定该如何回收可回收对象
Java虚拟机将堆内存划分为新生代、老年代和永久代
其中永久代是HotSpot虚拟机特有的概念,在JDK8之前方法区实际上就是采用的永久代作为实现,而在JDK8之后,方法区由元空间实现,并且使用的是本地内存,容量大小取决于物理机实际大小
在HotSpot虚拟机中,新生代被划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1,老年代的GC评率相对较低,永久代一般存放类信息等
运行原理:
新创建的对象,在一开始都会进入到新生代的Eden区(如果是大对象会被直接丢进老年代)
在进行新生代区域的垃圾回收时,首先会对所有新生代区域的对象进行扫描,并回收那些不再使用对象
一次垃圾回收之后,Eden区域没有被回收的对象,会进入到Survivor区的From区,当From区满了,将From中存活的对象移动到To区,此后每次 gc 存活的对象交换区域,每次交换都会进行年龄判定,当对象经历了15轮则会进入老年代
GC类型:
System.gc()
方法在一次GC后,新生代Eden区仍然存在大量的对象,超出Survivor区的容量,分配担保机制把Survivor区无法容纳的对象直接送到老年代
对于进入老年代的数据,会判断一下之前的每次垃圾回收进入老年代的平均大小是否小于当前老年代的剩余空间,如果小于,那么说明也许可以放得下,否则会先来一次Full GC,进行一次大规模垃圾回收,来尝试腾出空间,再次判断老年代是否有空间存放,要是还是装不下,直接抛出OOM错误
堆内存实际上是以分代收集机制为主,对于如何具体的收集过程有几种算法思想
标记-清除
算法:
首先标记出所有需要回收的对象,然后再依次回收掉被标记的对象,或是标记出所有不需要回收的对象,只回收未标记的对象。
缺点:回收内存顺序的不一致,导致内存空间出现内存碎片化,空间利用率降低
适用于新生代,新生代Survivor区其实就是这个思路
标记-复制算法:
将容量分为同样大小的两块区域,每次只使用其中的一块区域,每次垃圾回收结束后,将所有存活的对象全部复制到另一块区域中,并一次性清空当前区域。
优点:解决对象大面积回收后空间碎片化严重的问题
缺点:内存空间减半,消耗时间进行复制操作
进入到老年代,一般长期都不会回收,标记-复制算法就不太适合
在标记所有待回收对象之后,不急着去进行回收操作,而是将所有待回收的对象整齐排列在一段内存空间中,而需要回收的对象全部往后丢。
这样保证内存空间充分使用,但是效率低,由于需要修改对象在内存中的位置,此时程序必须要暂停才可以,在极端情况下,可能会导致整个程序发生停顿。
CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
它主要采用标记清除算法
它的垃圾回收分为4个阶段:
缺点:
Garbage First (G1) 收集器是一款主要面向于服务端的垃圾收集器,并且在JDK9时,取代了JDK8默认的 Parallel Scavenge + Parallel Old 的回收方案
G1收集器将整个Java堆划分成2048
个大小相同的独立Region
块,每个Region块
的大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且都为2的N次幂。所有的Region
大小相同,且在JVM的整个生命周期内不会发生改变。
每一个Region
都可以根据需要,自由决定扮演哪个角色(Eden、Survivor和老年代),收集器会根据对应的角色采用不同的回收策略。
G1收集器还存在一个Humongous区域,它专门用于存放大对象(超过了Region容量一半的对象为大对象)这样,新生代、老年代在物理上,不再是一个连续的内存区域,而是到处分布的。
回收过程与CMS大体类似:
在JDK8之后,Hotspot虚拟机不再使用永久代,而是采用了全新的元空间。
类的元信息被存储在元空间中。元空间没有使用堆内存,而是与堆不相连的本地内存区域。理论上系统可以使用的内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题。这项改造也是有必要的,永久代的调优是很困难的,虽然可以设置永久代的大小,但是很难确定一个合适的大小,因为其中的影响因素很多,比如类数量的多少、常量数量的多少等。
在Java中,如果变量是一个对象类型的,那么它实际上存放的是对象的引用,但是如果是一个基本类型,那么存放的就是基本类型的值。这样的的引用类型称为强引用
。
当JVM内存空间不足时,JVM宁愿抛出OutOfMemoryError使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。
软引用不像强引用那样不可回收,当 JVM 认为内存不足时,会去试图回收软引用指向的对象,即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。
引用比软引用的生命周期还要短,在进行垃圾回收时,不管当前内存空间是否充足,都会回收它的内存。
public class Main {
public static void main(String[] args) {
SoftReference<Object> softReference = new SoftReference<>(new Object());
WeakReference<Object> weakReference = new WeakReference<>(new Object());
System.gc();
System.out.println("软引用对象:"+softReference.get());
System.out.println("弱引用对象:"+weakReference.get());
}
}
虚引用相当于没有引用,随时都有可能会被回收。
Object());
WeakReference weakReference = new WeakReference<>(new Object());
System.gc();
System.out.println(“软引用对象:”+softReference.get());
System.out.println(“弱引用对象:”+weakReference.get());
}
}
#### 虚引用(鬼引用)
虚引用相当于没有引用,随时都有可能会被回收。
虚引用本身就不算是个引用,相当于这个对象不存在任何引用,并且只能使用带队列的构造方法,以便对象被回收时接到通知。