先上网上大神整理的一张思维导图,对整体理解JVM
非常有帮助
1、JVM内存区域
JVM内存区域
就是Java虚拟机
在执行Java
程序的过程中把它管理的内存划分为若干个不同的数据区域,也叫运行时数据区
。
jdk1.8
和之前的版本有所不同。
jdk1.8
之前:
jdk1.8
:
(1)jdk1.8和jdk1.7以及之前的区别
jdk1.8
和之前不同的是,jdk1.8
彻底移除了方法区,取之的是元空间
,元空间
使用的是直接内存
。
jdk1.7
以及之前,堆内存可以分为三个部分:新生代、老生代和永生代内存
。而永生代也被称为方法区。(拓展:方法区是Java虚拟机的一个概念,而永生代则是方法区的一种具体实现)。
jdk1.8
之后就用元空间替代了永生代。原因有2个:
a.整个永生代有JVM
本身设置的一个固定大小上限,无法调整,而元空间使用的是直接内存,只受本机可使用内存的限制,内存溢出概率大为减少。
b.元空间中存放的是类的元数据,这样加载多少类就不由MaxPermSize
(方法区最大大小参数)限制了,而是由系统实际可用内存控制,可以加载更多的类。
(2)是否线程私有
线程私有:程序计数器、Java虚拟机栈、本地方法栈
。
线程共享:堆、方法区、直接内存(非运行时数据区的一部分)
。
1.1、程序计数器
程序计数器是一块比较小的内存空间,可以看作是当前线程执行的字节码的行号指示器。主要有两个作用:
(1)字节码解释器通过改变程序计数器的大小来依次读取指令,从而实现代码的流程控制,如循序执行、选择、循环和异常处理等。
(2)在多线程的情况下,程序计数器用于记录当前线程的执行位置,这样线程切换回来的时候就知道线程上次执行到什么位置。
程序计数器是唯一不会出现 OutOfMemoryError
的内存区域。它的生命周期伴随线程的创建和消亡。
1.2、Java虚拟机栈
Java虚拟机栈
的生命周期和线程相同,描述的是Java
方法执行的内存模型,每次方法调用的数据都是通过栈
传递的。Java
内存可以粗略的分为堆内存
和栈内存
,其中栈内存
就是Java虚拟机栈
,或者是Java虚拟机栈中的局部变量表
部分。(Java虚拟机栈
由一个个栈帧
组成,每个栈帧都有:局部变量表、操作数栈、动态链接、方法出口等
)
局部变量表中主要存储了编译期可知的各种数据类型
(long、int、boolean、byte
等)和对象引用
(reference
类型,它不同于对象本身,可能存储的是一个指向对象起始地址的引用指针,也可能是代表一个对象的句柄或其他和对象相关的位置信息)
Java虚拟机栈
可能出现两种错误:
(1)StackOverFlowError
:若当前Java虚拟机栈的内存不允许动态扩展,那么当线程请求的栈深度超过了Java虚拟机栈的最大深度,就是抛出这个错误。
(2)OutOfMemoryError
:Java虚拟机在动态拓展栈的时候申请不到足够的内存时,会抛出这个异常。
1.3、本地方法栈
本地方法栈和Java虚拟机栈的作用相似,Java虚拟机栈是为虚拟机执行Java方法服务的,本地方法栈是为虚拟机执行本地方法(native方法)服务的
。
1.4、堆
堆是Java虚拟机所管理的内存中最大的一块,是所有线程共享的一块内存区域。几乎所有的对象实例和数组是在堆中分配内存的(是几乎不是绝对,会存在一些对象在栈上分配内存)。
Java堆也是垃圾收集器管理的主要区域,也称为GC堆。Java堆可以细分为新生代、老生代、永生代(jdk1.8后被替换为元空间)。更细致点:Eden空间、from Survivor、to Survivor空间等,进一步划分的目的是为了更好的分配和回收内存空间。
对象会首先在Eden中创建,在经历一次新生代垃圾收集后,若该对象还存活,则进入到from Survivor或to Survivor,并且年龄加1,等到年龄到达一个阈值(默认是15)时,就是进入到老生代。这个阈值可以通过-XX:MaxTenuringThreshold
来设置。
堆这里最容易出现的就是OutOfMemoryError
错误,并且出现这种错误之后的表现形式还会有几种,比如:
(1)OutOfMemoryError: GC Overhead Limit Exceeded
: 当JVM
花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
(2)java.lang.OutOfMemoryError: Java heap space
:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发java.lang.OutOfMemoryError: Java heap space
错误。
1.5、方法区
方法区也是被线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译期编译的代码数据等。Java虚拟机规范中把方法区描述为堆的一个逻辑部分,但是它有一个别名Non-Heap(非堆)。
方法区也被称为永生代,在jdk1.8被移除,替换为元空间。
永生代常用参数:
-XX:PermSize=N //方法区 (永久代) 初始大小
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
元空间常用参数
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小
拓展
:运行时常量池和字符串常量池
在jdk1.7
,运行时常量池存在方法区中,里面存的除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用),实际常量对象存在堆中的,而字符串常量池从方法区中拿到了堆中。
在jdk1.8
,元空间替换了方法区存于直接内存中,这时字符串常量池还在堆中,运行时常量池在直接内存的元空间中。
1.6、直接内存
直接内存不是Java虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。
JDK1.4
中新加入的NIO(New Input/Output)
类,引入了一种基于通道(Channel)
与缓存区(Buffer
) 的 I/O
方式,它可以直接使用Native
函数库直接分配堆外内存
,然后通过一个存储在 Java 堆
中的DirectByteBuffer
对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆
和 Native 堆
之间来回复制数据。
本机直接内存的分配不会受到 Java
堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
2、虚拟机对象
2.1、对象的创建
(1)类加载检查
Java
虚拟机执行一个new
指令时,首先判断这个指令能否在常量池中定位到这个类的符号引用,并且检查这个符合引用代表的类是否被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
(2)分配内存
在类加载过后,虚拟机会为新生对象分配内存,分配内存的大小在类加载的时候就已经确定了。分配内存的方式有指针碰撞
和空闲列表
两种,取决于堆中内存是否规整,堆内存是否规整又取决于GC
堆垃圾收集算法是标记-清理(不规整)
还是标记-整理(规整)
,标记-复制
也是规整的。
内存分配也有并发问题,因为系统中创建对象的操作是非常频繁的,JVM
采用两种方法来保证线程安全,一是CAS
+失败重试,二是先为每一个线程在Eden
中分配一个内存区域TLAB
,在TLAB
中创建对象,若对象大小大于TLAB
或者TLAB
内存不够了,则采用CAS
+失败重试。
(3)初始化零值
内存分配完成后,虚拟机将分配的内存空间初始化零值(不包括对象头),这样保证对象的实例字段在Java
代码中不需要初始值就可以使用它们的数据类型所对应的零值。
(4)设置对象头
初始化零值后,虚拟机开始设置该对象的对象头相关信息,如该对象是哪个类的实例,类的元数据在什么地方,对象的哈希值,GC
分代年龄等。
(5)执行init
方法
上面的都完成后,从虚拟机的角度来看,一个对象就已经产生了,但是在Java
程序来看,对象创建才开始,<init>
方法还没有执行,所有字段还都为零,所以执行new
命令后,需要执行<init>
方法,对象按照程序员的逻辑初始化,对象才算真正的创建成功了。
2.2、对象的内存分布
在Hotstop
虚拟机中,对象在内存中分为三个区域:对象头,实例数据和对齐填充
。对象头
存储的是对象自身运行时数据(哈希值、GC
分代年龄、锁标志等),还存储类型指针
,即这个对象指向它的类的元数据的指针。实例数据
就是对象存储的真正有效的信息。对齐填充
不是必须存在的,没有什么特殊含义,仅仅是占位使用,因为虚拟机要求对象的大小必须是8字节的整数倍,不够的话就对齐填充。
2.3、对象的访问定位
Java程序通过虚拟机栈
上的reference
数据来操作对象,主要的访问方式有两种:句柄访问和直接指针
。
3、JVM垃圾收集
3.1、垃圾收集的区域
针对HotSpot VM
的实现,它里面的GC
其实准确分类只有两大种:
(1)部分收集 (Partial GC
):新生代收集(Minor GC / Young GC)
:只对新生代进行垃圾收集;老年代收集(Major GC / Old GC)
:只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;混合收集(Mixed GC)
:对整个新生代和部分老年代进行垃圾收集。
(2)整堆收集 (Full GC):收集整个 Java 堆和方法区。
3.2、怎么判断对象是否死亡
堆中几乎存放的所有的对象实例,垃圾回收的第一步就是判断哪些对象已经死亡。
(1)引用计数法
给对象添加一个引用计数器,当有引用时,就加1,当引用失效后,就减1,任何时候计数器的值为0则说明这个对象是不可用的。
这个方法简单效率高,但是有个问题,不能解决两个对象之间互相循环引用的问题。
(2)可达性分析算法
通过一系列称为GC Roots的对象作为起点,从这些起点开始向下搜索,节点走过的路径称为引用链,当一个对象到达GC Roots没有任何引用链相连的话,则说明这个对象没有引用。
可作为 GC Roots
的对象包括下面几种:
a.虚拟机栈(栈帧中的本地变量表)中引用的对象;
b.本地方法栈(Native 方法)中引用的对象;
c.方法区中类静态属性引用的对象;
d.方法区中常量引用的对象;
e.所有被同步锁持有的对象。
如图中,对象5、6、7到达不了GC Roots
,所有是可以被回收的。
(3)不可达对象并非是非死不可
可达性分析中不可达的对象是处于缓刑阶段,要真正判断是个一个对象是否死亡,至少要经历两次标记
过程;可达性分析法中不可达的对象被第一次标记
并且进行一次筛选
,筛选的条件是此对象是否有必要执行 finalize
方法。当对象没有覆盖finalize
方法,或 finalize
方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记
,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
(4)四种引用
强引用
:大部分引用都是强引用,如果一个对象有强引用,垃圾回收器是不会回收这个对象的。
软引用
:一个对象如果只具有软引用,如果内存空间足够,垃圾回收器就不会回收它,但是内存空间不够的话,就会回收这个对象。
弱引用
:弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
虚引用
:"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。
弱引用
和虚引用
在程序中很少使用,软引用比较常用,因为软引用可以加速JVM对垃圾内存的回收速度,维护系统的运行安全,减少内存溢出的问题。
(5)判断废弃常量
假如在字符串常量池中存在字符串 "abc"
,如果当前没有任何 String
对象引用该字符串常量的话,就说明常量"abc"
就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc"
就会被系统清理出常量池了。
(6)判断无用的类
a.该类所有的实例都已经被回收,也就是Java 堆
中不存在该类的任何实例。
b.加载该类的 ClassLoader
已经被回收。
c.该类对应的java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
满足上面三个条件的类可以被回收,但是并不绝对。
3.3、垃圾收集算法
(1)标记-清除算法
首先标记出不需要回收的对象,然后清除所有没有标记的对象。该算法有两个问题,一是效率问题,二是标记-清除后会产生大量不连续的内存碎片。
(2)标记-复制算法
为了解决效率问题,标记-复制算法把内存空间分为相等的两块,每次使用其中的一块,当这一块的内存使用完后,将存活的对象标记,复制到另一半内存中,然后清除该块已使用的内存。这样每次垃圾回收只回收一半的内存区域。
(3)标记-整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
(4)分代收集算法
根据对象存活的周期的不同,将内存分为几个区域,一般将Java堆分为新生代和老生代,这样可以根据不同年度的特点来选择不同的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
3.4、垃圾收集器
垃圾收集器是垃圾收集算法的具体实现。
4、类加载
一个类完整的生命周期如下:
4.1、类加载过程
类加载过程主要有三步:加载、连接、初始化
。其中连接分为:验证、准备、解析
。
4.2、类加载器
(1)BootstrapClassLoader
(启动类加载器) :最顶层的加载类,由C++
实现,负责加载%JAVA_HOME%/lib
目录下的jar
包和类或者或被 -Xbootclasspath
参数指定的路径中的所有类。
(2)ExtensionClassLoader
(扩展类加载器) :主要负责加载目录 %JRE_HOME%/lib/ext
目录下的jar
包和类,或被 java.ext.dirs
系统变量所指定的路径下的jar
包。
(3)AppClassLoader
(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath
下的所有jar包和类。
4.3、双亲委派加载模型
每一个类都有一个对应它的类加载器。系统中的ClassLoder
在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的 loadClass()
处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader
中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader
作为父类加载器。
双亲委派模型保证了Java
程序的稳定运行,可以避免类的重复加载(JVM
区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java
的核心 API
不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object
类。
5、JVM参数