巩固基础,砥砺前行 。
只有不断重复,才能做到超越自己。
能坚持把简单的事情做到极致,也是不容易的。
JVM 类加载机制
JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程
加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class 对象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个Class 文件获取,这里既可以从 ZIP 包中读取(比如从jar 包和 war 包中读取),也可以在运行时计算生成(动态代理), 也可以由其它文件生成(比如将 JSP 文件转换成对应的Class 类)
验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程
符号引用
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的Class 文件格式中
直接引用
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在
初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM 主导。到了初始阶段,才开始真正执行类中定义的Java 程序代码
类构造器
初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证子方法执行之前,父类的方法已经执行完毕,如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。
注意以下几种情况不会执行类初始化:
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 定义对象数组,不会触发该类的初始化
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类
- 通过类名获取Class 对象,不会触发类的初始化。
- 通过Class.forName 加载指定类时,如果指定参数 initialize 为false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
通过ClassLoader 默认的loadClass 方法,也不会触发初始化动作。
虚拟机类加载机制(类加载过程)
加载过程
接下来我们详细讲解一下加我群里一中类加载的全过程,也就是加载,验证,准备解析和初始化这五个阶段所执行的具体动作。
加载
加载是类加载过程的一个阶段,希望读者没有混淆。这两个看起来好像是的名词,在家找阶段虚拟机需要完成下面三件事。
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个代表这个类。的对象作为方法区这个类的各种数据的访问入口
虚拟机规范的这三点要求其实并不算具体,因此虚拟机实现与具体应用的灵活度。都是相当大的,例如通过一个类的全限定名来。获取定义此类的二进制字节流这条它并没有指定二进制字节流。一定要从一个class文件中获取,准确的说是根本没有指定从哪里获取,怎样获取。虚拟机设计团队在加载阶段。搭建了一个相当开放广阔的舞台,Java发展历程中,充分创造力的开发人员则在这个舞台上玩出了各种花样。举足轻重的Java技术都建立在这一基础之上,例如,
1)从zip包中读取这种很常见,最终日后的炸ear。war格式的基础
2)从网络中获取这种场景,最典型的就是applied
3)运行时计算生成这种场景使用的最多的就是动态代理技术。在java.lang.reflect.proxy中,就是使用了Process generator店generator proxy class。来为特定接口生成显示为$ proxy代理类的二进制字节流。
4)有其他文件产生。典型的应用就是JSP应用,既有这次文件上传对应的class类。
5)从数据库中读取这种场景相对少见,例如有些中间件服务器。可以选择把应用程序安装到数据库中来完成程序代码。在集群间的发放。
相当于类加载过程的其他阶段,一个非数组类的加载阶段,准确的说是加载阶段中获取类的二进制。自己留的动作是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成。开发人员可以通过自定义的类加载器去控制自己留的获取方式。G重写一个类加载器的load class方法。
对数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由加瓦虚拟机直接创建的,但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型。指的是数组,去掉所有维度的类型,最终是要考虑加载器去创建。一个数组类创建过程就要遵循以下原则。
1)如果数组的组件类型是引用类型,那就要递归采用绑结。中定义的加载过程去加载这个组件类型。数组c将在加载该组建类型的类加载器的。那名空间上被标识
2)如果数组的组件类型不是引用类型千瓦虚拟监会把数组c标记为与引导类加载器关联。
3)数组类型的可见心与他的组件类型的可见性一致,如果组件类型不是引用类型那出组的。类的可见性被默认为public。
加载阶段完成好后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。方法句中的数据存储格式由虚拟机实现自新定义。胸肌规范,未规定此区域的具体数据结构,然后在内存中实例化一个java.lang.class类的对象。并没有明确规定是在Java堆中。对于hot stop虚拟机而言,Class对象比较特殊,它虽然是对象,但是存储在方法区里面。这个对象将作为程序访问方法区中的这些类型数据的外部接口。加载阶段与链接阶段的部分内容是交叉进行的。加载阶段尚未完成,链接阶段可能已经开始,但这些加载加载阶段之中进行的动作仍然属于链接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。
虚拟机的加载机制(验证)
验证试验阶段的第一步,这一步的目的是为了确保class文件的字节流包含的信息符合当前虚拟机的要求。而且不会危害虚拟机自身的安全
java语言本身是相对安全的语言使用纯粹加瓦语言的代码。无法做到。诸如如何访问数组边界以外的数据,将一个对象转化为它。并为实现的类型跳转到不存在的代码行之类的事情。如果这样做了,边界将拒绝便宜,但前面杰说过,class文件并不一定要求Java源码编译而来。可以使用任何途径产生甚至包含用16进制编辑器直接编写下来的class文件。再次解码语言层面上删除Java代码无法做到的事情是可以实现的,至少遇上。可以表达出来的虚拟机如果不检查输入的字节流,对其完全信任的话,很可能因为载入有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作
验证阶段是非常重要的,这个阶段是否严谨直接决定了交往虚拟机是否传授恶意代码攻击,从执行性能的角度讲讲验证阶段的工作量是虚拟机的类加载此系统中账了相当大的一部分。对这个机制的限制知道还比较笼统的规范中列举了一些class文件格式的静态和结构化约束,如果验证到输入的字节流不符合class文件格式的约束,虚拟机就会抛出一个Java。乱点verify,exception异常或此类异常,但具体应当检查哪些方面,如何检查,核实,检查都没有足够的要求和明确的说明,直到2011年发布的Java虚拟机规范Java se第七版。大幅增加了描述验证过程的篇幅,从不到十页增加到100。30页,这时约束和验证规则才变得具体起来,受篇幅所限。当初无法逐条规则去剪剪,当从整体上去看,验证阶段大致会完成下面四个阶段的检验动作。文件格式检验、元数据检验、字节码检验、符号引用检验。
文件格式检验
第一阶段要验证自己留是否符合class文件格式的规范,并且能被当前版本的虚拟机处理,这一阶段可能包含下面这些验证点。
1.是否以魔数xcafebabe开头
2.主次版本号是否在当前虚拟机处理范围之内。
3.常量值的常量中是否有不被支持的常量类型
4.指向常量的各种索引值中是否有只限不存在的常量或不符合类型的常量
5.Class文件中各个部分及文件本身是否有被删除或附加的其他信息。
实际上,第一阶段的验证点远不止如此,上面只是从hot stop虚拟机。源码中宅出的一小部分内容,该验证阶段的主要目的是保证输入的字节流。蓝正确的解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求这阶段的验证是基于二进制字节流进行的。只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储。所以后面的三个验证阶段全部是基于方法区的存储结构进行的。不会再直接操作字节流
元数据验证
第二阶段是对字节码描述的信息进行语义分析,一保障其描述的信息符合Java。语言规范的要求这个阶段可能包含的验证点如下
1.这个类是否有父类,除了object这个他之外所有的累都有父类。
2.这个类的父类是否继承了不允许被继承的类?被final修饰的类
3.如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
4.类中的字段方法是否与父类产生矛盾,如果覆盖了父类的final字段。或者出现了不符合规则的方法重载,例如方法,参数都一致,当返回类型却不相同等
第二阶段主要的目的是对类的元数据信息进行语义检验保证不存在不符合java语言规范的元数据信息
字节码验证
第三阶段是整个验证过程中最复杂的一个阶段。主要的目的是通过数据流和控制流分析确定程序语义是合法的,符合逻辑的。在第二阶段,对元数据信息中的数据类型做完校验后,这个阶段间对类的方法体进行就业分析保障被救援的类的方法,在运行时不会做出危险虚拟机的安全事件,例如,
1.保证任意时刻操作数栈的数据类型和指令编代码。序列都能配合工作,例如不会出现类似的情况。在操作站放了一个int类型的数据,使用时确按照long来加载到本地变量表中。
2.保证跳转指令不会跳转到方法体以外的字节码的指令上。
3.保障方法体重的类型状况是有效的,例如可以把一个子类对象赋值给父类数据类型。这是安全的,当时把父类对象赋值给此类数据类型,甚至把对象赋值给它,毫无继承关系完全不相干的一个数据类型。这是危险合不合法的
如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的。但如果一个方法体通过了字节码验证,也不能说明其一定就是安全的。即使自己码验证之中存在了大量的检查,也不能保证这一点,这里涉及那离散数学中一个很著名的问题Halting Problem。通俗一点说,通过查数据校验程序逻辑是无法做到绝对准确的,不能通过程序准确的检查出程序是否能在有限时间之内结束运行。
对于数据流验证的高复杂性迅疾设计团队为了避免过多的时间how在自己把验证阶段在这dk1.6之后的加我c编译器和Java虚拟机中进行了一键优化给封话题的code属性。属性表中增加了一下名为stick map table的属性。这项属性描述了方法体中所有的基本块儿。按照流程,拆封的代码块开始本地变量表和操作数栈。亦友的状态再次解绑,验证期间就不需要根据程序推导这个状态的合法性。只需要检查stick maple table属性中的记录是否合法即可。这样直接把验证的类型推导变成了类型检查,从而节约了一些时间。
理论上的stick map table属性也存在错误或被篡改的可能。所以,是否有可能在恶意篡改了code属性的同时,也生成相应的stick map table属性来骗过迅疾的内心。校验则是虚拟机设计者值得思考的问题
在jdk1.6的hot spot报虚拟机提供了-XX: UseSplitverifer选项来关闭此优化或者使用参数。-XX: FailOverToOldVerfier要求在类型校验失败的时候退回到旧的类型,推导方式进行校验。而在这第一个1.7之后,对于主版本大于50的class文件,使用类型检查来完成数据流分析。校验则是唯一的选择不允许再退回到类型推导的方式
符号引用验证
最后一个阶段的校验发生在迅即将符号引用转换为直接引用的时候,这个转化动作间在链接的第三阶段解析阶段中发生。符号引用验证可以看做是对类自身以外的信息进行匹配性。检验。通常需要检验下列的内容
1.符号应用中通常字符串描述的全限地名是否找到对应的类。
2.在指定类中是否存在符号方法的字段描述符以及简单米长所描述的方法和字段。
3.符号引用中的类字段方法的访问c是否可以被当前类访问
符号引用验证的目的是保证解析动作正常执行,如无法通过符号引用验证。那么将会抛出一个异常的子类。
对于虚拟机的类加载机制来说,验证阶段是一个非常重要的,但不是一定必要的阶段,如果所运行的全部代码都已经被反复使用和验证过,那么在实施阶段就可以考虑使用参数-Xverify: none仓鼠来关闭大部分的内件验措施,以缩短虚拟机类加载的时间。
虚拟机类加载机制(准备)
准备阶段是正式为类变量分配内存空间。并设置类变量初始值的阶段,这些变量所使用的内存将在方法区中进行分配,这个阶段中有两个容易产生混淆的概念,要强调一下,首先,这时候进行的内存分配仅包含类变量被static修饰的变量。而不包含实例变量,实例变量将会在对象实例化时随着对象一起分配到甲瓦堆中。其次,这里所说的初始化通常情况下是指数据类型的零值。
上面提到在通常情况下初始值是零值,那么相对的会有一些特殊情况,如果类字段的属性表中存在constant value属性。那么在准备阶段,变量value就会被初始化为constant value属性所指定的值。如,被final修饰。
虚拟机类加载机制(初始化)
虚拟机类加载机制
代码编译的结果从本地机器码转变为字节码是存储格式发展的一小步,却是编程语言发展的一大步。
概述
上一章我们了解了class文件存储格式的具体细节,在class文件中描述的各种信息。最终都需要加载到虚拟机中,之后才能运行和使用,而虚拟机如何加载这些class文件,class文件中的信息进入到虚拟机后会发生什么变化,这些都是本章要进行讲解的内容。
去你鸡巴描述类的数据从class文件加载到内存,并对数据进行校验,转换,解析和初始化最终形成。可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
与那些在编译时需要进行链接工作的语言不同在Java语言里类型的加载。链接和初始化过程都是在程序运行期间完成的,这种策略虽然会令那加载时稍微增加一些性能开销,倒是会为Java应用程序提供。高度的灵活性加我李天生可以动态扩展的语言特性,就是依赖运行期动态加载和动态链接这两个特性实现的。例如,如果编写一个面向接口的应用程序,可以等到运行时在指定企业实际的实现类。用户可以通过Java预定义和自定义类在加载器,让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分。这种组装应用程序的方式目前已经广泛应用于加我程序之中。从最基本的JSP applied要相对复杂的os gi技术。都使用了Java语言运行期内加载的特性。
为了避免语言表达中可能存在偏差,在本章正式开始之前,笔者先设立两个语言上的约定,第一,在实际情况中,每个class文件都有可能代表着Java语言中的一个接口或类。课文中对类的描述都包含了类和接口的可能性,而对类和接口需要分开描述的场景会特别指明,第二,于前面介绍class文件格。而是时约定一致。笔者本站所提到的class文件并非指某个存在于具体磁盘中的文件,这里所说的class文件应当是一串二进制的自己流。无论以任何形式存在都可以。
类的加载时机
内存被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期包括加载,验证,准备解析,初始化,使用和卸载七个阶段。其中验证准备解析。三个部分统称为链接。
加载,验证,准备,初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析阶段则不一定。他在某些情况下可以在初始化之后进行开始,这是为了支持Java语言的运行,是绑定也称为动态绑定。或往期绑定注意,这里笔者写的是按部就班的开始,而不是按部就班的运行或完成,强调这点是因为这些阶段通常都是相互交叉混合进行的,通常会在一个阶段执行的过程中调用激活另外一个阶段。
什么情况下开始类加载过程的第一个阶段?加载千瓦虚拟机规范中并没有进行强制约束,这点可以。作为虚拟机的具体实现来自有把握当做于初始化阶段,虚拟机规范则是严格规定了,有且只有五种情况,必须立即对。类进行初始化,而加载,验证准备自然需要在此之前开始。
1.遇到new get static,put static或Invoke static这四条字节码指令时,如果累没有进行过初始化。则需要先触发及初始化发生这四条指令的最常见的Java代码,团结是使用new关键字实例化对象的时候。读取或设置一个类的静态字段被final修饰,已在编译器把结果放入常量池的静态字段除外。以及调用一个类的静态方法的时候。
2.使用Java.lang.reflect包的方法对类进行反射时调用的时候。如果累没有进行过初始化,则需要相触发及初始化。
3.当初始化一个类的时候,如果发现其父类还没有进行过的初始化。则需要先触发其父类的初始化。
4.当虚拟机启动时,用户需要指定一个执行的主类,包含内方法的那个类。虚拟机会先初始化这个主类
5.当时用JDK七的动态语言支持时,如果一个Java Lang, dear invoke method handler.实力最后的解析结果是Ref,get static ref put static if evoke static的方法句柄.并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
对于这五种住发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语。有且只有这五种场景中的行为称为对一个类进行。主动引用,除此之外,所有引用类的方法都不会触发。初始化被称为被动引用。
JVM之Java中的四种引用类型
在java中一切都是对象,对象的操作是通过对象的引用实现的,java中的引用对象类型有四种:强、软、弱、虚。
- 强:在java中最常见的及时前引用,再把一个对象赋值给一个引用变量时,这个引用变量就是一个强引用。有强引用的对象一定为可达性状态,所以不会被垃圾回收机制回收,因此,强引用就是造成java内存溢出的主要原因。
- 软:阮引用通过softreference类实现,如果一个低下只有软引用,则在系统内存空间不足时该对象将被回收
- 弱:弱引用通过weakreference类实现,如果一个对象只有弱引用,则在垃圾回收过程中一定会被回收
- 虚:虚引用通过phantomreference类实现,虚引用和引用对象联合使用,主要用于追踪对象的垃圾回收状态
JVM之类加载器和双亲委派机制
JVM之类加载器
虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类,JVM 提供了 3 种类加载器
启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类
扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs 系统变量指定路径中的类库
应用程序类加载器(Application ClassLoader):
负责加载用户路径(classpath)上的类库。JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader 实现自定义的类加载器
双亲委派机制
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中, 只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的
Class),子类加载器才会尝试自己去加载。
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。
Java垃圾回收与算法
如何确定垃圾?
java采用引用计数法和可达性分析算法来确定对象是否应该被护手,其中引用激素分容易产生循环依赖的问题,可达性分析算法通过根据搜索算法来实现。根据搜索算法可以一系列gc roots 的点作为起点向下搜索,在一个对象到任何gc roots都没有引用链相连时,说明对象已经死亡,根据搜索算法主要针对栈中的引用,方法区中的静态变量引用和jni中的引用展开分析。
引用计数法
在Java中如果要操作对象,就必须先获得该对象的引用,因此可以通过引用计数法来判断一个对象是否可以被回收。在为对象添加一个引用是,引用计数+1,在为对象删除一个引用时,减少一个引用。如果一个对象的引用计数为0 ,则表示该对象没有被引用,可以被回收。
引用计数法容易产生循环引用的问题,循环引用值两个对象互相引用,导致他们的引用一直存在,而不能回收
可达性分析算法
为了解决引用计数法的循环引用的问题,java还采用了可达性分析算法来判断对象是否可以被回收,具体做法,首先定义一些gc roots 对象,然后以这些对象作为起点向下搜索,如果gcroots 和一个对象直接没有可达路径,则称该对象是不可达的。不可达对象要经过至少两次标记才能判断是否可以被回收,如果在两次标记后该对象仍然不是可达的,则将被垃圾回收期回收。
哪些对象可以作为 gcroots对象呢 ?稍后回复
Java中常用的来及回收算法
Java中常用的垃圾回收算法有标记清除、复制、标记整理、分代回收四种垃圾回收算法
标记清除算法
标记清除算法是基础的垃圾回收算法,其过程分为标记和清理两个阶段,在标记阶段标记所有需要回收的对象,在清除极端可回收的对象并释放所长用的内存空间。
由于标记清除算法实在清理对象锁占用的内存空间后并没有重新整理可用的内存空间,因此如果内存中被护手的小对象过多,则会引起内存碎片化的问题,继而引起大对象无法获得连续可用的内存空间问题
复制算法
复制算法为了解决标记清除算法内存碎片化的问题而设计的,复制算法首先将内存活粉两块大小相等的内存区域,1区域,2区域,新生成的对象都被存放在1区域,在1区域内的对象存储忙后会对内存1进行一次标记,并将标记后仍然存活的对象全部复制到区域2中,然后直接清理1区域的内存。
复制算法的内存清理效率高,并且容易实现,但由于同一时刻只有一个内存区域可用,即可用的内存空间被压缩为原来的一半,因此存在大量的内存空间浪费,同时,在系统中有大量长时间存活的对象是,这些对象将存活在1区域和2区域之间来回复制而影响系统的运行效率。因此该算法只在对象存活时间较短时效率高。
标记整理算法
标记整理算法结合了标记清除算法和复制算法的有点,其标记阶段和标记清除算法的标记阶段仙童,在标记完成后存活的对象复制到内存的一端,然后清除该端的对象并释放内存
分代回收算法
无论是标记清除算法、复制算法、标记整理算法,都无法堆所有类型(长生命手气、短生命周期、大对象、小对象)的对象进行垃圾回收。因此正对不同的对象类型,jvm采用了不同的垃圾回收算法,该算法被称为分代回收算法
分代回收算法根据对象的不同类型将内存划分为不同的区域,jvm将对划分为新生代和老年代,新生代主要存放新生成的对象,其特点是对象数量多但是生命周期短,在每次进行垃圾回收是都有大量的堆被回收,老年代的主要存放大对象和生命走起长的对象,因此可回收的对象相对较少,因此,jvm根据不同的区域的特点选择不同的算法。
目前,大部分jvm在新生代都采用了复制算法,因为在新生代中每次进行垃圾回收时都有大量的 垃圾对象被回收,需要复制的对象(存活的)比较少,不存在大量的对象在内存中被来回复制的问题,因此采用复制算法能安全、高效的回收新生代大量的生命周期的对象并释放内存空间。
jvm将新生代进一步划分为一块大的eden区和两块比较小的Survivor区,Survivor区又分为SurvivorFrom和SurvivorTo区,jvm在运行过程中主要使用eden区和SurvivorFrom区,进行垃圾回收时会将eden区和SurvivorFrom区中存活的对象复制到SurvivorTo区,然后清理eden区和SurvivorFrom区。
老年代主要存放声音周期比较长的对象和大对象,因为每次只有少量的非存活的对象被回收,因而在老年代采用标记清除算法。
在jvm中海油一个区域,即方法区中的永久代,永久代用存储class类、常量、方法描述等,在永久代主要回收废弃的常量和无用的类。
jvm内存中的对象组要被分配到新生代中的eden区和SurvivorFrom区,在少数情况下回直接分配到来年代,在新生代的eden区和SurvivorFrom区的内存空间不足时会触发一次gc,该过程成为minorgc。在minorgc后,eden区和SurvivorFrom区中存活的对象会被复制到SurvivorTo中,然后eden区和SurvivorFrom区中的对象被清理,如果此时在SurvivorTo区无法找到连续的内存空间存储某个对象,则将这个对象直接存储到老年代,若Survivor区的对象经过一次gc之后仍然存活,则其年龄+1,默认情况下,对象在年龄达到15时,将被移动到老年代。
Java中的内存区域
JVM 的内存区域分为私有区域(程序计数器、虚拟机栈、本地方法区)、线程共享区域(对、方法区)和直接内存。
线程私有区域的生命周期与线程仙童,随线程的启动而创建,随着线程的结束而销毁,在JVM内部,每个线程斗鱼操作系统的本地线程直接映射,因此线程私有区域的存在与否和本地线程的启动和销毁对应
线程共享区域随着虚拟机的饿启动而创建,随着关闭而销毁
直接内存也交所对外内存,他并不是JVM运行时的数据区的一部分,但是在并发中被频繁使用,JDK的nio模块提供基于CHannel和Buffer的IO操作就是基于对堆外内存实现的,nio模块通过调用本地方法库直接在操作系统上分配堆内存,然后直接使用DirectByteBuffer对象作为这块内存的引用对内存进行操作,Java进程可以通过对外内存技术避免在Java对和Native中来回复制数据带来的资源兰妃和性能损耗和性能消耗,因此对外内存在搞并发场景下被广泛使用。
程序计数器
程序计数器 线程私有
去内存泄漏的问题,它是一块很小的内存空间,用于存储当前运行的线程锁执行的字节码的行号指示器,每个运行中的线程中都有一个独立的程序计数器,在方法正在执行时,该方法的程序计数器记录的是事实虚拟机字节码指令的地址,如果该方法执行的是本地方法,则程序计数器的值为空
虚拟机栈
线程私有 描述Java方法的执行过程,虚拟机栈是描述Java方法的执行过程的内存模型,提前在当前栈针中存储了局部变量表、动态链接、方法出口等信息。同时,栈针用来存储部分运行时数据及其数据结构,处理动态链接方法的返回值和异常分派。
栈针用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一个与之对应的栈针,方法的执行和返回对应栈针在虚拟机栈中的入栈和出栈,无论方法时正常运行还是异常完成(抛出了在方法内未被捕获的异常),都是为方法运行结束。
本地方法区
本地方法区和虚拟机栈作用类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为本地方法服务
堆
堆,也叫云信使数据区,线程共享。在JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是被线程共享的内存区域,也是垃圾回收器进行垃圾回收的最重要的内存区域。由于现代JVM采用分代回收算法,因此,JVM堆从GC的角度可以细分为新生代、老年代、永久代。
方法区
方法区,线程共享,方法区也成为永久代,用于存储常量、静态变量、类信息、即时编译器编译后的机器码、运行时常量池等数据
JVM吧GC分代手机款张志方法区,及时用Java堆的永久代来实现方法区,这样JVM的垃圾回收期就可以像管理java堆一样管理这部分内存,永久代的内存回收主要正对常量池的回收和类的卸载,以你可回收的对象很少
常量被存储在运行时常量池中,是方法区的一部分,静态病例也属于方法区的一部分,在类信息中不但保存了类的版本、字段、方法、接口等描述信息,还保存了常量信息
在即时编译后,代码的内容将在执行阶段(类加载完成之后)被保存在付费区的运行时常量池中,Java虚拟机堆class文件每一部分的格式都有明确的规定,只有符合jvm规范的class文件才能通过虚拟机的检查,然后被装载、执行。
JVM的运行时区域
JVM的运行时区域也叫作jvm堆,从gc的角度讲jvm分成新生代、老年代、永久代。其中新生代默认占1/3堆内存空间,老年代默认占用2/3堆内存空间,永久代占非常少的堆内存空间,新生代又分为Eden区、ServivorFrom区、ServivorTo区,Eden区默认占用8/10新生代空间,ServivorFrom区和ServivorTo区默认占用1/10新生代空间。
新生代
新生代分为 Eden区、ServivorFrom区、ServivorTo区。JVM新创建的对象(除了大对象)会被存放在新生代,默认占用新生代的1/3空间,由于jvm会频繁创建对象,所以新生代会频繁出发minorGC进行来及回收。
- Eden区 java次年创建的对象首相会被存放在Eden区,如果新创建的对象属于大对象,,则直接分配到老年区,大对象的定义和具体的jvm版本,堆大小和垃圾回收策略有关,一般为2-128K,可通过xx:PretenureSizeThreshoud设置大小,在Eden区的内存空间不足时会触发minorGC,对新生代进行一次垃圾回收。
- ServivorTo区 保留上一次minorGC时的幸存者
- ServivorFrom区 将上一次minorGC的幸存者作为这一次minorGC的被扫描这
新生代的gc过程叫minorGC,采用复制算法实现
- 把Eden区和ServivorFrom区中村活的对象复制到ServivorTo区。如果某对像的年龄达到 老年代的标准(对象晋升老念叨的标准由XX:maxTenuringThreshold设置,默认是15),将其复制到老年代,同事吧这些对象的年龄+1,如果ServivorTo区的内存空间不够,则也直接将其复制到老年代;如果对象属于大对象(2-128k的对象属于大对象),则也直接将其复制到老年代
- 清空Eden区和ServivorFrom区中的对象
- 将ServivorTo区和ServivorFrom区互换,原来ServivorTo区成为下一次GC的ServivorFrom区。
老年代
老对象主要存放长生命周期的对象和大对象,老年代的gc成为majorGC,在老年代,对象比较稳定,majorGC不会被频繁出发,在进行majorGC前,jvm会进行一次minorGC,在minorGC后人人出现老遍地当且仅当老年代空间不足或者无法找到足够大的连续内存空间分配给新创建的大对象是,会触发majorGC进行垃圾回收,释放jvm的内存空间
majorGC采用标记清楚算法,该算法首先会扫描所有对象并标记存活的对象,然后挥手未被标记的对象,释放内存空间
因为先要扫描老年代的所有对象再回收,所以majorGC的耗时比较长,majorGC的标记清楚算法容易产生内存碎片,在老年代没还有足够存储空间可分配是,会抛出oom异常
永久代
永久代值内存的永久保存区域,主要存放class和meta的信息,class在类加载时被放入到永久代,永久代和老年代、新生代不同,过程不会再程序运行过程中对永久代的内存进行清理,这也导致永久代的内存会随着加载class文件的增加而增加,在加载的class文件过多时会抛出oom异常,比如Tomcat引用jar文件过多导致jvm内存不足而无法启动
需要注意的是,在java8中永久代已经被元数据区(元空间)取代,元数据区的作用和永久代类似,二者最大的区别在于:元数据并没有使用虚拟机的内存,而是使用直接内存,因此元空间大小不受jvm内存的限制,主要和操作系统的内存有关
在java8中,jvm将类的元数据放在本地内存中,将常量池和类的静态变量放入到java堆中,这样jvm能够加载多少元数据信息就不再有jvm的最大可用内存空间决定,而是由操作系统的实际可用内存空间决定。