四、JVM
-
JVM内存分区
[图片上传失败...(image-a420b3-1642485776065)]
https://note.youdao.com/yws/public/resource/7ad083d3cb19a5c9a95bc4a3445ca9d7/xmlnote/98278A62E20C4E548A7B6A7CF4EFEBCF/2961
-
方法区/永久代(只有HotSpot才有永久代)(所有线程共有)
方法区是各个内存所共享的内存空间,方法区中主要存放被JVM加载的类信息、常量、静态变量、即时编译后的代码等数据。
-
堆区Head(所有线程共有)
是JVM所管理的内存中最大的一块。唯一目的就是存放实例对象,几乎所有的对象实例都在这里分配。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。
-
虚拟机栈(线程私有)
描述的是java方法执行的内存模型:每个方法在执行时都会创建一个栈帧,用于存储局部变量表,操作数栈,动态连接,方法出口等信息。每一个方法从调用直至完成的过程, 就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
-
本地方法栈(线程私有)
与VM栈发挥的作用非常相似,VM栈执行Java方法(字节码)服务,Native方法栈执行的是Native方法服务。
-
程序计数器(线程私有)
每条线程都需要有一个程序计数器,计数器记录的是正在执行的指令地址。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。如果正在执行的是Natvie方法,这个计数器值为空。这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。
栈溢出(StackOverflowError):如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。HotSpot虚拟机的选择是不支持扩展,所以除非在创建线程申请内存是就因无法申请足够的内存而抛出OutOfMemoryError,否则在线程运行时是是不会因为扩展而导致内存溢出的,只会因为栈容量无法获得获得新的栈帧而导致StackOverflowError异常
模拟一个栈溢出:
public class StackOverflowError { public static void main(String[] args) { StackOverflowError stackOverflowError = new StackOverflowError(); try { stackOverflowError.stackLeak(); System.out.print(stackOverflowError.i); } catch (Throwable e) { e.printStackTrace(); } } private int i = 1; public void stackLeak() { i++; stackLeak(); } }
堆溢出(OutOfMemoryError):Java堆主要存储对象实例,并且这些对象不可被GC,随着的对象的容量增加,总容量触及最大堆的容量后会抛出内存溢出异常。发生OOM后首先确认导致OOM的对象是否是必要的,然后确认是内存泄漏还是溢出,如果是溢出的话,可以看看堆的参数能否向上在调整,或者检查代码中是否存在生命周期过长,持有状态时间过长,存储结构设计不合理的,尽量减少内存消耗。
public class OOM { static class OOMObject{} public static void main(String[] args) { ArrayList<OOMObject> arrayList = new ArrayList<>(); while (true){ arrayList.add(new OOMObject()); } } }
永久代
jdk1.6及之前静态变量放在永久代,jdk1.7字符串常量池,静态变量移除保存在堆里
指内存的永久保存区域,主要存放 Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域,它和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出OOM异常。
元空间
jdk1.8及之后无永久代,改为元空间
在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory, 字符串池和类的静态变量放入 java堆中,这样可以加载多少类的元数据就不再由 MaxPermSize 控制, 而由系统的实际可用空间来控制。
补充:
-
常量池:编译器生成的各种字面量和符号引用;
-
关于字符串常量池和运行时常量池的位置说明:
-
jdk1.6 永久代 字符串常量池、运行时常量池、静态变量都是在永久代中
-
jdk1.7 永久代 字符串常量池和静态变量被移动到了堆当中,运行时常量池还是在永久代中
-
jdk1.8 元空间 字符串常量池和静态变量仍然在堆当中;运行时常量池、类型信息、常量、字段、方法被移动都了元空间中
-
-
元空间的好处:
-
减少报OOM的可能:元空间与永久代类似,本质区别是元空间并不占用虚拟机内存了,而是使用本地内存,由于本地内存一般是比较大的,所以方法区就没有那么容易报OOM(OutOfMemoryError)。
-
提高JVM性能:元空间的垃圾回收很少,一定程度上减少了GC扫描及压缩的时间。
-
类及相关的元数据的生命周期与类加载器的一致;
-
-
-
常见GC算法(答题顺序,先说可达性,再说算法)
垃圾是指在运行程序中没有任何指针指向的对象
-
判断对象存活
-
引用计数算法(根本没采用过,所以提一下就行)
每个对象有一个引用计数属性,新增一个引用时计数加 1,引用释放时计数减 1,计数为 0 时 可以回收。此方法简单,无法解决对象相互循环引用的问题。
-
可达性分析算法
从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有 任何引用链相连时,则证明此对象是不可用的。不可达对象。
固定可作为GC Roots的对象:
-
虚拟机栈中引用的对象,譬如:各线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
-
方法区中类静态属性引用的对象,譬如:Java类的引用类型静态变量
-
方法区中常量引用的对象,譬如:字符串常量池引用的对象
-
本地方法栈中JNI(即通常说的Native方法)引用的对象
-
java虚拟机内部引用的对象,譬如:基本数据类型对应的Class对象,一些常驻的异常对象等,还有系统类加载器
-
所有被同步锁持有的对象
-
反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
-
注意:不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
当一个对象经过可行性分析后发现没有和GC Roots相连接的引用链,将会被第一次标记,随后会进行第二次筛选,筛选的条件是该对象是否有必要执行finalize()方法。如果该对象没有覆盖finalize()方法或者此方法已经被虚拟机调用过,虚拟机将这两种情况视为"没有必要执行";如果判定有必要执行,会将对象放入一个名为F-Queue的队列中,稍后会有一个虚拟机自动创建的、低调度优先级的线程去执行它们的finalize()方法。倘若这次对象重新与引用链上的任何一个对象建立关系,即拯救了自己,会被移除"即将回收"的集合,如果这次还没有逃脱,基本就要被回收了。
-
-
垃圾收集算法
-
标记-清除算法
"标记-清除"(Mark-Sweep)算法,如它的名字一样,算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
它的主要缺点有两个:一个是效率问题,当回收的对象过多时,标记和清除过程的效率会降低;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
-
标记-复制算法(为解决内存碎片问题)
"复制"(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:这种算法的代价是将内存缩小为原来的一半,空间浪费巨大。
事实上,随着研究的进展,新生代中的对象有98%熬不过第一轮的收集,发现并不需要按照1:1的比例来划分Eden和Survivor的内存空间,一些新生代收集器的布局策略是分为一块较大的Eden空间和两块较小的Survivor空间(8:1),每次只使用Eden和1块Survivor,发生GC时,将活着的对象复制到另一块Survivor上。
-
标记-整理算法
标记阶段和 Mark-Sweep 算法相同,标记后不是直接清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。
-
分代收集算法
一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法;在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就采用"标记-清除"或"标记-整理"算法来进行回收。
补充:
-
"Stop The World"多发生在老年代
在垃圾回收过程中经常涉及到对象的移动操作(对象在Survivor from和Survivor to之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,必须全程暂停用户应用程序才能进行,这种情况被称为“Stop-The-World”,会导致系统全局停顿。
-
经典垃圾回收器
CMS、Serial、ParNew
-
-
-
类加载机制(什么是符号引用 -> 直接引用)
加载、验证、准备、解析、初始化、使用和卸载(验证、准备、解析属于连接)
-
加载
加载是一个读取Class文件,将其转化为某种数据结构存储在方法区内,并在堆中生成一个便于用户调用的java.lang.Class对象的过程。
在加载阶段,java虚拟机必须完成以下三件事:
-
通过一个类的全限定名来获取定义此类的二进制字节流
-
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
-
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类各种数据的访问接口
-
-
验证(验证包含很多步骤,分散在各个不同的阶段)
保证 Class 文件的字节流包含的信息符合 JVM 规范,不会给 JVM 造成危害
-
文件格式验证
检验字节流是否符合Class文件格式规范
-
元数据验证
对字节码描述的信息进行语言分析,是否符合《Java语言规范》的要求
-
字节码验证
主要目的是通过数据流分析和控制流分析,确定程序语义的合法化、符合逻辑的
-
符号引用验证
发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作发生在连接的第三个阶段——解析阶段中
-
-
准备
正式为类中定义的静态变量分配内存并设置初始值(赋零值),如果声明了final,例如:public static final int value = 123; 在准备阶段就直接将123赋值给value
-
解析(可以在初始化前,也可以在后)
Java虚拟机将常量池内的符号引用替换为直接引用的过程。
当一个java类被编译成Class之后,假设称为A,并且A中引用了B,那么在编译阶段,A是不知道B有没有被编译的,而且此时的B一定没有被加载,所以A肯定不知道B的实际地址,那么此时的A的Class文件,将使用一个字符串S来代表B的地址,那么S就被称为符号引用。
在运行时,如果A发生了类加载,到解析阶段会发现B还未被加载,那么就会触发B的类加载,将B加载到虚拟机中,此时A中B的符号引用将会被替换成B的实际地址,这被称为直接引用,这样A就能真正调用到B了。
如果上面A调用的B是一个具体的实现类,那么就称为静态解析, 因为解析的目标很明确。假如上层Java代码使用了多态,那么B可能是一个抽象类或者接口,它有两个实现类C和D,此时具体实现类并不明确,会等到运行过程中发生了调用,此时虚拟机中有了具体的类型信息,这时候才会去解析,就能用明确的直接引用替换符号引用了,这就为什么解析有时候会发生在初始化阶段之后,这就是动态解析,用它来实现后期绑定。
-
符号引用
以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。与虚拟机内存布局无关,引用的目标不一定是已经加载到内存当中的数据
-
直接引用
可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄,与虚拟机的内存布局有关,如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。
-
-
初始化
简单来讲,就是此时会判断代码中,是否存在主动的资源初始化操作,这里主动初始化动作不是指的构造函数,而是class层面的,比如静态代码块的逻辑、静态变量的赋值动作。
-
使用
使用过程就是根据程序定义的行为执行
-
卸载
卸载由 GC 完成。
-
-
类加载器
-
启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。
-
扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
-
应用程序类加载器(Application ClassLoader)
负责加载用户路径(classpath)上的类库。 JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader 实现自定义的类加载器。
-
如果用户认为有必要,可以自定义类加载器
https://note.youdao.com/yws/public/resource/7ad083d3cb19a5c9a95bc4a3445ca9d7/xmlnote/DFBAA44904E2490DAB85C7AD1A113395/3064
类加载过程(双亲委派机制)
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的 Class),子类加载器才会尝试自己去加载。
好处:
-
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。
-
这种层级关系可以避免类的重复加载,已经加载过的类就不需要再去加载了。
-
防止危险代码的植入,安全,比如String类,如果在AppClassLoader就直接被加载,就相当于会被篡改了,所以都要经过老大,也就是BootstrapClassLoader进行检查。
-
-
java四种引用,区别
-
强引用
最传统的“引用”的定义,是指程序代码之中普遍存在的引用赋值,即类似于“Object obj =new Object()”这样的引用。无论任何情况下,只要强引用这种关系还在,它就是可达状态,垃圾收集器就不会回收被引用的对象。因此强引用是造成java内存泄漏的只要原因之一。
-
软引用
软引用需要用 SoftReference 类来实现,可以和一个引用队列联合使用,用来描述一些还有用,但非必须的对象。对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被列入回收范围进行二次回收(如果这次还是没有足够的内存,才会抛出内存溢出异常)。软引用通常用在对内存敏感的程序中。
-
弱引用
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
-
虚引用
虚引用需要 PhantomReference 类来实现,它是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存的时间构成影响,也无法用虚引用来取一个对象实例。为一个对象设置虚引用的唯一目的只是为了能在这个对象被垃圾收集器回收时收到一个系统通知(跟踪对象被垃圾回收的状态)。
示意图
-
参考书籍:
《深入理解Java虚拟机》
网友评论