一、类加载子系统
类加载过程
准备(Prepare)
为类变量(static)分配内存并且设置初始值。
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中。
方法区存放的数据:static ,final, class类型信息,常量池(目前理解的维度),方法区在1.8及以后存放于元空间之中,可通过下面程序验证
初始化过程
初始化阶段就是执行类构造器方法clInit()的过程。 clInit是ClassInit缩写。此方法并不是程序员定义的构造方法。
是javac编译器自动收集类中的所有类变量(Static)的赋值动作和静态代码块中的语句合并而来。
构造器方法中指令按语句在源文件中出现的顺序执行
若该类具有父类,jvm会保证子类的clinit()执行前,父类的clinit()已经执行完毕
双亲委派机制:
介绍
Java虚拟机对class文件采用的是按需加载的方式,
也就是说当需要使用该类时才会将它的class文件加载到内存生成的class对象。
而且加载某个类的class文件时,java虚拟机采用的是双亲委派模式。
即把请求交由父类处理,它是一种任务委派模式
双亲委派机制优势
避免类的重复加载
当自己程序中定义了一个和Java.lang包同名的类,此时,由于使用的是双亲委派机制,会由启动类加载器去加载JAVA_HOME/lib中的类,而不是加载用户自定义的类。此时,程序可以正常编译,但是自己定义的类无法被加载运行。
保护程序安全,防止核心API被随意篡改
类加载的时机
关于类加载的时机,《Java虚拟机规范》中并没有明确规定。这点可以由虚拟机的具体实现决定。
但是类的初始化阶段,规范中明确规定当某个类没有进行初始化,只有以下6中情况才会触发其初始化过程。
遇到new,getStatic,putStatic,invokeStatic,这四条字节码指令的时候,如果改类型没有进行初始化,则会触发其初始化。也就是如下情况 1.1. 遇到new关键字进行创建对象的时候。 1.2. 读取或者设置一个类的静态字段的时候(必须被final修饰,也就是在编译器把结果放入常量池中)。 1.3. 调用一个类的静态方法的时候。
使用java.lang.reflect进行反射调用的时候。
当初始化某个类,发现其父类没有初始化的时候。
当虚拟机启动的时候,会触发其主方法所在的类进行初始化。
当使用JDK1.7中的动态语言支持时,如果一个java.lang.invoke.MethidHandle实例最后的解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial四种类型的方法句柄,并且这个句柄对应的类没有被初始化。
当一个接口实现了JDK1.8中的默认方法的时候,如果这个接口的实现类被初始化,则该接口要在其之前进行实例化。
对于以上6中触发类的初始化条件,在JVM规范中有一个很强制的词,if and only if (有且只有)。这六种行为被称为对类进行主动引用,除此之外,其他引用类的方式均不会触发类的初始化。
二、运行时数据区
PC寄存器
PC寄存器是用来存储指向下一条指令的地址,也即将将要执行的指令代码。由执行引擎读取下一条指令。
特征
面试常问
栈
栈中放什么: 8大基本数据类型 + 对象引用 +实例方法
栈与堆的区别
栈的运行原理
变量小结
本地方法栈
存放 native方法
堆
核心概述
一个进程对应一个jvm实例,同时包含多个线程,这些 线程共享方法区和堆,每个线程独有程序计数器、本地方法栈和虚拟机栈。
一个jvm实例只存在一个堆内存,堆也是java内存管理的核心区域
Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间(堆内存的大小是可以调节的)
《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的
所有的线程共享java堆,在这里还可以划分线程私有的缓冲区(TLAB:Thread Local Allocation Buffer).(面试问题:堆空间一定是所有线程共享的么?不是,TLAB线程在堆中独有的)
《Java虚拟机规范》中对java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。
从实际使用的角度看,“几乎”所有的对象的实例都在这里分配内存 (‘几乎’是因为可能存储在栈上)
数组或对象永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置
在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除
堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域
内存结构
对象分配过程
1.参数设置
-XX:NewRatio=x 表示老年代/新生代 默认为2
-XX:SurvivorRatio :设置新生代中伊甸园区/幸存者区。默认值是8
-XX:-UseAdaptiveSizePolicy :关闭自适应的内存分配策略 (暂时用不到)
-Xmn:设置新生代的空间的大小。 (一般不设置)
几乎所有的Java对象都是在Eden区被new出来的
绝大部分的Java对象都销毁在新生代了(IBM公司的专门研究表明,新生代80%的对象都是“朝生夕死”的)
可以使用选项-Xmn设置新生代最大内存大小(这个参数一般使用默认值就好了)
2.过程
针对幸存者s0,s1区:复制之后有交换,谁空谁是to 关于垃圾回收:频繁在新生区收集,很少在养老区收集,几乎不再永久区/元空间收集。
-
new的对象先放伊甸园区。此区有大小限制。
-
当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。将伊甸园中的剩余对象移动到幸存者0区。
-
然后加载新的对象放到伊甸园区
-
如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。
-
如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。
-
啥时候能去养老区呢?可以设置次数。默认是15次。·可以设置参数:-XX:MaxTenuringThreshold=进行设置。
-
在养老区,相对悠闲。当老年区内存不足时,再次触发GC:Major GC,进行养老区的内存清理。
-
若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常
方法区
方法区是一种规范
JDK1.7及之前,用永久代实现,使用虚拟机的内存
JDK1.8及以后,用元数据区实现,使用本地内存
运行时数据结构图
堆、栈、方法区交互关系
方法区的理解
方法区在运行时数据区的理解
方法区存储的信息
方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
类型信息
运行时常量池
静态变量
域(属性)信息
方法信息
JIT代码缓存
类型信息
对每个加载的类型( 类class、接口interface、枚举enum、注解annotation),JVM必 .须在方法区中存储以下类型信息:
①这个类型的完整有效名称(全名=包名.类名)
②这个类型直接父类的完整有效名(对于interface或是java. lang.Object,都没有父类)
③这个类型的修饰符(public, abstract, final的某个子集)
④这个类型直接实现接口的一个有序列表
域信息
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。
域的相关信息包括:域名称、 域类型、域修饰符(public, private, protected, static, final, volatile, transient的某个子集)
方法信息
JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:
方法名称
方法的返回类型(或void)
方法参数的数量和类型(按顺序)
方法的修饰符(public, private, protected, static, final, synchronized, native , abstract的一个子集)
方法的字节码(bytecodes)、操作数栈、局部变量表及大小( abstract和native 方法除外)
异常表( abstract和native方法除外)
每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引
non-final的类变量
静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分
类变量被类的所有实例所共享,即使没有类实例你也可以访问它。
全局常量 static final 在编译的时候就被分配赋值了。
运行时常量池
几种在常量池内存储的数据类型包括:
数量值
字符串值
类引用
字段引用
方法引用
运行时常量池( Runtime Constant Pool)是方法区的一部分。
常量池表(Constant Pool Table)是Class文件的一部分,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
运行时常量池,在加载类和接口到虚拟机后,就会创建对应的运行时常量池。
JVM为每个已加载的类型(类或接口)都维护一个常量池。池中的数据项像数组项一样,是通过索引访问的。
运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了,这里换为真实地址。
运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。
运行时常量池类似于传统编程语言中的符号表(symbol table) ,但是它所包含的数据却比符号表要更加丰富一些。
当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛OutOfMemoryError异常。
方法区进化过程
永久代为什么要被元空间替换
随着Java8的到来,HotSpot VM中再也见不到永久代了。但是这并不意味着类.的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间( Metaspace )。
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。
这项改动是很有必要的,原因有:
1)为永久代设置空间大小是很难确定的。
在某些场景下,如果动态加载类过多,容易产生Perm区的O0M。
比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。 “Exception in thread’ dubbo client x.x connector’java.lang.OutOfMemoryError: PermGenspace”
而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。
2)对永久代进行调优是很困难的。。
常见面试
百度
三面:说一下JVM内存模型吧,有哪些区?分别干什么的?
蚂蚁金服:
Java8的内存分代改进 JVM内存分哪几个区,每个区的作用是什么? 一面: JVM内存分布/内存结构?栈和堆的区别?堆的结构?为什么两个survivor区? 二面: Eden和Survivor的比例分配
小米:
jvm内存分区,为什么要有新生代和老年代
字节跳动:
二面: Java的内存分区 二面:讲讲jvm运行时数据库区 什么时候对象会进入老年代?
京东:
JVM的内存结构,Eden和Survivor比例 。 JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。
天猫:
一面: Jvm内存模型以及分区,需要详细到每个区放什么。 一面: JVM的内存模型,Java8做了什么修改
拼多多:
JVM内存分哪几个区,每个区的作用是什么?
三、执行引擎
四、垃圾回收机制
概述
自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发
对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,那么这将会是一场灾难,最严重的就会弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力。
此时,了 解JVM的自动内存分配和内存回收原理就显得非常重要,只有在真正了解JVM是如何管理内存后,我们才能够在遇见OutOfMemoryError时, 快速地根据错误异常日志定位问题和解决问题。
当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。
垃圾回收器可以对年轻代回收,也可以对老年代回收,甚至是全堆和方法区的回收。
其中,Java堆是垃圾收集器的工作重点。
从次数上讲:
频繁收集Young区
较少收集0ld区
基本不动Perm区(方法区)
判断对象是否存活方法
引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一 条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
清除阶段
分代收集算法
前面所有这些算法中,并没有一种算法可以完全替代其他算法,
它们都具有自己独特的优势和特点。分代收集算法应运而生。
分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。
因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,
以提高垃圾回收的效率。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,
比如Http请求中的Session对象、线程、Socket连接, 这类对象跟业务直接挂钩,因此生命周期比较长
但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如: String对象, 由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
目前几乎所有的GC都是采用分代收集(Generational Collecting) 算法执行垃圾回收的。
在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。
年轻代(Young Gen)
年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。·
老年代(Tenured Gen)
老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记清除或者是标记整理的混合实现。
➢标记阶段的开销与存活对象的数量成正比。
➢清除阶段的开销与所管理区域的大小成正相关。
➢压缩阶段的开销与存活对象的数据成正比。
以HotSpot中的CMS回收器为例,CMS是基于标记清除实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于标记压缩算法的Serialold回收器作为补偿措施:当内存回收不佳(碎片导致的执行失败时),将采用Serial 0ld执行Full GC(标记整理算法)以达到对老年代内存的整理。 分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。
内存溢出和内存泄漏
内存溢出
内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。
由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现O0M的情况。
大多数情况下,GC会进行各种年龄段的垃圾回收,实在不行了就放大招,来一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用。
javadoc中对OutOfMemoryError的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。
首先说没有空闲内存的情况:说明Java虚拟机的堆内存不够。原因有二:
(1) Java虚拟机的堆内存设置不够。 比如:可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小。我们可以通过参数一Xms、一Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)对于老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现OutOfMemoryError也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似intern字符串缓存占用太多空间,也会导致0OM问题。对应的异常信息,会标记出来和永久代相关: “java. lang. OutOfMemoryError: PermGen space”。 随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的00M有所改观,出现00M,异常信息则变成了:“java. lang. OutOfMemoryError: Metaspace"。 直接内存不足,也会导致0OM。
这里面隐含着一层意思是,在抛出0utOfMemoryError之 前,通常垃圾收集器会被触发,尽其所能去清理出空间。
➢例如:在引用机制分析中,涉及到JVM会去尝试回收软引用指向的对象等。
➢在java.nio.BIts.reserveMemory()方法中,我们能清楚的看到,System.gc()会被调用,以清理空间。
当然,也不是在任何情况下垃圾收集器都会被触发的
➢比如,我们去分配一一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接拋出OutOfMemoryError
内存泄漏(Memory Leak)
也称作“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。
但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致内存溢出0OM,也可以叫做宽泛意义上的“内存泄漏
尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现0utOfMemory异常,导致程序崩溃。
注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
举例
1、单例模式 单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
2、一些提供close的资源未关闭导致内存泄漏 数据库连接( dataSourse. getConnection()),网络连接(socket)和io连接必须手动close,否则是不能被回收的。
五、JPforfiler的使用
- idea 安装插件JPforfiler
- 下载Jpforfiler客户端
- 配置idea
4.配置虚拟机内存和打印信息
5. 验证
7