本文主要讲述jvm 的核心知识点,包括jvm结构和GC
一、什么是JVM
JVM
一般C或者C++都是直接将代码生成机器指令,CPU可以直接执行这些指令,而Java则需要先生成字节码,JVM再将字节码解释成机器码。这么做的好处就是JVM屏蔽了底层平台的差别,可以做到一次编译,再各个平台运行,比如在Windows编译,也可以在Linux运行,这么做的缺点是JVM会影响性能,这也是Java的性能一般不如C或C++的原因。
【1】https://www.nowcoder.com/discuss/868904
【2】https://www.nowcoder.com/discuss/916441
二、JVM的主要组成部分及作用
JVM主要由类装载系统、执行引擎、运行时数据区、本地接口等四部分组成,其中运行时数据区是重点掌握内容
Class loader(类装载器):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
Execution engine(执行引擎):也称为解释器,负责解释classes中的指令,交由操作系统执行
Native Interface(本地接口):与native libraries交互,是与其它编程语言交互的接口。
Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
工作原理
首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader)再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构
三、JVM 运行时数据区
JVM的运行时数据区主要由方法区、堆、虚拟机栈、本地方法栈、程序计数器组成,其中方法区和堆是线程共享数据区,虚拟机栈、本地方法栈、程序计数器是线程私有数据区
(1)程序计数器(Program Counter Register)
①作用:记录当前线程所执行到的字节码的行号,字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
②意义:JVM的多线程是通过线程轮流切换并分配处理器来实现的,为了保证各线程指令的安全顺利执行,每条线程都有独立的私有的程序计数器
③内容:记录线程正在执行的代码对应的虚拟机字节码指令的地址
④异常:此内存区域是唯一一个在JVM上不会发生内存溢出异常(OutOfMemoryError)的区域
⑤生命周期:它的生命周期随着线程的创建而创建,随着线程的结束而死亡
(2)虚拟机栈(Java Virtual Machine Stacks)
①作用:描述Java方法执行的内存模型,每个方法在执行的同时都会开辟一段内存区域用于存放方法运行时所需的数据,成为栈帧,一个栈帧包含 如:局部变量表、操作数栈、动态链接、方法出口等信息。
②意义:JVM是基于栈的,所以每个方法从调用到执行结束,就对应着一个栈帧在虚拟机栈中入栈和出栈的整个过程。
③内容:局部变量表(编译期可知的各种数据类型和指向一条字节码指令的returnAddress类型)、操作数栈、动态链接、方法出口等信息
局部变量表所需的内存空间在编译期间完成分配。在方法运行的阶段是不会改变局部变量表的大小的。
④异常:
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
如果在动态扩展内存的时候无法申请到足够的内存,就会抛出OutOfMemoryError异常。
(3)本地方法栈(Native Method Stack)
①作用:本地方法栈与虚拟机栈的作用是一样的,区别是本地方法栈是为虚拟机调用Native方法服务
②意义:同上
③内容:同上
④异常:同上
(4)Java 堆(Java Heap)
①作用:为所有线程开辟的一块内存区域,存储对象实例,在虚拟机开启的时候创建,所有线程共享
②意义:方便集中管理对象内存(分配,回收)
③内容:对象实例,处于物理上不连续的内存空间,只要逻辑上是连续的就可以
④异常:如果堆上没有内存进行分配,并无法进行扩展时,将会抛出OutOfMemoryError异常
(5)方法区(Methed Area)
①作用:用于存储运行时常量池、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,所有线程共享
②意义:与堆形成对比,这里是存放类行管信息。方便对运行时常量池、常量、静态变量等数据进行管理
③内容:运行时常量池(具有动态性)、已被虚拟机加载的类信息、常量、静态变量等
④异常:当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
总结:一句话,划分的区域从静态数据(类加载),动态数据(对象),java执行(java方法),本地方法执行(本地方法)进行分析
四、HotSpot虚拟机对象
(1)对象是如何创建的
①类加载检查:
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
②分配内存:
在类加载检查后,就要为新生对象分配内存了,对象内存所需大小在类加载完成后便可以确定,内存分配方式根据Java堆中内存是否完整主要分为指针碰撞和空闲列表两种。
③初始化零值:
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这也是为什么字段在Java代码中可以不赋值就能直接使用的原因。
④设置对象头:
初始化零值后,虚拟机需要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息都是存放在对象的对象头中。根据虚拟机当前的运行状态不同,如是否使用偏向锁等,对象头都会有不同的设置方式。
⑤执行init方法:
上述操作完成后,从虚拟机的角度看,一个新的对象已经产生了。但从Java程序的角度看,对象创建才刚刚开始,方法还没有执行,所有的字段都还为零。所以,一般执行完new指令后还会接着执行方法,把对象按照程序员的意愿进行初始化(赋值),这样一个真正可用的对象才算生产出来
(2)创建对象时内存是如何分配的
①指针碰撞:
假设为Java堆中内存是绝对完整的,所有用过的内存放到一边,空闲的内存放到另一边,中间放着一个指针作为分界点的指示器,所分配的内存就是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞。
②空闲列表:
假设Java堆中的内存并不是完整的,已使用的内存和空闲内存都混在一起了,这时虚拟机需要维护一个列表,用来记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为空闲列表。
(3)对象的内存布局
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头、实例数据和对齐填充
①对象头
对象头包含两部分信息,一部分用于存储自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
②实例数据
实例数据部分是对象真正存储的有效信息,也是代码中所定义的各种类型的字段内容
③对齐填充
HotSpot虚拟机的自动内存管理系统要求对象起止地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍,对象头部分正好是8字节的整数倍,所以,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全,对齐填充并不是必然存在的,也没有特殊的含义,只是起到了占位符的作用。
(4)对象的访问方式
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。主流的访问方式有使用句柄和直接指针两种。
①句柄
Java堆中会划分出一块内存来作为句柄,reference中存储的是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
使用句柄访问对象的优势:reference中存储的是稳定的句柄地址,在对象移动时(垃圾回收)只会改变句柄中的实例数据指针,无需改变reference
②直接指针
如果使用直接指针访问,reference中存储的就是对象地址,而Java堆对象的布局需要考虑如何放置访问累类型数据的相关信息
使用直接指针访问对象的优势是省了一次指针定位的时间开销,速度更快(图片来源于《深入理解Java虚拟机》)
五、内存溢出异常问题
内存泄漏:指程序中动态分配给内存一些临时对象,并且这些对象始终没有被回收,一直占用着内存,简单来说就是申请内存使用完了不进行释放
可能出现的情况:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露
内存溢出:指程序运行过程中无法申请到足够的内存导致的错误
内存溢出的原因:死循环,启动参数内存值设置过小,长期的内存泄漏
内存溢出解决方案:修改JVM启动参数,增加内存;对代码进行排查
六、垃圾收集器
(1)简介
java语言最显著的特点就是引入了垃圾回收机制,它使java程序员在编写程序时不再考虑内存管理的问题。
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫面那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。
(2)垃圾回收器的基本原理
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。
通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。
当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。
(3)对象分配与回收策略
下图给出java堆内存结构,其分为两大块区域:新生代、老年代。其中新生代又包含三个区域:一个Eden区和两个Survivor区,由于在发生Minor GC时候会把存活的对象拷贝到另一个Survivor区上,因此也称为from区和to区 。
①gc执行步骤
一次Minor GC发生后:
1)Eden区活着的对象 + From Survivor 存储的对象被复制到 To Survivor ;
2)清空 Eden 和 From Survivor ;
3)颠倒 From Survivor 和 To Survivor 的逻辑关系: From 变 To ,To 变 From 。
–为什么新生代内存需要有两个Survivor区 https://www.jianshu.com/p/2caad185ee1f
②对象内存分配方式
对象优先在Eden区域分配,大对象直接进入老年代;之所以大对象直接进入老年代的目的是为了避免Eden和Survivor的互相拷贝大对象。 长期存活的对象会进入老年代;如果对象在Eden分配后并经过一次Minor GC(Young GC)依然存活,并且能被Survivor区域容纳,将对象复制到Survivor区域,同时将对象的年龄设置为1,对象在Survivor经历过一次Minor GC年龄加1,年龄增加到一定程度(默认是15岁),就会晋升到老年代。
–java堆内存分配与回收策略
③GC 触发的条件:
MinorGC(Young GC)是指发生在新生代的垃圾收集动作,因为java 对象大多数都具备朝生夕灭的特性,所以YoungGC一般比较频繁,一般回收的速度也比较快。触发条件:Eden空间不足以分配内存给新的对象。
Major GC(Full GC)指发生在老年代GC,出现Full GC 一般会伴有Yong GC,Full GC 速度一般比Young GC慢10倍以上。触发条件: 老生代空间不足,新生代对象或者大对象无法转入老生代,大对象一般需要大的连续空间,如果直接进入老年代,很容易出现Full GC,因此避免短存活期的大对象存在.
–JVM的组成和类的加载过程 https://www.nowcoder.com/discuss/864206 –Minor GC、Major GC和Full GC之间的区别
(4)GC 的两种判定方法:
①引用计数法
为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。
它有一个缺点不能解决循环引用的问题;同时,这种方式一方面无法区分软、虛、弱、强引用类别。
②可达性分析算法:
这个算法的基本思路是通过一系列的称为“GC Root”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,
当一个对象到GC Roots没有任何引用链相连(即从GC Roots到这个对象不可达)时,则证明该对象是不可用的,就说明可以回收
https://www.nowcoder.com/discuss/916441
七、JVM 有哪些垃圾回收算法
标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。
缺点:内存使用率不高,只有原来的一半。
标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法(上面提到)
八、深拷贝和浅拷贝
浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
九、一个对象的生命之旅(对象分配过程)
一个对象产生时,首先尝试在栈上分配,如果符合条件 分配在栈了,当方法结束时栈弹出,对象就终结了;
如果没在栈上分配,就判断对象,如果特别大直接进入Old区,否则的话就分配至Eden区(TLAB也属于Eden区);
如果进入Eden区:
经过一次GC后.Eden区中的存活对象进入S1;
每次GC,会把S1的存活对象扔进S2,S2的存活对象扔进S1,每换个区对象的年龄+1;
多次垃圾回收后,对象的年龄到了,就进入Old区.
(1)对象的分配总结图
–
–
–
完毕