运行时数据区
方法区、堆–>线程共享
虚拟机栈,本地方法栈,程序计数器–>线程私有的
方法区包含运行时常量池,运行时常量池是编译期生成引用
堆–>虚拟机启动时创建
栈–>方法执行时,其中局部变量表所需内存在编译期完成分配
运行时常量池–>1.Class中描述的符号引用 2.翻译出来的直接引用 3.运行时也可以将新的常量加入(例子:String类的intern()方法)
对象访问
主流方式 1.使用句柄 2.直接指针
- 句柄访问
Java堆中划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据的各自的具体的地址。
- 直接指针
reference中直接存储的就是对象地址
句柄访问中的reference稳定的指向句柄地址,在对象移动时(垃圾收集时,移动对象很常见),只需要改变句柄池中对象实例的地址,而不需要改变reference本身。
直接指针最大的好处就是速度更快,节省了一次指针定位的开销。
堆内存溢出
Java堆中用来存储对象的实例,我们不断创建对象,并保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会导致堆内存溢出异常。(OutOfMemoryError Java heap space)
出现问题原因:1.内存泄漏 2.内存溢出
分析:查看内存中的对象是否是必要的,如果是内存泄漏则查看泄露对象到GC Roots的引用链,则可以分析内存泄漏。
如果不是内存泄漏,换句话说内存中的对象确实必须还要活着,那就应当检查虚拟机的堆参数(-Xms -Xmx),与机器内存对比看是否可以调大,代码上检查是否有某些对象生命周期过长,持有状态时间过长。
虚拟机栈和本地方法栈溢出
栈容量由-Xss设置,两种异常:1.线程请求的栈深度大于虚拟机所允许的最大深度,StackOverflowError异常
2.虚拟机在扩展栈时无法申请到足够的空间OutOfMemoryError异常
内存 - Xmx(最大堆容量)- MaxPermSize(最大方法区容量)- 程序计数器消耗内存(小到可以忽略不记)= 虚拟机栈 + 本地方法栈
(如果虚拟机本身进程耗费的内存不在计算中)
运行时常量池溢出
(OutOfMemoryError PermGen space)
String类的intern()方法不断加入运行常量池
方法区溢出
(OutOfMemoryError PermGen space)
方法区用于存放Class相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等。
垃圾收集器与内存分配策略
引用计数算法
给对象添加一个引用计数器,每当有用他的时候计数器加1,当引用失效时,计数器就减1,任何时刻计数器都为0的对象就是不可能在被使用的。
上面的策略不能解决Java的对象相互循环引用问题objA,objB都有字段instance,objA.instance = objB;
objB.instance = objA,除此之外没有再被访问,但是他们相互引用导致引用计数器一直不为0,于是引用计数器无法通知GC收集器回收他们。
根搜索算法(JAVA、C#)
通过一系列的名为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所有走过的路径称为引用链(Reference Chain),当一个对象到GC roots没有任何一个引用链相连(图论里面的该对象不可达)时,则证明该对象是不可用的。
在Java语言里,可以作为GC Roots的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中的引用的对象
- 方法区中的类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的是Native方法)的引用对象
再谈引用
-
强引用
只要强引用还存在,垃圾收集器永远都不会回收掉被引用的对象。类似Object obj = new Object(); -
软引用
描述一些还有用但非必需。在系统将要发生内存溢出正常之前,将会把这些对象列入回收范围之内进行二次回收。 -
弱引用
也是用来描述非必需的对象。它的强度比软引用还要弱一些,被若引用的对象只能生存到下一次垃圾收集发生之前。 -
虚引用
幽灵引用,幻影引用。一个对象是否有需应用的存在都不会产生影响,也无法通过虚应用来获取一个对象实例。
生存还是死亡
在根搜索算法中,不可达并非意味着死亡,至少经历两次标记过程。
对象进行根搜索算法,如果没有连接GC Roots则会被第一次标记。 接着进行筛选,是否有必要执行finalize()方法?1.对象没有覆盖该方法 2.finalize()已被虚拟机调用过 这两种情况都是没有必要执行finalize()方法,则拯救自己失败,死亡。
如果有必要执行,将该对象放入F-Queue队列中,稍后虚拟机自己创建低优先级的finalize线程执行(虚拟机触发),只要重新与引用链上任何一个方法建立关联即可,拯救自我成功。
注:一个对象只能调用一次finalize()方法,面对下一次就失败,只能自我拯救一次。(不推荐使用finalize()方法,消耗大,不确定性高)
回收方法区
永久代的回收主要是两类:废弃常量和无用的类
废弃常量:该常量没有被引用
无用的类:1.该类的所有实例都被回收 2.加载该类的ClassLoader已经被回收 3.该类对应的对象java.lang.Class没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。同时满足这3个条件才是无用的类。
垃圾收集算法
标记-清除算法
缺点:1.效率低,标记和清除过程的效率都不高 2.空间问题,标记清除之后会产生大量不连续的内存碎片,导致可能无法找到足够大的内存。
复制算法
将容量划分为大小相等的两块,每次使用其中一块,一块用完了就将还存活的复制到另一块上,然后再把已使用的内存空间一次清理掉。代价是将内存缩小一半。 很多商业虚拟机使用该算法回收新生代,“From”区和“To”区互换角色,原Survivor To成为下一次GC时的Survivor From区, 总之,GC后,都会保证Survivor To区是空的。
标记-整理算法
让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
分代收集算法
新生代、老年代,根据对象的存活周期不同分类,再根据各个年代的特点采用最适合的收集算法。
垃圾收集器
Serial收集器
曾是新生代收集的唯一选择,单线程收集器。"Stop The World"收集时暂停其他进程。Client模式下很好的选择。
ParNew收集器
Serial收集器的多线程版本,很多运行在Server模式下新生代收集的首选。
Parallel Scavenge收集器
新生代收集器,也是并行多线程,使用复制算法的收集器。有两个参数用于精确控制吞吐量。分别是控制最大垃圾收集停顿时间、设置吞吐量大小的参数。
Serial Old收集器
单线程 标记-整理算法
Parallel Old收集器
Parallel Scavenge收集器的老版本,多线程“标记-整理”算法
CMS收集器
以获取最短回收停顿时间为目标的收集器,重视服务的响应速度。“标记-清除”算法实现
G1收集器
“标记-整理”算法
内存分配
-
对象优先在Eden分配,当空间不足时进行一次Minor GC,大对象直接进入老年代,大对象就是需要大量连续内存空间的Java对象。
虚拟机还设置了一个参数: -XX:PretenureSizeThreshold 令大于这个参数的对象直接进入老年代中分配,目的是避免在Eden区以及两个Suvivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)
PretenureSizeThreshold 参数只对Serial 和 ParNew两款收集器有效 -
长期存活的对象进入老年代,虚拟机给每一个对象设置了一个年龄( Age )计数器,每经历一次Minor GC就增加一岁,当增加到一定程度(默认是15岁)时就晋升到老年代。 -XX:MaxTenuringThreshold 可以设置年龄阈值。
-
动态对象年龄判断,当Suvivor区年龄相同的对象总大小 超过了Suvivor空间的一半,则年龄大于或者等于该年龄的进入老年代。
-
空间分配担保,在发生Minor GC时,虚拟机会检测晋升到老年代的平均大小是否大于老年代剩余空间大小,如果大于则进行Full GC 。如果小于则查看 HandlePromotionFailure 设置是否允许担保失败,如果允许则进行Minor GC,不允许则进行Full GC
类文件结构
class文件是二进制文件,它的内容具有严格的规范,文件中没有任何空格,全是连续的0/1。class文件中的所有内容被分为两种类型:无符号数 和 表。
- 无符号数
它表示class文件中的值,这些值没有任何类型,但有不同的长度。根据这些值长度的不同分为:u1、u2、u4、u8,分别代表1字节的无符号数、2字节的无符号数、4字节的无符号数、8字节的无符号数。 - 表
class文件中所有数据(即无符号数)要么单独存在,要么由多个无符号数组成二维表。即class文件中的数据要么是单个值,要么是二维表。
class文件的组织结构
- 魔数、本文件的版本信息、常量池、访问标志、类索引、父类索引、接口索引集合、字段表集合、方法表集合
类加载机制
一个类从.Class中开始被加载到内存中直到被卸载一共包括7个阶段:加载、验证、准备、解析、初始化、使用、卸载,其中验证、准备、解析被统称为连接。
四种情况必须对类进行实例化(主动引用)
- 遇到new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行初始化,则需要先进行初始化。
- 使用java.lang.reflect包的方法对类进行发射调用的时候。
- 当初始化一个类的时候,如果其父类没有初始化,则需要先触发其父类的初始化。
- 虚拟机启动时要对主类初始化
被动引用的例子:
- 通过子类引用父类的静态字段,不会导致子类的初始化
- 通过数组定义的引用类,不会触发此类的初始化
- 常量在编译阶段会存入该类的常量池中,不会出发此类的初始化
加载
加载阶段需要做如下三件事:
- 通过一个类的全限定名来获取其定义的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在Java堆中生成一个代表这个类的 java.lang.Class对象,作为对方法区中这些数据的访问入口。
验证
验证是连接阶段的第一步,它是为了确保被加载的类的正确性
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
为类的静态变量分配内存(static 定义的类变量,而不包括实例变量),并将其初始化为默认值。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,JDK8之前这些内存都将在方法区中分配,JDK8中他们会对着Class对象一起被存放于Java堆中。