今天咱们来讨论下 Java 虚拟机,经过一系列常见的问题来逐渐深刻了解 JVM 建立对象过程,内存布局,类加载以及 GC 回收算法等机制。java
十问 JVM 问题整理:算法
Java虚拟机建立对象的过程 (使用 new 的方式)数组
对象的内存布局安全
双亲委派机制多线程
JVM的内存布局app
JVM 中的类加载机制函数
垃圾回收算法与垃圾收集器布局
在什么状况下对象从年轻代转移到老年代学习
Java方法区中存哪些东西, 如何控制方法区的大小编码
JVM 如何判断对象已死,可达性算法分析
happen-before 原则
下面咱们从 Java 建立对象的过程开始,逐渐深刻了解下 JVM。
Java虚拟机建立对象的过程 (使用 new 的方式):
虚拟机接到一条new指令时,首先检查这个类的参数是否能在常量池中定位到一个类的符号引用而且检查这个符号引用表明的类是否已被加载、解析和初始化过。
类加载检查经过后,虚拟机为新生对象分配内存(堆中分配内存方法:碰撞指针与空闲列表)
内存分配完成后,虚拟机须要将分配到的内存空间初始化为零值(默认值)
对对象进行必要的设置,如设置对象的哈希码,分代年龄等信息
执行 init 方法,按照程序值初始化。
通过以上步骤虚拟机会新建立一个对象,对象的内存布局以下:
对象头包括两部分信息,第一部分用于存储对象自身运行数据,如哈希码,GC分代年龄,锁状态标志,线程持有锁,偏向线程ID等,官方成为 Mark Word。
第二部分是类型指针,即对象指向它的类元数据信息
如图所示:
了解完了建立对象的过程与对象内存布局,在开始讨论 JVM 内存布局以前首先要了解下 JVM 的双亲委派机制:
主要目的是为了肯定类在 JVM 中的惟一性,确保程序运行稳定,安全。
当须要加载类时,类自己的加载器不会去加载而是先调用父类加载器,将请求委派给父类加载器,每层都是如此,所以全部加载最终都会到达顶层的启动类加载器。只有当父类加载器反馈不能加载,才会将加载任务给子加载器。
例如经过双亲委派机制使得用户自定义的 String 类不会被加载,从而保证系统的安全稳定。
启动类加载器 BootStrap 是 C++实现,主要负责加载 JAVA_HOME lib下类或被 –Xbootclasspath 参数指定的类库不能直接使用该加载器;
扩展类加载器 Extension 主要负责加载 JAVA_HOME下 lib/ext 下的类或系统变量 java.ext.dirs 指定类路径下的类库开发者能够直接使用;
应用类加载器 Application 加载用户指定的路径即 classpath 下的类若是应用程序没有指定加载器则默认使用该加载器。
JVM的内存布局
如图所示:
程序计数器:内存中较小的一块区域。当前线程在执行字节码的行号指示器。
线程是私有的,每一个线程都有一个程序计数器,线程之间相互独立,是虚拟机中惟一一个没有规定OutOfMemoryError 状况的区域。
虚拟机栈
栈也叫栈内存,主管Java程序的运行,是在线程建立时建立,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来讲不存在垃圾回收问题。
8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
栈帧中主要保存3 类数据:
本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
栈操做(Operand Stack):记录出栈、入栈的操做;
栈帧数据(Frame Data):包括类文件、方法等等。
本地方法栈
同虚拟机栈相同,只不过本地方法栈是为native 方法服务
堆
Java 堆是被线程共享的一块区域。 Java 堆是用来存放实例对象和数组对象。因为如今有了逃逸分析技术,也能够将对象分配在栈上。
同时 Java堆也能够是物理上不连续的区域,只要逻辑上连续便可。
在堆中为对象分配空间的方法有指针碰撞 和 空闲列表。
指针碰撞:内存规整
空闲列表:内存不连续
选择哪种分配方法取决因而否规整,是否规整取决于垃圾回收算法是否压缩。
方法区
与堆同样,是线程共享区域。用于存储已经被虚拟机加载的类的类信息、常量池、静态变量、编译后的代码,运行时常量池(存储编译器生产的各类字面值与符号引用),方法区不足时抛出OutOfMemoryError PermGen Space 异常。
堆外内存
即直接内存。堆外内存能减小IO时的内存复制,实现零拷贝,不须要GC。
JVM 中的类加载机制
主要过程分为 :加载验证 准备 解析 初始化
加载:将类转换成二进制字节流,将字节流表明的静态结构转化为动态结构,在内存中生产一个表明这个类的 java.lang.Class 对象,做为方法区中的这个类的访问入口。
验证:验证Class文件中的字节流是否符合java 虚拟机规范,包括文件格式,元数据验证等。
准备:为类变量分配内存并设置类变量的初始值,分配的内存在方法区中(类变量是类模板对象)
解析:将常量池中的符号引用转化为直接引用。
符号引用是使用一组符号描述锁引用的目标。Class 文件中的常量池中包括字面值与符号引用(字段的名称和描述符、方法名称和描述符、类和接口的权限定名),在 Class文件阿忠不会保存各个方法字段的最终布局信息。所以这些符号不住安慰是没法获得真正的内存入口地址。
直接引用于虚拟机实现的内存布局有关,能够直接指向目标的指针,偏移量或者指向目标的句柄。
初始化阶段:该阶段才会真正的开始执行类中定义java代码。初始化时执行类构造器 clinit()方法的过程。
垃圾回收算法与垃圾收集器
垃圾回收算法 :
标记清除算法:将全部须要回收的对象进行标记,标记结束后对标记的对象进行回收,可是效率低,会形成大量碎片。
复制算法:复制算法将空间分为两部分,每次使用其中的一部分,当一块内存用完了,就将这块的全部对象复制到另外一块,将已使用的块清除。不会产生碎片,会浪费必定的内存空间。在堆中的年轻代使用该算法。
由于年轻代对象多为生命周期比较短的对象。
年轻代将内存分为一个 Eden ,两个 survivor(from区、to区)。每次使用Eden与一个survivor。当回收时,将from 与Eden 中存活的对象复制到另外一个to区,最后清理掉Eden与from。当survivor 与Eden 中存活对象大小超过另外一个 survivor,则须要老年代来担保。
标记整理算法
复制算法在对象存活率较高时,复制会使效率下降,根据老年代特色,使用标记整理算法。标记以后将全部存活对象移向一端,将其余的清理,解决了碎片的问题。
分代收集算法
年轻代,老年代根据各自不一样特色采用不一样算法。
垃圾收集器:
Serial 收集器是单线程的收集器,在进行垃圾回收时须要中止其余的全部工做线程。
ParNew 收集器是Serial 的多线程版本。在单线程的环境下,parNew毫不比 Serial 收集器有更好的优点,随着spu增长能够提现出优点。
Parallel Scavenge 收集器,年轻代收集器,多线程并行收集,使用复制算法,与parNew 类似。
Serial Old 是serial 在老年代的版本
CMS:是一种获取最短停顿时间为目标的收集器。基于标记清除的算法实现。
G1收集器 能够用于年轻代与老年代的垃圾收集器。采用标记整理算法。
7. 将对象从年轻代到老年代是如何判断该对象执行力多久,在什么状况下转移? 哪些对象在老年代中?
轻GC 发生在年轻代,频率高速度快。
Full GC 是清理整个堆空间—包括年轻代和永久代。
通常新生成的对象都出如今 Eden 区,当 Eden 区被填满时,全部通过垃圾回收还存活的对象将被复制到两个 Survivor 区域中的一个,假定是 From 区,当From 区也被填满时,这个区域通过垃圾回收仍存活的对象将会被复制进 To 区,原From 区被清空,而且从 Eden 区过来的数据将直接进入To区。当To区也被填满时,以前从From区过来的数据若是仍在活动,则将被放入年老代,须要注意的是,两个survivor 区域总有一个是空的。
经过年龄计数器。对象每通过一个GC 存活,年龄计数器加一。当年龄超过设定的值,则将其经过担保机制转移到老年代。
老年代对象:
大对象(字符串与数组),超过了设定值的对象,直接在老年代中分配。
长期存活的对象进入老年代。
8. Java方法区中存哪些东西? JVM 如何控制方法区的大小以及内存溢出的缘由和解决方法。
方法区大小不是固定的,能够经过 JVM 参数动态调整。方法区中主要存放常量、静态变量、虚拟机加载的类信息和编译后的代码,运行时常量池。
减小程序中class 的数量。尽可能使用较少的静态变量。修改–XX : MaxPermSize调大。
错误类型为方法区溢出:PermGen space 错误。
9. JVM 如何判断对象已死,可达性算法分析
首先有一系列的 GC root 根节点。从这些根节点开始向下搜索,走过的路径成为引用链。当一个对象到 GC root 不存在任何引用链时,则此对象不可活,当对象不可活时,可用 finalize() 方法自救。但第二次被回收时则会死亡。
可做为 GC root 根节点的对象包括:
方法区中常量引用的对象
方法区中静态属性引用的对象
虚拟机栈中引用的对象
本地方法中引用的对象
引用类型:
强引用
特色:咱们日常典型编码Object obj = new Object()中的obj就是强引用。经过关键字new建立的对象所关联的引用就是强引用。 当JVM内存空间不足,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具备强引用的“存活”对象来解决内存不足的问题。对于一个普通的对象,若是没有其余的引用关系,只要超过了引用的做用域或者显式地将相应(强)引用赋值为 null,就是能够被垃圾收集的了,具体回收时机仍是要看垃圾收集策略。
软引用
特色:软引用经过SoftReference类实现。软引用的生命周期比强引用短一些。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 以前,清理软引用指向的对象。
弱引用
弱引用经过WeakReference类实现。弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程当中,一旦发现了具备弱引用的对象,无论当前内存空间足够与否,都会回收它的内存。
虚引用
特色:虚引用也叫幻象引用,经过PhantomReference类来实现。没法经过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 之后,作某些事情的机制。若是一个对象仅持有虚引用,那么它就和没有任何引用同样,在任什么时候候均可能被垃圾回收器回收。
10. happen before 原则
若是两个操做之间具备 happen –before 关系,那么前一个操做结果会对后一个操做可见。
常见的happen-before原则:
程序顺序规则
线程中的每一个操做,happen-before于线程中任意后续操做可见。
锁规则
对一个锁解锁,happen-before于随后给这个锁加锁。
volatile 规则
对 volatile的写happen-before 于任意后续volatile的读。
至此,十问JVM的问答也结束了,经过这一系列的问题咱们讨论了 JVM 的一些特性,在之后的学习中,咱们还会更加深刻的去探讨JVM更底层的一些性质。
参考资料:
《深刻理解 Java虚拟机 第二版》
关注一下,我写的就更来劲儿啦