一、内存分区
Java将内存分为了程序计数器、栈区、方法区、堆区。
1、程序计数器
程序计数器是内存中最小的区域,保存了下一条要执行指令的地址。
程序运行时,JVM就会把字节码加载起来,放到内存中,程序把指令从内存中取出来,放到CPU上执行这也就需要随时记住当前指令执行的位置,因为CPU是并发执行程序的,并且操作系统以线程为单位进行调度执行的,所以每个线程都有程序计数器。
2、栈
栈区主要保存的是局部变量和方法的调用信息,方法调用的时候就会涉及“入栈”操作,方法执行完之后就会涉及“出栈”的操作,局部变量也是类似。
栈空间也是比较小的,代码处理不当就会出现栈溢出异常。
每个进程都会分配一个栈区。
3、堆
堆区是内存空间中最大的区域,每次new出来的对象存在于堆区,对象的成员变量也就自然存在于堆区了。
一个进程会分配一个堆区,多个线程共享一个堆区。
4、方法区
方法区存放的是类对象,每次当.class文件编译形成.class文件,.class文件会被加载到内存中,也就会被JVM构造成类对象(加载的过程就是类加载),此处的类对象就会存放到方法区,类对象中的静态成员也会存放到方法区。
二、类加载
类加载是将.class文件加载到内存中,构建成类对象,类加载主要有Loading、Linking和Initiallizatio三步。
1、Loading
Loading过程是先找到对应的.class文件,然后打开并读取.class文件,会将读取并解析到的信息初步填写到类对象中,同时会初步生成一个类对象。
2、Linking
Linking来建立多个实体之间的联系,Linking又可以分为Verification、Preparation和Resolution三个阶段。
Verification
校验阶段,主要来验证读到的内容与规范中规定的格式是否完全匹配,如果返现格式不匹配,就会类加载失败,并且抛出异常。
Preparation
准备阶段,是正式为类中的变量分配内存并设置初始值,也就是给静态变量分配内存,并设置初值。
Resolution
解析阶段是Java虚拟机将常量池中的符号引用替换为直接引用的过程,也就对常量初始化的过程。
3、Initializing
是真正对类对象进行初始化,尤其是对静态成员
4、双亲委派模型
双亲委派模型其实是Loading的一个环节,描述的是JVM中的类加载器如何根据类的全限定名来找到.class文件的过程,也就是描述了找目录的过程。
JVM中提供了许多类加载器,每个类加载器负责一个片区,默认的类加载器主要有三个:
- BootStrapClassLoader:负责加载标准库中的类。
- ExtensionClassLoader:负责加载JDK扩展的类。
- ApplicationClassLoader:负责加载当前项目目录中的类。
查找标准库中的类的过程:
- 启动程序,先进入ApplicationClassLoader类加载器。
- ApplicationClassLoader就会检查它的父类是否已经加载过了,如果没有就调用父加载器ExtensionClassLoader。
- ExtensionClassLoader也会检查它的父类是否已经加载过了,如果没有就调用父加载器BootStrapClassLoader。
- BootStrapClassLoader会扫描自己负责的目录,一定会找到,找到后负责类加载的后序过程,直至查找环节结束。
查找自定义类的过程:
- 启动程序,先进入ApplicationClassLoader类加载器。
- ApplicationClassLoader就会检查它的父类是否已经加载过了,如果没有就调用父加载器ExtensionClassLoader。
- ExtensionClassLoader也会检查它的父类是否已经加载过了,如果没有就调用父加载器BootStrapClassLoader。
- BootStrapClassLoader会扫描自己负责的目录,如果没找到,就回到子加载器ExtensionClassLoader继续进行扫描。
- ExtensionClassLoader也会对自己负责的目录进行扫描,如果没找到,就回到子加载器ApplicationClassLoader继续进行扫描。
- ApplicationClassLoader对自己负责的目录进行扫描,如果找到就进行后续加载,否则就会抛出ClassNotFoundException异常。
三、垃圾回收
如果一直申请空间而不进行空间回收的话就会导致内存用完,也就是内存泄露的问题,对于栈和程序计数器不考虑垃圾回收,当方法或进程结束时所占用的空间也会随着释放,垃圾回收主要针对堆和方法区。
1、如何判断为垃圾?
引入引用计数
就是给对象再开辟一块区域用于引入计数器,当调用这个对象时,计数器就会加1,当引用失效的时候,计数器就会-1,如果计数器为0,该对象就会变为“垃圾”,进行回收。
但是也存在缺点:
空间利用率低,当对象本身空间很小时再增加一个计数器空间利用率低。
循环引用
public class T { public T t = null; public static void main(String[] args) { T t1 = new T(); T t2 = new T(); t1.t = t2; t2.t = t1; t1 = null; t2 = null; } }
两个对象相互引用之后引用的t对象的程序计数器就为1,无法进行进行回收。
可达性分析
定义一个起始位置GCRoots,类似于深度优先遍历,对可以遍历到的对象进行标记,没标记的对象就是不可达,就成了垃圾。
例如:
GCRoots可以访问所有结点就不存在垃圾。
GCRoots无法访问C和F,C和F就是垃圾。
在Java语言中,可作为GC Roots的对象包含下面几种:
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
2. 方法区中类静态属性引用的对象;
3. 方法区中常量引用的对象;
4. 本地方法栈中 JNI(Native方法)引用的对象。
2、如何进行垃圾回收?
标记清除
进行可达性标记,对于未标记的直接进行清除释放内存。
缺点:产生许多内存碎片。
复制算法
为了避免产生内存碎片,就引入了复制算法,将申请的内存一分为2,将不是垃圾的拷贝到另一边,将原来使用的一半空间整体释放。
缺点:
- 空间利用率低。
- 当垃圾较少时,复制开销较大。
标记整理
将不是垃圾的元素都拷贝到申请的空间的前面,后面的空间可以正常利用。类似于顺序表删除元素。
分代回收
对象新创建出来的时候先放在伊甸园,当伊甸园中的对象熬过一轮GC扫描,利用复制算法就会被拷贝到幸存区,在后序的几轮GC中幸存区的对象还是利用复制算法在幸存区之间来回拷贝,每一轮又会进行淘汰,在持续若干次之后,对象就会进入老年代,对象越老,继续存活的可能性就越大,老年的扫描频率低,并且老年代使用标记整理的方式进行回收。
分代回收中占用内存较大的对象可直接进入老年代。