1、内存结构
1、程序计数器
当前线程所执行字节码的行号指示器
每个线程都有一个独立的程序计数器,线程私有,随着线程的创建而创建,随着线程的结束而销毁
是唯一一个不会出现内存溢出的内存区域
若当前线程正在执行的是一个本地方法,那么此时程序计数器值为Undefined
2、Java虚拟机栈
每个方法执行都会创建一个栈帧,用于存储局部变量表,操作数栈,动态连接,方法出口信息,每一个方法调用直到执行完成,对应着一个栈帧在虚拟机栈中从入栈到出栈
局部变量表:基本数据类型,对象引用,返回地址
栈帧内数据共享,栈帧之间数据不能共享
线程私有,随着线程创建而创建,随着线程的结束而销毁
3、本地方法栈
程序计数器、虚拟机栈、本地方法栈随线程而生,也随线程而灭;栈帧随着方法的开始而入栈,随着方法的结束而出栈。
与Java虚拟机栈相似,Java虚拟机栈执行Java方法,本地方法栈执行Native方法
4、堆
Java虚拟机管理内存中最大的一块,主要存放new的对象实例和数组,线程共享,垃圾回收的主要区域
字符串常量池被移到了堆中
Java堆、栈区别
1、栈主要存储的是基本数据类型和对象的引用,堆主要存储new的对象实例、数组、字符串常量池
2、栈的存取速度比堆要快,仅次于寄存器
3、栈线程私有,堆线程共享
4、栈内存随着线程的结束被释放,堆内存被垃圾收集器不定时回收
年轻代使用复制算法回收对象,复制算法不会产生内存碎片
1、当Eden区没有足够的空间时,虚拟机将发起一次Minor GC,Minor GC开始的时候,对象只在Eden区和From Survivor,To Survivor是空的
2、Minor GC进行的时候,Eden区中所有存活的对象都会被复制到To Survivor
3、在From Survivor中仍存活的对象会根据他们的年龄来决定去向,年龄达到阈值(默认为15)会被移动到老年代中,没有达到阈值的对象会被复制到To Survivor,年龄+1
4、Minor GC过后,Eden区和From Survivor被清空,这个时候,From Survivor和To Survivor交换角色,会保证To Survivor是空的
5、Minor GC会一直重复这样的过程,直到To Survivor被填满,此时需要依赖老年代进行分配担保,将这些对象存放在老年代中
堆为什么分代
优化GC性能,如果没有分代,所有的对象都在一块,GC的时要找到哪些对象是没用的,这样就会对堆的所有区域进行扫描
GC期间会停止所有线程等待GC完成
Minor GC : 清理年轻代
Major GC : 清理老年代,出现Major GC通常会出现至少一次Minor GC
Full GC : 清理整个堆、元空间
Full GC触发条件:
1、System.gc() 方法的调用
2、老年代空间不足
3、方法区永久代空间不足
4、空间分配担保
5、老年代最大可用连续空间小于Minor GC历次晋升到老年代对象的平均大小
4、内存分配
1、新创建的对象优先分配到Eden区
2、大对象直接进入老年代
大对象是指需要大量连续内存空间的 Java 对象,如很长的字符串或数据
一个大对象能够存入 Eden 区的概率比较小,发生分配担保的概率比较大,而分配担保需要涉及大量的复制,就会造成效率低下
3、长期存活的对象将进入老年代
对象在Survivor区中每经过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到老年代中
4、动态对象年龄判定
Survivor中相同年龄对象的大小总和大于Survivor空间的一半,大于等于该年龄的对象可以直接进入老年代
5、空间分配担保
只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。通过清除老年代中废弃数据来扩大老年代空闲空间,以便给新生代作担保
5、方法区
用于存储被虚拟机加载的类信息、常量、静态变量、编译后的代码
方法区又被称为静态区,是程序中永远唯一的元素
Java 8中,元空间取代了方法区(永久代),元空间不在虚拟机中,而是使用本地内存
线程共享
方法区中主要清除两种垃圾:废弃常量、无用的类
6、直接内存(堆外内存)
直接内存是除 Java 虚拟机之外的内存
3、垃圾回收
1、判定对象是否存活
1、引用计数法
无法判断循环引用
2、可达性分析法
Java虚拟机中的垃圾回收器采用可达性分析法来探索所有存活的的对象
所有和 GC Roots 直接或间接关联的对象都是有效对象,和 GC Roots 没有关联的对象就是无效对象。
5、垃圾回收算法
1、标记-清除算法
遍历 GC Roots
,将存活的对象标记
清理没有标记的对象
缺点:
1、标记和清除效率不高
2、内存碎片
2、复制算法(新生代)
优点:不会产生内存碎片
缺点:内存减少一半
Eden、From Survivor、To Survivor比例是 8:1:1,每次使用Eden和From Survivor,浪费10%的内存
3、标记-整理算法(老年代)
遍历 GC Roots
,将存活的对象标记
移动存活的对象,回收末端内存
4、分代收集算法
Java堆根据对象的存活周期分为新生代和老年代
新生代中只有少量对象会存活,选用复制算法
老年代中对象存活率较高,选用标记-清除算法和标记-整理算法
6、垃圾收集器
1、新生代
1、Serial
复制算法,单线程,在垃圾收集过程中停止一切用户线程(Stop The World)
2、ParNew
Serial 的多线程版本,清理过程依然需要 Stop The World
追求低停顿时间
3、Parallel Scavenge
复制算法、多线程
追求 CPU 吞吐量,能够在较短时间内完成指定任务
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
2、老年代
1、Serial Old
标记整理算法、单线程,Serial 的老年代版本
Serial Old 收集器是 Serial 的老年代版本,都是单线程收集器,只启用一条 GC 线程,都适合客户端应用。它们唯一的区别就是:Serial Old 工作在老年代,使用“标记-整理”算法;Serial 工作在新生代,使用“复制”算法。
2、Parallel Old
标记整理算法,多线程,Parallel Scavenge 的老年代版本,追求 CPU 吞吐量(让单位时间内,stw的时间最短)
3、CMS
标记清除算法,多线程,追求低停顿,响应时间优先(尽可能让单次stw的时间最短)
在垃圾收集时使得用户线程和 GC 线程并发执行
- 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
- 并发标记:使用多条标记线程,与用户线程并发执行。此过程进行可达性分析,标记出所有废弃对象。速度很慢。
- 重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
- 并发清理:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象。这个过程非常耗时。
3、G1
它没有新生代和老年代的概念,而是将堆划分为一块块独立的 Region。当要进行垃圾收集时,首先估计每个 Region 中垃圾的数量,每次都从垃圾回收价值最大的 Region 开始回收,因此可以获得最大的回收效率。
G1 是整体上基于标记整理算法实现的收集器,从局部(两个 Region 之间)上看是基于复制算法实现的,这意味着运行期间不会产生内存空间碎片。
- 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
- 并发标记:使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢。
- 最终标记:Stop The World,使用多条标记线程并发执行。
- 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行。
2、类加载
类的生命周期
类加载的过程
1、加载
类加载器获取二进制字节流,将静态存储结构转化为方法区的运行时数据结构,并生成此类的Class对象。
2、验证
验证文件格式、元数据、字节码、符号引用,确保Class的字节流中包含的信息符合当前虚拟机的要求
3、准备
为类变量(静态成员变量)分配内存并设置其初始值,这些变量使用的内存都将在方法区中进行分配
4、解析
将常量池内的符号引用解析为直接引用,包括类或接口的解析、字段解析、类方法解析、接口方法解析
5、初始化
执行类中定义的Java程序代码
Java类的加载顺序
1、有继承关系的加载顺序
关于关键字static,大家 都知道它是静态的,相当于一个全局变量,也就是这个属性或者方法是可以通过类来访问,当class文件被加载进内存,开始初始化的时候,被static修饰的变量或者方法即被分配了内存,而其他变量是在对象被创建后,才被分配了内存的。
所以在类中,加载顺序为:
1.首先加载父类的静态字段或者静态语句块
2.子类的静态字段或静态语句块
3.父类普通变量以及语句块
4.父类构造方法被加载
5.子类变量或者语句块被加载
6.子类构造方法被加载
- 2、没有继承关系的加载顺序
- 静态代码块(只加载一次)
- 构造方法(创建一个实例就加载一次)
- 静态方法,调用的时候才会加载,不调用的时候不会加载
- 静态语句块和静态变量被初始化的顺序与代码先后顺序有关
4、类加载器
双亲委派模型
如果一个类加载器收到了类加载的请求,它会先把这个请求委派给父加载器去完成,,只有当父类加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载,因此所有的加载请求都会传送到启动类加载器中
在 java.lang.ClassLoader 中的 loadClass() 方法中实现该过程
使用双亲委派模型使得不同加载器加载的java.lang.Object类都是同一个