巩固基础 , 砥砺前行。
只有不断重复 , 才能做到超越自己 。
能坚持把简单的事情做到极致 , 也是不容易的 。
JVM 类加载机制
JVM 类加载机制分为五个部分:加载 , 验证 , 准备 , 解析 , 初始化 , 下面我们就分别来看一下这五个过程
加载
加载是类加载过程中的一个阶段 , 这个阶段会在内存中生成一个代表这个类的java.lang.Class 对象 , 作为方法区这个类的各种数据的入口 。注意这里不一定非得要从一个Class 文件获取 , 这里既可以从 ZIP 包中读取(比如从jar 包和 war 包中读取) , 也可以在运行时计算生成(动态代理) , 也可以由其它文件生成(比如将 JSP 文件转换成对应的Class 类)
验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求 , 并且不会危害虚拟机自身的安全
准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段 , 即在方法区中分配这些变量所使用的内存空间
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程
符号引用
符号引用与虚拟机实现的布局无关 , 引用的目标并不一定要已经加载到内存中 。各种虚拟机实现的内存布局可以各不相同 , 但是它们能接受的符号引用必须是一致的 , 因为符号引用的字面量形式明确定义在 Java 虚拟机规范的Class 文件格式中
直接引用
直接引用可以是指向目标的指针 , 相对偏移量或是一个能间接定位到目标的句柄 。如果有了直接引用 , 那引用的目标必定已经在内存中存在
初始化
初始化阶段是类加载最后一个阶段 , 前面的类加载阶段之后 , 除了在加载阶段可以自定义类加载器以外 , 其它操作都由JVM 主导 。到了初始阶段 , 才开始真正执行类中定义的Java 程序代码
类构造器
初始化阶段是执行类构造器方法的过程 。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的 。虚拟机会保证子方法执行之前 , 父类的方法已经执行完毕 , 如果一个类中没有对静态变量赋值也没有静态语句块 , 那么编译器可以不为这个类生成()方法 。
注意以下几种情况不会执行类初始化:
通过子类引用父类的静态字段 , 只会触发父类的初始化 , 而不会触发子类的初始化 。定义对象数组 , 不会触发该类的初始化常量在编译期间会存入调用类的常量池中 , 本质上并没有直接引用定义常量的类 , 不会触发定义常量所在的类通过类名获取Class 对象 , 不会触发类的初始化 。通过Class. 加载指定类时 , 如果指定参数为false 时 , 也不会触发类初始化 , 其实这个参数是告诉虚拟机 , 是否要对类进行初始化 。
通过 默认的 方法 , 也不会触发初始化动作 。虚拟机类加载机制(类加载过程) 加载过程
接下来我们详细讲解一下加我群里一中类加载的全过程 , 也就是加载 , 验证 , 准备解析和初始化这五个阶段所执行的具体动作 。
加载
加载是类加载过程的一个阶段 , 希望读者没有混淆 。这两个看起来好像是的名词 , 在家找阶段虚拟机需要完成下面三件事 。
1.通过一个类的全限定名来获取定义此类的二进制字节流 。
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 。
3.在内存中生成一个代表这个类 。的对象作为方法区这个类的各种数据的访问入口
虚拟机规范的这三点要求其实并不算具体 , 因此虚拟机实现与具体应用的灵活度 。都是相当大的 , 例如通过一个类的全限定名来 。获取定义此类的二进制字节流这条它并没有指定二进制字节流 。一定要从一个class文件中获取 , 准确的说是根本没有指定从哪里获取 , 怎样获取 。虚拟机设计团队在加载阶段 。搭建了一个相当开放广阔的舞台 , Java发展历程中 , 充分创造力的开发人员则在这个舞台上玩出了各种花样 。举足轻重的Java技术都建立在这一基础之上 , 例如 ,
1)从zip包中读取这种很常见 , 最终日后的炸ear 。war格式的基础
2)从网络中获取这种场景 , 最典型的就是
3)运行时计算生成这种场景使用的最多的就是动态代理技术 。在java.lang..proxy中 , 就是使用了 店 proxy class 。来为特定接口生成显示为$ proxy代理类的二进制字节流 。
4)有其他文件产生 。典型的应用就是JSP应用 , 既有这次文件上传对应的class类 。
5)从数据库中读取这种场景相对少见 , 例如有些中间件服务器 。可以选择把应用程序安装到数据库中来完成程序代码 。在集群间的发放 。
相当于类加载过程的其他阶段 , 一个非数组类的加载阶段 , 准确的说是加载阶段中获取类的二进制 。自己留的动作是开发人员可控性最强的 , 因为加载阶段既可以使用系统提供的引导类加载器来完成 , 也可以由用户自定义的类加载器去完成 。开发人员可以通过自定义的类加载器去控制自己留的获取方式 。G重写一个类加载器的load class方法 。
对数组类而言 , 情况就有所不同 , 数组类本身不通过类加载器创建 , 它是由加瓦虚拟机直接创建的 , 但数组类与类加载器仍然有很密切的关系 , 因为数组类的元素类型 。指的是数组 , 去掉所有维度的类型 , 最终是要考虑加载器去创建 。一个数组类创建过程就要遵循以下原则 。
1)如果数组的组件类型是引用类型 , 那就要递归采用绑结 。中定义的加载过程去加载这个组件类型 。数组c将在加载该组建类型的类加载器的 。那名空间上被标识
2)如果数组的组件类型不是引用类型千瓦虚拟监会把数组c标记为与引导类加载器关联 。
3)数组类型的可见心与他的组件类型的可见性一致 , 如果组件类型不是引用类型那出组的 。类的可见性被默认为 。
加载阶段完成好后 , 虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中 。方法句中的数据存储格式由虚拟机实现自新定义 。胸肌规范 , 未规定此区域的具体数据结构 , 然后在内存中实例化一个java.lang.class类的对象 。并没有明确规定是在Java堆中 。对于hot stop虚拟机而言 , Class对象比较特殊 , 它虽然是对象 , 但是存储在方法区里面 。这个对象将作为程序访问方法区中的这些类型数据的外部接口 。加载阶段与链接阶段的部分内容是交叉进行的 。加载阶段尚未完成 , 链接阶段可能已经开始 , 但这些加载加载阶段之中进行的动作仍然属于链接阶段的内容 , 这两个阶段的开始时间仍然保持着固定的先后顺序 。
虚拟机的加载机制(验证)
验证试验阶段的第一步 , 这一步的目的是为了确保class文件的字节流包含的信息符合当前虚拟机的要求 。而且不会危害虚拟机自身的安全
java语言本身是相对安全的语言使用纯粹加瓦语言的代码 。无法做到 。诸如如何访问数组边界以外的数据 , 将一个对象转化为它 。并为实现的类型跳转到不存在的代码行之类的事情 。如果这样做了 , 边界将拒绝便宜 , 但前面杰说过 , class文件并不一定要求Java源码编译而来 。可以使用任何途径产生甚至包含用16进制编辑器直接编写下来的class文件 。再次解码语言层面上删除Java代码无法做到的事情是可以实现的 , 至少遇上 。可以表达出来的虚拟机如果不检查输入的字节流 , 对其完全信任的话 , 很可能因为载入有害的字节流而导致系统崩溃 , 所以验证是虚拟机对自身保护的一项重要工作
验证阶段是非常重要的 , 这个阶段是否严谨直接决定了交往虚拟机是否传授恶意代码攻击 , 从执行性能的角度讲讲验证阶段的工作量是虚拟机的类加载此系统中账了相当大的一部分 。对这个机制的限制知道还比较笼统的规范中列举了一些class文件格式的静态和结构化约束 , 如果验证到输入的字节流不符合class文件格式的约束 , 虚拟机就会抛出一个Java 。乱点 , 异常或此类异常 , 但具体应当检查哪些方面 , 如何检查 , 核实 , 检查都没有足够的要求和明确的说明 , 直到2011年发布的Java虚拟机规范Java se第七版 。大幅增加了描述验证过程的篇幅 , 从不到十页增加到100 。30页 , 这时约束和验证规则才变得具体起来 , 受篇幅所限 。当初无法逐条规则去剪剪 , 当从整体上去看 , 验证阶段大致会完成下面四个阶段的检验动作 。文件格式检验、元数据检验、字节码检验、符号引用检验 。
文件格式检验
第一阶段要验证自己留是否符合class文件格式的规范 , 并且能被当前版本的虚拟机处理 , 这一阶段可能包含下面这些验证点 。
1.是否以魔数开头
2.主次版本号是否在当前虚拟机处理范围之内 。
3.常量值的常量中是否有不被支持的常量类型
4.指向常量的各种索引值中是否有只限不存在的常量或不符合类型的常量
5.Class文件中各个部分及文件本身是否有被删除或附加的其他信息 。
实际上 , 第一阶段的验证点远不止如此 , 上面只是从hot stop虚拟机 。源码中宅出的一小部分内容 , 该验证阶段的主要目的是保证输入的字节流 。蓝正确的解析并存储于方法区之内 , 格式上符合描述一个Java类型信息的要求这阶段的验证是基于二进制字节流进行的 。只有通过了这个阶段的验证后 , 字节流才会进入内存的方法区中进行存储 。所以后面的三个验证阶段全部是基于方法区的存储结构进行的 。不会再直接操作字节流
元数据验证
第二阶段是对字节码描述的信息进行语义分析 , 一保障其描述的信息符合Java 。语言规范的要求这个阶段可能包含的验证点如下
1.这个类是否有父类 , 除了这个他之外所有的累都有父类 。
2.这个类的父类是否继承了不允许被继承的类?被final修饰的类
3.如果这个类不是抽象类 , 是否实现了其父类或接口之中要求实现的所有方法
4.类中的字段方法是否与父类产生矛盾 , 如果覆盖了父类的final字段 。或者出现了不符合规则的方法重载 , 例如方法 , 参数都一致 , 当返回类型却不相同等
第二阶段主要的目的是对类的元数据信息进行语义检验保证不存在不符合java语言规范的元数据信息
字节码验证
第三阶段是整个验证过程中最复杂的一个阶段 。主要的目的是通过数据流和控制流分析确定程序语义是合法的 , 符合逻辑的 。在第二阶段 , 对元数据信息中的数据类型做完校验后 , 这个阶段间对类的方法体进行就业分析保障被救援的类的方法 , 在运行时不会做出危险虚拟机的安全事件 , 例如 ,
1.保证任意时刻操作数栈的数据类型和指令编代码 。序列都能配合工作 , 例如不会出现类似的情况 。在操作站放了一个int类型的数据 , 使用时确按照long来加载到本地变量表中 。
2.保证跳转指令不会跳转到方法体以外的字节码的指令上 。
3.保障方法体重的类型状况是有效的 , 例如可以把一个子类对象赋值给父类数据类型 。这是安全的 , 当时把父类对象赋值给此类数据类型 , 甚至把对象赋值给它 , 毫无继承关系完全不相干的一个数据类型 。这是危险合不合法的
如果一个类方法体的字节码没有通过字节码验证 , 那肯定是有问题的 。但如果一个方法体通过了字节码验证 , 也不能说明其一定就是安全的 。即使自己码验证之中存在了大量的检查 , 也不能保证这一点 , 这里涉及那离散数学中一个很著名的问题。通俗一点说 , 通过查数据校验程序逻辑是无法做到绝对准确的 , 不能通过程序准确的检查出程序是否能在有限时间之内结束运行 。
对于数据流验证的高复杂性迅疾设计团队为了避免过多的时间how在自己把验证阶段在这dk1.6之后的加我c编译器和Java虚拟机中进行了一键优化给封话题的code属性 。属性表中增加了一下名为stick map table的属性 。这项属性描述了方法体中所有的基本块儿 。按照流程 , 拆封的代码块开始本地变量表和操作数栈 。亦友的状态再次解绑 , 验证期间就不需要根据程序推导这个状态的合法性 。只需要检查stick maple table属性中的记录是否合法即可 。这样直接把验证的类型推导变成了类型检查 , 从而节约了一些时间 。
理论上的stick map table属性也存在错误或被篡改的可能 。所以 , 是否有可能在恶意篡改了code属性的同时 , 也生成相应的stick map table属性来骗过迅疾的内心 。校验则是虚拟机设计者值得思考的问题
在jdk1.6的hot spot报虚拟机提供了-XX: 选项来关闭此优化或者使用参数 。-XX: 要求在类型校验失败的时候退回到旧的类型 , 推导方式进行校验 。而在这第一个1.7之后 , 对于主版本大于50的class文件 , 使用类型检查来完成数据流分析 。校验则是唯一的选择不允许再退回到类型推导的方式
符号引用验证
最后一个阶段的校验发生在迅即将符号引用转换为直接引用的时候 , 这个转化动作间在链接的第三阶段解析阶段中发生 。符号引用验证可以看做是对类自身以外的信息进行匹配性 。检验 。通常需要检验下列的内容
1.符号应用中通常字符串描述的全限地名是否找到对应的类 。
2.在指定类中是否存在符号方法的字段描述符以及简单米长所描述的方法和字段 。
3.符号引用中的类字段方法的访问c是否可以被当前类访问
符号引用验证的目的是保证解析动作正常执行 , 如无法通过符号引用验证 。那么将会抛出一个异常的子类 。
对于虚拟机的类加载机制来说 , 验证阶段是一个非常重要的 , 但不是一定必要的阶段 , 如果所运行的全部代码都已经被反复使用和验证过 , 那么在实施阶段就可以考虑使用参数-: none仓鼠来关闭大部分的内件验措施 , 以缩短虚拟机类加载的时间 。
虚拟机类加载机制(准备)
准备阶段是正式为类变量分配内存空间 。并设置类变量初始值的阶段 , 这些变量所使用的内存将在方法区中进行分配 , 这个阶段中有两个容易产生混淆的概念 , 要强调一下 , 首先 , 这时候进行的内存分配仅包含类变量被修饰的变量 。而不包含实例变量 , 实例变量将会在对象实例化时随着对象一起分配到甲瓦堆中 。其次 , 这里所说的初始化通常情况下是指数据类型的零值 。
上面提到在通常情况下初始值是零值 , 那么相对的会有一些特殊情况 , 如果类字段的属性表中存在 value属性 。那么在准备阶段 , 变量value就会被初始化为 value属性所指定的值 。如 , 被final修饰 。
虚拟机类加载机制(初始化) 虚拟机类加载机制
代码编译的结果从本地机器码转变为字节码是存储格式发展的一小步 , 却是编程语言发展的一大步 。
概述
上一章我们了解了class文件存储格式的具体细节 , 在class文件中描述的各种信息 。最终都需要加载到虚拟机中 , 之后才能运行和使用 , 而虚拟机如何加载这些class文件 , class文件中的信息进入到虚拟机后会发生什么变化 , 这些都是本章要进行讲解的内容 。
去你鸡巴描述类的数据从class文件加载到内存 , 并对数据进行校验 , 转换 , 解析和初始化最终形成 。可以被虚拟机直接使用的Java类型 , 这就是虚拟机的类加载机制 。
与那些在编译时需要进行链接工作的语言不同在Java语言里类型的加载 。链接和初始化过程都是在程序运行期间完成的 , 这种策略虽然会令那加载时稍微增加一些性能开销 , 倒是会为Java应用程序提供 。高度的灵活性加我李天生可以动态扩展的语言特性 , 就是依赖运行期动态加载和动态链接这两个特性实现的 。例如 , 如果编写一个面向接口的应用程序 , 可以等到运行时在指定企业实际的实现类 。用户可以通过Java预定义和自定义类在加载器 , 让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分 。这种组装应用程序的方式目前已经广泛应用于加我程序之中 。从最基本的JSP 要相对复杂的os gi技术 。都使用了Java语言运行期内加载的特性 。
为了避免语言表达中可能存在偏差 , 在本章正式开始之前 , 笔者先设立两个语言上的约定 , 第一 , 在实际情况中 , 每个class文件都有可能代表着Java语言中的一个接口或类 。课文中对类的描述都包含了类和接口的可能性 , 而对类和接口需要分开描述的场景会特别指明 , 第二 , 于前面介绍class文件格 。而是时约定一致 。笔者本站所提到的class文件并非指某个存在于具体磁盘中的文件 , 这里所说的class文件应当是一串二进制的自己流 。无论以任何形式存在都可以 。
类的加载时机
内存被加载到虚拟机内存中开始到卸载出内存为止 , 它的整个生命周期包括加载 , 验证 , 准备解析 , 初始化 , 使用和卸载七个阶段 。其中验证准备解析 。三个部分统称为链接 。
加载 , 验证 , 准备 , 初始化和卸载这五个阶段的顺序是确定的 , 类的加载过程必须按照这种顺序按部就班的开始 , 而解析阶段则不一定 。他在某些情况下可以在初始化之后进行开始 , 这是为了支持Java语言的运行 , 是绑定也称为动态绑定 。或往期绑定注意 , 这里笔者写的是按部就班的开始 , 而不是按部就班的运行或完成 , 强调这点是因为这些阶段通常都是相互交叉混合进行的 , 通常会在一个阶段执行的过程中调用激活另外一个阶段 。
什么情况下开始类加载过程的第一个阶段?加载千瓦虚拟机规范中并没有进行强制约束 , 这点可以 。作为虚拟机的具体实现来自有把握当做于初始化阶段 , 虚拟机规范则是严格规定了 , 有且只有五种情况 , 必须立即对 。类进行初始化 , 而加载 , 验证准备自然需要在此之前开始 。
1.遇到new get , put 或 这四条字节码指令时 , 如果累没有进行过初始化 。则需要先触发及初始化发生这四条指令的最常见的Java代码 , 团结是使用new关键字实例化对象的时候 。读取或设置一个类的静态字段被final修饰 , 已在编译器把结果放入常量池的静态字段除外 。以及调用一个类的静态方法的时候 。
2.使用Java.lang.包的方法对类进行反射时调用的时候 。如果累没有进行过初始化 , 则需要相触发及初始化 。
3.当初始化一个类的时候 , 如果发现其父类还没有进行过的初始化 。则需要先触发其父类的初始化 。
4.当虚拟机启动时 , 用户需要指定一个执行的主类 , 包含内方法的那个类 。虚拟机会先初始化这个主类
5.当时用JDK七的动态语言支持时 , 如果一个Java Lang, dear.实力最后的解析结果是Ref , getref putif evoke 的方法句柄.并且这个方法句柄所对应的类没有进行过初始化 , 则需要先触发其初始化 。
对于这五种住发类进行初始化的场景 , 虚拟机规范中使用了一个很强烈的限定语 。有且只有这五种场景中的行为称为对一个类进行 。主动引用 , 除此之外 , 所有引用类的方法都不会触发 。初始化被称为被动引用 。
JVM之Java中的四种引用类型
在java中一切都是对象 , 对象的操作是通过对象的引用实现的 , java中的引用对象类型有四种:强、软、弱、虚 。
强:在java中最常见的及时前引用 , 再把一个对象赋值给一个引用变量时 , 这个引用变量就是一个强引用 。有强引用的对象一定为可达性状态 , 所以不会被垃圾回收机制回收 , 因此 , 强引用就是造成java内存溢出的主要原因 。软:阮引用通过类实现 , 如果一个低下只有软引用 , 则在系统内存空间不足时该对象将被回收弱:弱引用通过类实现 , 如果一个对象只有弱引用 , 则在垃圾回收过程中一定会被回收虚:虚引用通过类实现 , 虚引用和引用对象联合使用 , 主要用于追踪对象的垃圾回收状态 Java中有哪些类加载器
JDK自带有三个类加载器:
. 、
.
·是的父类加载器 , 默认负责加载%%lib下的jar包和class文件 。·是的父类加载器 , 负责加载%%/lib/ext文件夹下的jar包和class类 。·是自定义类加载器的父类 , 负责加载下的类文件 。
说说类加载器双亲委派模型
NVM中存在三个默认的类加载器:
1.
2.
3.
的父加载器是,的父加载器是 。
JVM在加载一个类时 , 会调用的方法来加载这个类 , 不过在这个方法中 , 会先使用的方法来加载类 , 同样的方法中会先使用来加载类 , 如果加载到了就直接成功 , 如果没有加载到 , 那么就会自己尝试加载该类 , 如果没有加载到 , 那么则会由来加载这个类 。
所以 , 双亲委派指得是 , JVM在加载类时 , 会委派给Ext和进行加载 , 如果没加载到才由自己进行加载 。
JVM之类加载器和双亲委派机制 JVM之类加载器
虚拟机设计团队把加载动作放到 JVM 外部实现 , 以便让应用程序决定如何获取所需的类 , JVM 提供了 3 种类加载器
启动类加载器( )
负责加载 \lib 目录中的 , 或通过- 参数指定路径中的 , 且被虚拟机认可(按文件名识别 , 如 rt.jar)的类
扩展类加载器( )
负责加载 \lib\ext 目录中的 , 或通过java.ext.dirs 系统变量指定路径中的类库
应用程序类加载器( ):
负责加载用户路径()上的类库 。JVM 通过双亲委派模型进行类的加载 , 当然我们也可以通过继承java.lang. 实现自定义的类加载器
双亲委派机制
当一个类收到了类加载请求 , 他首先不会尝试自己去加载这个类 , 而是把这个请求委派给父类去完成 , 每一个层次类加载器都是如此 , 因此所有的加载请求都应该传送到启动类加载其中 , 只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的
Class) , 子类加载器才会尝试自己去加载 。
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang. , 不管是哪个加载器加载这个类 , 最终都是委托给顶层的启动类加载器进行加载 , 这样就保证了使用不同的类加载器最终得到的都是同样一个对象 。
Java垃圾回收与算法
如何确定垃圾?
java采用引用计数法和可达性分析算法来确定对象是否应该被护手 , 其中引用激素分容易产生循环依赖的问题 , 可达性分析算法通过根据搜索算法来实现 。根据搜索算法可以一系列gc roots 的点作为起点向下搜索 , 在一个对象到任何gc roots都没有引用链相连时 , 说明对象已经死亡 , 根据搜索算法主要针对栈中的引用 , 方法区中的静态变量引用和jni中的引用展开分析 。
引用计数法
在Java中如果要操作对象 , 就必须先获得该对象的引用 , 因此可以通过引用计数法来判断一个对象是否可以被回收 。在为对象添加一个引用是 , 引用计数+1 , 在为对象删除一个引用时 , 减少一个引用 。如果一个对象的引用计数为0 , 则表示该对象没有被引用 , 可以被回收 。
引用计数法容易产生循环引用的问题 , 循环引用值两个对象互相引用 , 导致他们的引用一直存在 , 而不能回收
可达性分析算法
为了解决引用计数法的循环引用的问题 , java还采用了可达性分析算法来判断对象是否可以被回收 , 具体做法 , 首先定义一些gc roots 对象 , 然后以这些对象作为起点向下搜索 , 如果 和一个对象直接没有可达路径 , 则称该对象是不可达的 。不可达对象要经过至少两次标记才能判断是否可以被回收 , 如果在两次标记后该对象仍然不是可达的 , 则将被垃圾回收期回收 。
哪些对象可以作为 对象呢 ?稍后回复
JVM有哪些垃圾回收算法?
1.标记清除算法:
a.标记阶段:把控圾内存标记出来
b.清除阶段:直接将垃圾内存回收 。
c.这种算法是比较简单的 , 但是有个很严重的问题 , 就是会产生大量的内存碎片 。
2.复制算法:为了解决标记清除算法的内存碎片问题 , 就产生了复制算法 。复制算法将内存分为大小相等的两半 , 每次只使用其中一半 。垃圾回收时 , 将当前这一块的存活对象全部拷贝到另一半 , 然后当前这一半内存就可以直接清除 。这种算法没有内存碎片 , 但是他的问题就在于浪费空间 。而且 , 他的效率跟存活对象的个数有关 。3.标记压缩算法:为了解决复制算法的缺陷 , 就提出了标记压缩算法 。这种算法在标记阶段跟标记清除算法是一样的 , 但是在完成标记之后 , 不是直接清理垃圾内存 , 而是将存活对象往一端移动 , 然后将边界以外的所有内存直接清除 。
什么是STW?
STW:Stop-The-World , 是在垃圾回收算法执行过程当中 , 需要将JM内存冻结的一种状态 。在STW状态下 , JAVA的所有线程都是停止执行的-GC线程除外 , 方法可以
执行 , 但是 , 不能与JVM交互 。GC各种算法优化的重点 , 就是减少STW , 同时这也是JVM调优的重点 。
JVM参数有哪些?
JVM参数大致可以分为三类:
1.标注指令:-开头 , 这些是所有的都支持的参数 。可以用java-help打印出来 。
2.非标准指令:-X开头 , 这些指令通常是跟特定的版本对应的 。可以用java-X打印出来 。
3.不稳定参数:-×X开头 , 这一类参数是跟特定版本对应的 , 并且变化非常大 。
Java中常用的来及回收算法
Java中常用的垃圾回收算法有标记清除、复制、标记整理、分代回收四种垃圾回收算法
标记清除算法
标记清除算法是基础的垃圾回收算法 , 其过程分为标记和清理两个阶段 , 在标记阶段标记所有需要回收的对象 , 在清除极端可回收的对象并释放所长用的内存空间 。
由于标记清除算法实在清理对象锁占用的内存空间后并没有重新整理可用的内存空间 , 因此如果内存中被护手的小对象过多 , 则会引起内存碎片化的问题 , 继而引起大对象无法获得连续可用的内存空间问题
复制算法
复制算法为了解决标记清除算法内存碎片化的问题而设计的 , 复制算法首先将内存活粉两块大小相等的内存区域 , 1区域 , 2区域 , 新生成的对象都被存放在1区域 , 在1区域内的对象存储忙后会对内存1进行一次标记 , 并将标记后仍然存活的对象全部复制到区域2中 , 然后直接清理1区域的内存 。
复制算法的内存清理效率高 , 并且容易实现 , 但由于同一时刻只有一个内存区域可用 , 即可用的内存空间被压缩为原来的一半 , 因此存在大量的内存空间浪费 , 同时 , 在系统中有大量长时间存活的对象是 , 这些对象将存活在1区域和2区域之间来回复制而影响系统的运行效率 。因此该算法只在对象存活时间较短时效率高 。
标记整理算法
标记整理算法结合了标记清除算法和复制算法的有点 , 其标记阶段和标记清除算法的标记阶段仙童 , 在标记完成后存活的对象复制到内存的一端 , 然后清除该端的对象并释放内存
分代回收算法
无论是标记清除算法、复制算法、标记整理算法 , 都无法堆所有类型(长生命手气、短生命周期、大对象、小对象)的对象进行垃圾回收 。因此正对不同的对象类型 , jvm采用了不同的垃圾回收算法 , 该算法被称为分代回收算法
分代回收算法根据对象的不同类型将内存划分为不同的区域 , jvm将对划分为新生代和老年代 , 新生代主要存放新生成的对象 , 其特点是对象数量多但是生命周期短 , 在每次进行垃圾回收是都有大量的堆被回收 , 老年代的主要存放大对象和生命走起长的对象 , 因此可回收的对象相对较少 , 因此 , jvm根据不同的区域的特点选择不同的算法 。
目前 , 大部分jvm在新生代都采用了复制算法 , 因为在新生代中每次进行垃圾回收时都有大量的 垃圾对象被回收 , 需要复制的对象(存活的)比较少 , 不存在大量的对象在内存中被来回复制的问题 , 因此采用复制算法能安全、高效的回收新生代大量的生命周期的对象并释放内存空间 。
jvm将新生代进一步划分为一块大的eden区和两块比较小的区 , 区又分为和区 , jvm在运行过程中主要使用eden区和区 , 进行垃圾回收时会将eden区和区中存活的对象复制到区 , 然后清理eden区和区 。
老年代主要存放声音周期比较长的对象和大对象 , 因为每次只有少量的非存活的对象被回收 , 因而在老年代采用标记清除算法 。
在jvm中海油一个区域 , 即方法区中的永久代 , 永久代用存储class类、常量、方法描述等 , 在永久代主要回收废弃的常量和无用的类 。
jvm内存中的对象组要被分配到新生代中的eden区和区 , 在少数情况下回直接分配到来年代 , 在新生代的eden区和区的内存空间不足时会触发一次gc , 该过程成为 。在后 , eden区和区中存活的对象会被复制到中 , 然后eden区和区中的对象被清理 , 如果此时在区无法找到连续的内存空间存储某个对象 , 则将这个对象直接存储到老年代 , 若区的对象经过一次gc之后仍然存活 , 则其年龄+1 , 默认情况下 , 对象在年龄达到15时 , 将被移动到老年代 。
Java中的内存区域
JVM 的内存区域分为私有区域(程序计数器、虚拟机栈、本地方法区)、线程共享区域(对、方法区)和直接内存 。
线程私有区域的生命周期与线程仙童 , 随线程的启动而创建 , 随着线程的结束而销毁 , 在JVM内部 , 每个线程斗鱼操作系统的本地线程直接映射 , 因此线程私有区域的存在与否和本地线程的启动和销毁对应
线程共享区域随着虚拟机的饿启动而创建 , 随着关闭而销毁
直接内存也交所对外内存 , 他并不是JVM运行时的数据区的一部分 , 但是在并发中被频繁使用 , JDK的nio模块提供基于和的IO操作就是基于对堆外内存实现的 , nio模块通过调用本地方法库直接在操作系统上分配堆内存 , 然后直接使用对象作为这块内存的引用对内存进行操作 , Java进程可以通过对外内存技术避免在Java对和中来回复制数据带来的资源兰妃和性能损耗和性能消耗 , 因此对外内存在搞并发场景下被广泛使用 。
程序计数器
程序计数器 线程私有
去内存泄漏的问题 , 它是一块很小的内存空间 , 用于存储当前运行的线程锁执行的字节码的行号指示器 , 每个运行中的线程中都有一个独立的程序计数器 , 在方法正在执行时 , 该方法的程序计数器记录的是事实虚拟机字节码指令的地址 , 如果该方法执行的是本地方法 , 则程序计数器的值为空
虚拟机栈
线程私有 描述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区、区、区 , Eden区默认占用8/10新生代空间 , 区和区默认占用1/10新生代空间 。
新生代
新生代分为 Eden区、区、区 。JVM新创建的对象(除了大对象)会被存放在新生代 , 默认占用新生代的1/3空间 , 由于jvm会频繁创建对象 , 所以新生代会频繁出发进行来及回收 。
Eden区 java次年创建的对象首相会被存放在Eden区 , 如果新创建的对象属于大对象 , , 则直接分配到老年区 , 大对象的定义和具体的jvm版本 , 堆大小和垃圾回收策略有关 , 一般为2-128K,可通过xx:ud设置大小 , 在Eden区的内存空间不足时会触发 , 对新生代进行一次垃圾回收 。区 保留上一次时的幸存者区 将上一次的幸存者作为这一次的被扫描这
新生代的gc过程叫 , 采用复制算法实现
把Eden区和区中村活的对象复制到区 。如果某对像的年龄达到 老年代的标准(对象晋升老念叨的标准由XX:设置 , 默认是15) , 将其复制到老年代 , 同事吧这些对象的年龄+1 , 如果区的内存空间不够 , 则也直接将其复制到老年代;如果对象属于大对象(2-128k的对象属于大对象) , 则也直接将其复制到老年代清空Eden区和区中的对象将区和区互换 , 原来区成为下一次GC的区 。老年代
老对象主要存放长生命周期的对象和大对象 , 老年代的gc成为 , 在老年代 , 对象比较稳定 , 不会被频繁出发 , 在进行前 , jvm会进行一次 , 在后人人出现老遍地当且仅当老年代空间不足或者无法找到足够大的连续内存空间分配给新创建的大对象是 , 会触发进行垃圾回收 , 释放jvm的内存空间
采用标记清楚算法 , 该算法首先会扫描所有对象并标记存活的对象 , 然后挥手未被标记的对象 , 释放内存空间
因为先要扫描老年代的所有对象再回收 , 所以的耗时比较长 , 的标记清楚算法容易产生内存碎片 , 在老年代没还有足够存储空间可分配是 , 会抛出oom异常
永久代
永久代值内存的永久保存区域 , 主要存放class和meta的信息 , class在类加载时被放入到永久代 , 永久代和老年代、新生代不同 , 过程不会再程序运行过程中对永久代的内存进行清理 , 这也导致永久代的内存会随着加载class文件的增加而增加 , 在加载的class文件过多时会抛出oom异常 , 比如引用jar文件过多导致jvm内存不足而无法启动
需要注意的是 , 在java8中永久代已经被元数据区(元空间)取代 , 元数据区的作用和永久代类似 , 二者最大的区别在于:元数据并没有使用虚拟机的内存 , 而是使用直接内存 , 因此元空间大小不受jvm内存的限制 , 主要和操作系统的内存有关
在java8中 , jvm将类的元数据放在本地内存中 , 将常量池和类的静态变量放入到java堆中 , 这样jvm能够加载多少元数据信息就不再有jvm的最大可用内存空间决定 , 而是由操作系统的实际可用内存空间决定 。
面试题 双亲委派模型的好处:
·主要是为了安全性 , 避免用户自己编写的类动态替换 Java的一些核心类 , 比如 。
同时也避免了类的重复加载 , 因为JVM中区分不同类 , 不仅仅是根据类名 , 相同的class文件被不同的
加载就是不同的两个类
GC如何判断对象可以被回收
。引用计数法:每个对象有一个引用计数属性 , 新增一个引用时计数加1 , 引用释放时计数减1 , 计数为0时可以
回收 ,
可达性分析法:从 GC Roots开始向下搜索 , 搜索所走过的路径称为引用链 。当一个对象到GC Roots 没有任
何引用链相连时 , 则证明此对象是不可用的 , 那么虚拟机就判断是可回收对象 。
引用计数法 , 可能会出现A引用了B , B又引用了A , 这时候就算他们都不再使用了 , 但因为相互引用计数器=1永远无法被回收 。
GC Roots的对象有:
1.虚拟机栈(栈帧中的本地变量表)中引用的对象
2.方法区中类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI(即一般说的方法)引用的对象
可达性算法中的不可达对象并不是立即死亡的 , 对象拥有一次自我拯救的机会 。对象被系统宣告死亡至少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链 , 第二次是在由虚拟机自动建立的列中判断具否雪更执行方法
当对象变成(GC Roots)不可达时 , GC会判断该对象是否覆盖了方法 , 若未覆盖 , 则直接将其回收 。否则,若对象未执行过方法 , 将其放入F-Queue队列 , 由一低优先级线程执行该队列中对象的方法 。执行方法完毕后 , GC会再次判断该对象是否可达 , 若不可达 , 则进行回收 , 否则 , 对象“复活1
每个对象只能触发一次方法
由于方法运行代价高昂 , 不确定性大 , 无法保证各个对象的调用顺序 , 不推荐大家使用 , 建议遗忘它 。
Java类加载器
JDK自带有三个类加载器: 、、 。
是的父类加载器 , 默认负责加载%%lib下的jar包和class文
件 。
是的父类加载器 , 负责加载%%/lib/ext文件夹下的jar包和class类 。
是自定义类加载器的父类 , 负责加载下的类文件 。
继承实现自定义类加载器
Jdk1.7到Jdk1.8 java虚拟机发生了什么变化?
1.7中存在永久代 , 1.8中没有永久代 , 替换它的是元空间 , 元空间所占的内存不是在虚拟机内部 , 而是本地内存空间 , 这么做的原因是 , 不管是永久代还是元空间 , 他们都是方法区的具体实现 , 之所以元空间所占的内存改成本地内存 , 官方的说法是为了和统一 , 不过额外还有一些原因 , 比如方法区所存储的类信息通常是比较难确定的 , 所以对于方法区的大小是比较难指定的 , 太小了容易出现方法区溢出 , 太大了又会占用了太多虚拟机的内存空间 , 而转移到本地内存后则不会影响虚拟机所占用的内存
Java的内存结构 , 堆分为哪几部分 , 默认年龄多大进入老年代
1.年轻代
a.Eden区(8)
b. From 区 (1)c. To 区(1)2.老年代
默认对象的年龄达到15后 , 就会进入老年代
简单说说你了解的类加载器 , 可以打破双亲委派么 , 怎么打破 。
启动类加载器:由C++实现 , 负责加载目录中的 , 或通过-参数指定路径中的 , 且被虚拟机认可(按文件
名识别 , 如rt.jar)的类 。
扩展类加载器:负责加载\lib\ext目录中的 , 或通过java.ext.dirs 系统变量指定路径中的类库 。
应用程序类加载器:负责加载用户路径()上的类库 。
::·自定义类加载器:通过继承java.lang.Cl
简单说说你了解的类加载器 , 可以打破双亲委派么 , 怎么打破 。
启动类加载器:由C++实现 , 负责加载目录中的 , 或通过-参数指定路径中的 , 且被虚拟机认可(按文件
名识别 , 如rt.jar)的类 。
扩展类加载器:负责加载\lib\ext目录中的 , 或通过java.ext.dirs 系统变量指定路径中的类库 。
应用程序类加载器:负责加载用户路径()上的类库 。
::·自定义类加载器:通过继承java.lang. 实现自定义的类加载器 。
什么是双亲委派机制?
为了防止内存中存在多份同样的字节码 , 使用了双亲委派机制(它不会自己去尝试加载类 , 而是把请求委托给父加载器去完成 , 依次向上) ,
双亲委派模型可以确保安全性 , 可以保证所有的Java类库都是由启动类加载器加载 。
什么是打破了双亲委派机制?
因为加载class 核心的方法在 类的方法上(双亲委派机制的核心实现) , 那只要我自定义个 , 重写
方法(不依照往上开始寻找类加载器) , 那就算是打破双亲委派机制了 。
是怎么打破双亲委派机制的呢?
虽说现在都流行使用开发web应用 , 内嵌在中 。而在此之前 , 我们会使用最原生的方式 , + 的
方式开发和部署web程序 。
一个 可能会部署多个这样的web应用 , 不同的web应用可能会依赖同一个第三方库的不同版本 , 为了保证每个web应用的类库都是
独立的 , 需要实现类隔离 。而 的自定义类加载器 解决了这个问题 。
每一个web应用都会对应一个 实例 , 不同的类加载器实例加载的类是不同的 , Web应用之间通各自的类加载器相互隔
离 。
当然 自定义类加载器不只解决上面的问题 , 打破了双亲委派机制 , 即它首先自己尝试去加载某个类 , 如果找不
到再代理给父类加载器 , 其目的是优先加载Web应用定义的类 。
的类加载器是怎么打破双亲委派的机制的 。我们先看代码:
在自己的代码中 , 如果创建一个java.lang.类 , 这个类是否可以被类加载器加载?为什
么 。
类加载机制
如果一个类加载器收到了类加载的请求 , 它首先不会自己尝试去加载这个类 , 而是把这个请求委派给父类加载器 , 每一个层次的类加载器都是加此 , 因此所有的加载请求最终到达顶层的启动类加载器 , 只有当父类加载器反馈自己无法完成加载请求时(指它的搜索范围没有找到所需的类) , 子类加载器才会尝试自己去加载 。
各个类加载器之间是组合关系 , 并非继承关系 。
当一个类加载器收到类加载的请求 , 它将这个加载请求委派给父类加载器进行加载 , 每一层加载器都是如此 , 最终 , 所有的请求都会传送到启动类加载器中 。只有当父类加载器自己无法完成加载请求时 , 子类加载器才会尝试自己加载 。
双亲委派模型可以确保安全性 , 可以保证所有的Java类库都是由启动类加载器加载 。如用户编写的java.lang. , 加载请求传递到启动类加载器 , 启动类加载的是系统中的对象 , 而用户编写的java.lang.不会被加载 。如用户编写的java.lang.virus类 , 加载请求传递到启动类加载器 , 启动类加载器发现virus类并不是核心Java类 , 无法进行加载 , 将会由具体的子类加载器进行加载 , 而经过不同加载器进行加载的类是无法访问彼此的 。由不同加载器加载的类处于不同的运行时包 。所有的访问权限都是基于同一个运行时包而言的 。
jvm对象创建和内存分配?
大对象的大小 , 如果对象超过设置大小会直接进入老年代 , 不会进入年轻代 , 这个参数只在和两个收集器下有效 。
比如设置JVM参数:-×X:ld=(单位是字节)-XX:+ , 再执行下上面的第一
个程序会发现大对象直接进了老年代
为什么要这样呢?
为了避免为大对象分配内存时的复制操作而降低效率 。
长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存 , 那么内存回收时就必须能识别哪些对象应放在新生代 , 哪些对象应放在
老年代中 。为了做到这一点 , 虚拟机给每个对象一个对象年龄(Age)计数器 。
如果对象在Eden出生并经过第一次 Minor GC后仍然能够存活 , 并且能被 容纳的话 , 将被移动到空间中 , 并将对象年龄设为1 。对象在 中每熬过一次 , 年龄就增加1岁 , 当它的年龄增加到一定程度(默认为15岁 , CMS收集器默认6岁 , 不同的垃圾收集器会略微有点不同) , 就会被晋升到老年代中 。对象晋升到老年代的年龄阈值 , 可以通过参数-XX:来设置 。
对象动态年龄判断
当前放对象的区域里(其中一块区域 , 放对象的那块s区) , 一批对象的总大小大于这块区域内存大小的50%(-XX:可以指定) , 那么此时大于等于这批对象年龄最大值的对象 , 就可以直接进入老年代了.例如区域里现在有一批对象 , 年龄1+年龄2+年龄n的多个年龄对象总和超过了区域的50% , 此时就会把年龄n(含)以上的对象都放入老年代 。这个规则其实是希望那些可能是长期存活的对象 , 尽早进入老年代
垃圾收集器
G1收集器参数设置
-XX:+:使用G1收集器
-XX::指定GC工作的线程数量
-XX::指定分区大小(1MB~32MB , 且必须是2的N次幂) , 默认将整堆划分为
2048个分区
-XX::目标暂停时间(默认200ms)
-XX::新生代内存初始空间(默认整堆5% , 值配置整数 , 默认就是百分比)
-XX::新生代内存最大空间
-XX::区的填充容量(默认50%) , 区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了区域的50% , 此时就会把年龄n(含)以上的对象都放入老年代
-XX::最大年龄阈值(默认15)
-XX::老年代占用空间达到整堆内存阈值(默认45%) , 则执行新生代和老年代的混合收集() , 比如我们之前说的堆默认有2048个 , 如果有接近1000个都是老年代的 , 则可能就要触发了
Java里有如下几种类加载器
·引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库 , 比如rt.jar、.jar等
扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
应用程序类加载器:负责加载路径下的类包 , 主要就是加载你自己写的那些类
自定义加载器:负责加载用户自定义路径下的类包
void main([] args)
G1的使用场景总结
.out.(.class.()
ZGC核心之染色指针技术ZGC支撑TB级别堆空间秘籍
.out.(com.sun....class.
.out.(.class.().().g
类加载双亲委派机制是怎么回事
双亲委派机制说简单点就是 , 先找父亲加载加载 , 不行再由儿子加载器自己加载
G1垃圾收集分类有哪几种G1的核心调优参数有哪些每秒几十万并发的系统如何用G1来优化
比如我们自己写的一个类 , 最先会找应用程序类加载器加载 , 应用程序类加载器会先委托扩展类加载器加载 , 扩展类加载器再委托引导类加载器 , 顶层引导类加载器在自己的类加载路径里找了半天没找到你自己写的类 , 则向下退回加载类的请求 , 扩展类加载器收到回复就自己加载 , 在自己的类加载路径里找了半天也没找到 , 又向下退回类的加载请求给应用程序类加载器 , 应用程序类加载器于是在自己的类加载路径里找你自己写的类 , 结果找到了就自己加载了 。
为什么会有双亲委派机制?|
Y+.ja
G1的使用场景总结
ZGC核心之染色指针技术
ZGC支撑TB级别堆空间秘
.沙箱安全机制:自己写的java.lang..class类不会被加载 , 这样便可以防止核心API库被随意篡改·避免类的重复加载:当父亲已经加载了该类时 , 就没有必要子再加载一次 , 保证被加载类的唯一性
底层类加载是用的双亲委派机制吗
不是!底层类加载打破了双亲委派机制!
一个 Web容器可能需要部署两个应用程序 , 不同的应用程序可能会依赖同一个第三方类库的不同版本 , 不能要求同一个类库在同一个服务器只有一份 , 因此要保证每个应用程序的类库都是独立的 , 保证相互隔离 。
说下对象完整创建流程
对象创建的主要流程:
1.类加载检查
虚拟机遇到一条new指令时 , 首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用 , 并且检查这个符号引用代表的类是否已被加载、解析和初始化过 。如果没有 , 那必须先执行相应的类加载过程 。
new指令对应到语言层面上讲是 , new关键词、对象克隆、对象序列化等 。
2.分配内存
在类加载检查通过后 , 接下来虚拟机将为新生对象分配内存 。对象所需内存的大小在类加载完成后便可完全
确定 , 为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来 。
3.初始化零值
内存分配完成后 , 虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头) 。
4.设置对象头
初始化零值之后 , 虚拟机要对对象进行必要的设置 , 例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息 。这些信息存放在对象的对象头 之中 。在虚拟机中 , 对象在内存中存储的布局可以分为3块区域:对象头()、实例数据
( Data)和对齐填充() 。虚拟机的对象头包括两部分信息 , 第一部分用于存储对象自身的运行时数据 , 如哈希码()、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等 。对象头的另外一部分是类型指针 , 即对象指向它的类元数据的指针 , 虚拟机通过这个指针来确定这个对象是哪个类的实例 。
32位对象头
对象分配内存时的指针碰撞与空闲列表机制知道吗
?指针碰撞(Bump the )(默认用指针碰撞)
如果Java堆中内存是绝对规整的 , 所有用过的内存都放在一边 , 空闲的内存放在另一边 , 中间放着一个指针
作为分界点的指示器 , 那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离 。
。空闲列表(Free List)
如果Java堆中的内存并不是规整的 , 已使用的内存和空闲的内存相互交错 , 那就没有办法简单地进行指针碰撞了 , 虚拟机就必须维护一个列表 , 记 录上哪些内存块是可用的 , 在分配的时候从列表中找到一块足够大的空间划分给对象实例 , 并更新列表上的记录 。
在并发情况下 , 可能出现正在给对象A分配内存 , 指针还没来得及修改 , 对象B又同时使用了原来的指针来
分配内存的情况 。
解决并发问题的方法:
CAS
( and swap)
CAS ( and swap)
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理 。
。本地线程分配缓冲( Local,TLAB)
把内存分配的动作按照线程划分在不同的空间之中进行 , 即每个线程在Java堆中预先分配一小块内存 。通过-XX:+/-参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+) , -XX:指定TLAB大小 。
什么是java对象的指针压缩?
1.jdk1.6 开始 , 在64bit操作系统中 , JVM支持指针压缩
2.jvm配置参数: , –压缩、oop()–对象指针
3.启用指针压缩:-XX:+(默认开启) , 禁止指针压缩:-XX:-
为什么要进行指针压缩?
1.在64位平台的中使用32位指针(实际存储用64位) , 内存使用会多出1.5倍左右 , 使用较大指针在主内存和缓存之间移动数据 , 占用较大宽带 , 同
时GC也会承受较大压力
2.为了减少64位平台下内存的消耗 , 启用指针压缩功能
3.在jvm中 , 32位地址最大支持4G内存(2的32次方) , 可以通过对对象指针的存入堆内存时压缩编码、取出到cpu寄存器后解码方式进行优化(对象指针在堆
中是32位 , 在寄存器中是35位 , 2的35次方=32G) , 使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G)
4.堆内存小于4G时 , 不需要启用指针压缩 , jvm会直接去除高32位地址 , 即使用低虚拟地址空间
5.堆内存大于32G时 , 压缩指针会失效 , 会强制使用64位(即8字节)来对java对象寻址 , 这就会出现1的问题 , 所以堆内存不要大于32G为好
对象内存分配
对象栈上分配
我们通过JVM内存分配可以知道JAVA中的对象都是在堆上进行分配 , 当对象没有被引用的时候 , 需要依靠GC进行回收内存 , 如果对象数量较多的时候 , 会给GC带来较大压力 , 也间接影响了应用的性能 。为了减少临时对象在堆内分配的数量 , JVM通过逃逸分析确定该对象不会被外部访问 。如果不会逃逸可以将该对象在找上分配内存 , 这样该对象所占用的内存空间就可以随栈帧出栈而销毁 , 就减轻了垃圾回收的压力 。
对象逃逸分析;就是分析对象动态作用域 , 当一个对象在方法中被定义后 , 它可能被外部方法所引用 , 例如作为调用参数传递到其他地方中 。
很显然test1方法中的user对象被返回了 , 这个对象的作用域范围不确定 , test2方法中的user对象我们可以确定当方法结束这个对象就可以认为是无效对象
了 , 对于这样的对象我们其实可以将其分配在栈内存里 , 让其在方法结束时跟随栈内存一起被回收掉 。
JVM对于这种情况可以通过开启逃逸分析参数(-XX:+)来优化对象内存分配位置 , 使其通过标量替换优先分配在栈上(栈上分配) , JDK7之后
默认开启逃逸分析 , 如果要关闭使用参数(-XX:-)
标量替换:通过逃逸分析确定该对象不会被外部访问 , 并且对象可以被进一步分解时 , JVM不会创建该对象 , 而是将该对象成员变量分解若干个被这个方法
使用的成员变量所代替 , 这些代替的成员变量在栈帧或寄存器上分配空间 , 这样就不会因为没有一大块连续空间导致对象内存不够分配 。开启标量替换参数(-XX:+) , JDK7之后默认开启 。
标量与聚合量:标量即不可被进一步分解的量 , 而JAVA的基本数据类型就是标量(如:int , long等基本数据类型以及类型等) , 标量的对立就是可
以被进一步分解的量 , 而这种量称之为聚合量 。而在JAVA中对象就是可以被进一步分解的聚合量 。
栈上分配示例:
”栈上分配 , 标量替换
, 代码调用了1亿次alloc() , 如果是分配到堆上 , 大概需要1GB以上堆空间 , 如果堆空间小于该值 , 必然会触发GC 。
”使用如下参数不会发生GC
判断对象是否是垃圾的引用计数法有什么问题
文章插图
给对象中添加一个引用计数器 , 每当有一个地方引用它 , 计数器就加1;当引用失效 , 计数器
就减1;任何时候计数器为0的对象就是不可能再被使用的 。|
这个方法实现简单 , 效率高 , 但是目前主流的虚拟机中并没有选择这个算法来管理内存 , 其最主要的原因是它很难解决对象之间相互循环引用的问题 。所谓对象之间的相互引用问题 , 如下面代码所示:除了对象objA和objB相互引用着对方之外 , 这两个对象之间再无任何引用 。但是他们因为互相引用对方 , 导致它们的引用计数器都不为0 , 于是引用计数算法无法通知GC回收器回收他们 。
class
= null;
void main([] args)
objA = new ();
GC底层可达性分析算法是怎么回事
将“GC Roots”对象作为起点 , 从这些节点开始向下搜索引用的对象 , 找到的对象都标记为非
垃圾对象 , 其余未标记的对像都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
什么样的类能被回收
.该类所有的对象实例都已经被回收 , 也就是Java 堆中不存在该类的任何实例 。
加载该类的已经被回收 。
.该类对应的java.lang.Class对象没有在任何地方被引用 , 无法在任何地方通过反射访问该类
的方法 。
解释下JVM内部各种垃圾收集算法
分代收集理论
当前虚拟机的垃圾收集都采用分代收集算法 , 这种算法没有什么新的思想 , 只是根据对象存活周期的不同将内存分为几块 。一般将java堆分为新生代和老年代 , 这样我们就可以根据各个年代的特点选择合适的垃圾收集算法 。
比如在新生代中 , 每次收集都会有大量对象(近99%)死去 , 所以可以选择复制算法 , 只需要付出少量对象的复制成本就可以完成每次垃圾收集 。而老年代的对象存活几率是比较高的 , 而且没有额外的空间对它进行分配担保 , 所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集 。注意 , “标记-清除”或“标记-整理”算法会比复制算法慢10倍以上 。
复制算法
为了解决效率问题 , “复制”收集算法出现了 。它可以将内存分为大小相同的两块 , 每次使用其
中的一块 , 当这一块的内存使用完后
就将还存活的对象复制到另一块去 , 然后再把使用的空间一次清理掉 。这样就使每次的内存回收都是对内存区间的一半进行回收 。
标记-清除算法
算法分为“标记”和“清除”阶段:标记存活的对象 , 统一回收所有未被标记的对象(一般选择这种);也可以反过来 , 标记出所有需要回收的对象 , 在标记完成后统一回收所有被标记的对象 。它是最基础的收集算法 , 比较简单 , 但是会带来两个明显的问题:
1.效率问题(如果需要标记的对象太多 , 效率不高)2.空间问题(标记清除后会产生大量不连续的碎片)
【JVM 类加载机制】解释下CMS收集器垃圾收集过程
CMS ( Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器 。它非常符合在注重用户体验的应用上使用 , 它是虚拟机第一款真正意义上的并发收集器 , 它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作 。
从名字中的Mark Sweep这两个词可以看出 , CMS收集器是一种“标记-清除”算法实现的 , “
的运作过程相比于前面几种垃圾收集器来说更加复杂一些 。整个过程分为四个步骤:
。初始标记: 暂停所有的其他线程(STW) , 并记录下gc roots直接能引用的对象 , 速度很快 。并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程 , 这个过程耗时较长但是不需要停顿用户线程 , 可以与垃圾收集线程一起并发运行 。因为用户程序继续运行 , 可能会有导致已经标记过的对象状态发生改变 。
重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录 , 这个阶段的停顿时间一般会比初始标记阶段的时间稍长 , 远远比并发标记阶段时间短 。主要用到三色标记里的增量更新算法(见下面详解)做重新标记 。·并发清理:开启用户线程 , 同时GC线程开始对未标记的区域做清扫 。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解) 。
并发重置:
重置本次GC过程中的标记数据
从它的名字就可以看出它是一款优秀的垃圾收集器 , 主要优点:并发收集、低停顿 。但是它有
下面几个明显的缺点:
·对CPU资源敏感(会和服务抢资源);
。无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾 , 这种浮动垃圾只能等到下一次
gc再清理了);
·它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生 , 当然通过参数-XX:+可以让jvm在执行完标记清除后再做整理
。执行过程中的不确定性 , 会存在上一次垃圾回收还没执行完 , 然后垃圾回收又被触发的情况 , 特别是在并发标记和并发清理阶段会出现 , 一边回收 , 系统一边运行 , 也许没回收完就再次触发full gc , 也就是" mode " , 此时会进入stop the world,用 old垃圾收集器来回收
CMS比较严重的问题并发收集阶段再次触发Full gc怎么处理
CMS垃圾收集器收集垃圾过程中 , 会存在上一次垃圾回收还没执行完 , 然后垃圾回收又被触发
的情况 , 特别是在并发标记和并发清理阶段会出现 , 一边回收 , 系统一边运行 , 也许没回收完
就再次触发full gc , 也就是“ mode " , 此时会进入stop the world,用 old垃圾收集器来回收
解释下垃圾收集底层三色标记算法
在并发标记的过程中 , 因为标记期间应用线程还在继续跑 , 对象间的引用可能发生变化 , 多标和漏标的情况就有可能发生 。漏标的问题主要引入了三色标记算法来解决 。
三色标记算法把Gc roots可达性分析遍历对象过程中遇到的对象 , 按照“是否访问过”这个条
件标记成以下三种颜色:
。黑色: 表示对象已经被垃圾收集器访问过 , 且这个对象的所有引用都已经扫描过 。黑色的对象代表已经扫描过 , 它是安全存活的 , 如果有其他对象引用指向了黑色对象 , 无须重新扫描一遍 。黑色对象不可能直接(不经过灰色对象)指向某个白色对象 。
。灰色:表示对象已经被垃圾收集器访问过 , 但这个对象上至少存在一个引用还没有被扫描
过 。
白色:表示对象尚未被垃圾收集器访问过 。显然在可达性分析刚刚开始的阶段 , 所有的对
象都是白色的 , 若在分析结束的阶段 , 仍然是白色的对象 , 即代表不可达 。
解释下对象漏标的处理方案增量更新与原始快照(SATB)
漏标会导致被引用的对象被当成垃圾误删除 , 这是严重bug , 必须解决 , 有两种解决方案:增量更新( )和原始快照( At The ,SATB) 。增量更新就是当黑色对象插入新的指向白色对象的引用关系时 , 就将这个新插入的引用记录下来 , 等并发扫描结束之后 , 再将这些记录过的引用关系中的黑色对象为根 , 重新扫描一次 。这可以简化理解为 , 黑色对象一旦新插入了指向白色对象的引用之后 , 它就变回灰色对象了 。
原始快照就是当灰色对象要删除指向白色对象的引用关系时 , 就将这个要删除的引用记录下来 , 在并发扫描结束之后 , 再将这些记录过的引用关系中的灰色对象为根 , 重新扫描一次,这样就能扫描到白色的对象 , 将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来 , 待下一轮gc的时候重新扫描 , 这个对象也有可能是浮动垃圾)
CMS、G1与ZGC对于对象漏标问题的处理区别
现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想 , 尽管实现的方式不尽相同:比如白色/黑色集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可以是广度/深度遍历等等 。
对于读写屏障 , 以JavaVM为例 , 其并发标记时对漏标的方式处理如下:
CMS:写屏障+增量更新)
G1, :写屏障
·ZGC:读屏障
工程实现中 , 读写屏障还有其他功能 , 比如写屏障可以用于记录跨代/区引用的变化 , 读屏障可以用于支持移动对象的并发执行等 。功能之外 , 还有性能的考虑 , 所以对于选择哪种 , 每款垃圾回收器都有自己的想法 。
解释下G1收集器垃圾收集过程
G1收集器一次GC(主要值Mixed GC)的运作过程大致分为以下几个步骤:
。初始标记( mark , STW):暂停所有的其他线程 , 并记录下gc roots直接能引用的对
象 , 速度很快;
·并发标记( ):同CMS的并发标记
、最终标记( , STW):同CMS的重新标记
。筛选回收( , STW):筛选回收阶段首先对各个的回收价值和成本进行排序 , 根据用户所期望的GC停顿STW时间(可以用JVM参数-XX:指定)来制定回收计划 , 比如说老年代此时有1000个都满了 , 但是因为根据预期停顿时间 , 本次垃圾回收可能只能停顿200毫秒 , 那么通过之前回收成本计算得知 , 可能回收其中800个刚好需要200ms , 那么就只会回收800个( Set , 要回收的集合) , 尽量把GC导致的停顿时间控制在我们指定的范围内 。这个阶段其实也可以做到与用户程序一起并发执行 , 但是因为只回收一部分 , 时间是用户可控制的 , 而且停顿用户线程将大幅提高收集效率 。不管是年轻代或是老年代 , 回收算法主要用的是复制算法 , 将一个中的存活对象复制到另一个中 , 这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次 , G1采用复制算法回收几乎不会有太多内存碎片 。(注意:CMS回收阶段是跟用户线程一起并发执行的 , G1因为内部实现太复杂暂时没实现并发回收 , 不过到了ZGC , 就实现了并发收集 , 可以看成是G1的升级版本)
G1垃圾收集器最大停顿时间是如何实现的
G1收集器在后台维护了一个优先列表 , 每次根据允许的收集时间 , 优先选择回收价值最大的(这也就是它的名字-First的由来) , 比如一个花200ms能回收1M垃圾 , 另外一个花50ms能回收2M垃圾 , 在回收时间有限情况下 , G1当然会优先选择后面这个回收 。这种使用划分内存空间以及有优先级的区域回收方式 , 保证了G1收集器在有限时间内可以尽可能高的收集效率 。
GC是什么时候都能做吗?知道GC安全点与安全区域是怎么回事吗?
再给大家讲一种情况 , 一般电商架构可能会使用多级缓存架构 , 就是redis加上JVM级缓存 , 大多数同学可能为了图方便对于JVM级缓存就简单使用一个 , 于是不断往里面放缓存数据 , 但是很少考虑这个map的容量问题 , 结果这个缓存map越来越大 , 一直占用着老年代的很多空间 , 时间长了就会导致full gc非常频繁 , 这就是一种内存泄漏 , 对于一些老旧数据没有及时清理导致一直占用着宝贵的内存资源 , 时间长了除了导致full gc , 还有可能导致0OM 。
这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决 , 比如等自带一些LRU数据淘汰算法
的框架来作为JVM级的缓存 。
GC是什么时候都能做吗?知道GC安全点与安全区域是怎么回事吗?
GC不是任何时候都能做的 , 需要代码运行到安全点或安全区域才能做 。
安全点就是指代码中一些特定的位置 , 当线程运行到这些位置时它的状态是确定的 , 这样JVM就可以安全的进行一些操作 , 比如GC等 , 所以GC不是想什么时候做就立即触发的 , 是需要等待所有线程运行到安全点后才能触发 。
这些特定的安全点位置主要有以下几种:
1.方法返回之前2.调用某个方法之后3.抛出异常的位置4.循环的末尾
大体实现思想是当垃圾收集需要中断线程的时候 , 不直接对线程操作 , 仅仅简单地设置一个标志位 , 各个线程执行过程时在上面说的那些安全点位置都会检查这个标志 , 一旦发现中断标志为真时就自己在安全点上主动中断挂起 。
安全区域又是什么?
安全点是对正在执行的线程设定的 , 如果一个线程处于Sleep或中断状态 , 它就不能响应
JVM的中断请求 , 再运行到安全点上 。
因此JVM引入了安全区域 , 是指在一段代码片段中 , 引用关系不会发生变化 , 在这个区域内的任意地方开始GC都是安全的 。
如何进行JVM调优?JVM参数有哪些?怎么查看一个JAVA进程的JVM参数?谈谈你了解的JVM参数 。如果一个java程序每次运行一段时间后 , 就变得非常卡顿 , 你准备如何对他进行优化?
JVM调优主要就是通过定制JVM运行参数来提高JAVA应用程度的运行数据
JVM参数大致可以分为三类:
1、标注指令:-开头 , 这些是所有的都支持的参数 。可以用java-help打印出来 。
2、非标准指令:-X开头 , 这些指令通常是跟特定的版本对应的 。可以用java-X打印出来 。
3、不稳定参数: -XX 开头 , 这一类参数是跟特定版本对应的 , 并且变化非常大 。详细的文档资料非常少 。在
JDK1.8版本下 , 有几个常用的不稳定指令:
java -XX:+s:查看当前命令的不稳定指令 。
java -XX:+:查看所有不稳定指令的默认值 。
java -XX:+:查看所有不稳定指令最终生效的实际值 。
你们项目如何排查JVM问题
对于还在正常运行的系统:
1.可以使用jmap来查看JVM中各个区域的使用情况
2.可以通过来查看线程的运行情况 , 比如哪些线程阻塞、是否出现了死锁
3.可以通过jstat命令来查看垃圾回收的情况 , 特别是 , 如果发现比较频繁 , 那么就得进行调优了
4.通过各个命令的结果 , 或者等工具来进行分析
5.首先 , 初步猜测频繁发送的原因 , 如果频繁发生但是又一直没有出现内存溢出 , 那么表示实际上是回收了很多对象了 , 所以这些对象最好能在过程中就直接回收掉 , 避免这些对象进入到老年代 , 对于这种情况 , 就要考虑这些存活时间不长的对象是不是比较大 , 导致年轻代放不下 , 直接进入到了老年代 , 尝试加大年轻代的大小 , 如果改完之后 , 减少 , 则证明修改有效
6.同时 , 还可以找到占用CPU最多的线程 , 定位到具体的方法 , 优化这个方法的执行 , 看是否能避免某些对象的创建 , 从而节省内存
对于已经发生了0OM的系统:
1.一般生产系统中都会设置当系统发生了00M时 , 生成当时的dump文件(-XX:+-
XX:=/usr/local/base)
2.我们可以利用等工具来分析dump文件
3.根据dump文件找到异常的实例对象 , 和异常的线程(占用CPU高) , 定位到具体的代码
4.然后再进行详细的分析和调试
总之 , 调优不是一蹴而就的 , 需要分析、推理、实践、总结、再分析 , 最终定位到具体的问题
JAVA类加载的全过程是怎样的?什么是双亲委派机制?有什么作用?
JAVA的类加载器:->->
每种类加载器都有他自己的加载目录 。
JAVA中的类加载器: , -> -> ->
每个类加载器对他加载过的类 , 都是有一个缓存的 。
双亲委派:向上委托查找 , 向下委托加载 。作用:保护JAVA的层的类不会被应用程序覆盖 。
类加载过程: 加载-》连接-》初始化
加载:把Java的字节码数据加载到JM内存当中 , 并映射成JM认可的数据结构 。
连接:分为三个小的阶段: 1、验证:检查加载到的字节信息是否符合JVM规范 。
2、准备:创建类或接口的静态变量 , 并赋初始值 半初始化状态
3、解析:把符号引用转为直接引用
一个对象从加载到JVM , 再到被GC清除 , 都经历了什么过程?
(c =new ); c.xxx}
1、用户创建一个对象 , JVM首先需要到方法区去找对象的类型信息 。然后再创建对象 。
2、JVM要实例化一个对象 , 首先要在堆当中先创建一个对象 。->半初始化状态
B、对象首先会分配在堆内存中新生代的Eden 。然后经过一次Minor GC , 对象如果存活 , 就会进入S区 。在后续的每次GC中 , 如果对象一直存活 , 就会在S区来回拷贝 , 每移动一次 , 年龄加1 。-> 多大年龄才会移入老年代? 年龄最大15 , 超过定年龄后 , 对象转入老年代 。
4、当方法执行结束后 , 栈中的指针会先移除掉 。
5、堆中的对象 , 经过FulI GC , 就会被标记为垃圾 , 然后被GC线程清理掉 。
说一下JVM中 , 哪些是共享区 , 哪些可以作为gc root
1、堆区和方法区是所有线程共享的 , 栈、本地方法栈、程序计数器是每个线程独有的
2、什么是gc root , JVM在进行垃圾回收时 , 需要找到“垃圾”对象 , 也就是没有被引用的对象 , 但是直接找“垃圾”对象是比较耗时的 , 所以反过来 , 先找“非垃圾”对象 , 也就是正常对象 , 那么就需要从某些“根”开始去找 , 根据这些“根”的引用路径找到正常对象 , 而这些“根”有一个特征 , 就是它只会引用其他对象 , 而不会被其他对象引用 , 例如:栈中的本地变量、方法区中的静态变量、本地方法栈中的变量
、正在运行的线程等可以作为gc root 。
串行
整体过程比较简单 , 就像踢足球一样 , 需要GC时 , 直接暂停 , GC完了再继续 。
这个垃圾回收器 , 是早期垃圾回收器 , 只有一个线程执行GC 。在多CPU架构下 , 性能就会下降严重 。只适用于几十兆的内存空间
在串行基础上 , 增加多线程GC 。PS+PO这种组合是JDK1.8默认的垃圾回收器 。在多CPU的架构下 , 性能会比高很多 。
CMSMark Sweep
核心思想 , 就是将STW打散 , 让一部分GC线程与用户线程并发执行 。
核心思想 , 就是将STW打散 , 让一部分GC线程与用户线程并发执行 。整个GC过程分为四个阶段
1、初始标记阶段:STW只标记出根对象直接引用的对象 。
2、并发标记:继续标记其他对象 , 与应用程序是并发执行 。
3、重新标记: STW 对并发执行阶段的对象进行重新标记 。
4、并发清除:并行 。将产生的垃圾清除 。清除过程中 , 应用程序又会不断的产生新的垃圾 , 叫做浮动垃圾 。这些垃圾就要留
到下一次GC过程中清除 。
G1First 垃圾优先
他的内存模型是实际不分代 , 但是逻辑上是分代的 。在内存模型中 , 对于堆内存就不再分老年代和新生代 , 而是划分成一个一
个的小内存块 , 叫做 。每个可以隶属于不同的年代 。
GC分为四个阶段:
第一:初始标记 标记出直接引用的对象 。STW
第二:标记 , 通过RSet标记出上一个阶段标记的引用到的Old区 。
第三:并发标记阶段:跟CMS的步骤是差不多的 。只是遍历的范围不再是整个Old区 , 而只需要遍历第二步标记出来的
.
第四:重新标记: 跟CMS中的重新标记过程是差不多的 。
第五:垃圾清理:与CMS不同的是 , G1可以采用拷贝算法 , 直接将整个中的对象拷贝到另一个 。而这个阶
段 , G1只选择垃圾较多的来清理 , 并不是完全清理 。
CMS的核心算法就是三色标记 。
三色标记:是一种逻辑上的抽象 。将每个内存对象分成三种颜色: 黑色:表示自己和成员变量都已经标记完毕 。灰色:自己
标记完了 , 但是成员变量还没有完全标记完.
白色:自己未标记完 。
什么是STW?他都发生在哪些阶段?什么是三色标记?如何解决错标记和漏标记的问题?为什么要设计这么多的垃圾回收器?
STW: Stop-The-World 。是在垃圾回收算法执行过程当中 , 需要将JVM内存冻结的一种状态 。在STW状态下 , JAVA的所有线程都是停止执行的-GC线程除外 , 方法可以执行 , 但是 , 不能与JM交互 。GC各种算法优化的重点 , 就是减少STW , 同时这也是JVM调优的重点 。
JVM的垃圾回收器:
他的内存模型是实际不分代 , 但是逻辑上是分代的 。在内存模型中 , 对于堆内存就不再分老年代和新生代 , 而是划分成一个一
个的小内存块 , 叫做 。每个可以隶属于不同的年代 。
GC分为四个阶段:
第一:初始标记 标记出直接引用的对象 。STW
第二:标记 , 通过RSet标记出上一个阶段标记的引用到的OId区 。
第三:并发标记阶段:跟CMS的步骤是差不多的 。只是遍历的范围不再是整个OId区 , 而只需要遍历第二步标记出来的
.
第四:重新标记: 跟CM$中的重新标记过程是差不多的 。
第五:垃圾清理:与CMS不同的是 , G1可以采用拷贝算法 , 直接将整个中的对象拷贝到另一个 。而这个阶
段 , G1只选择垃圾较多的来清理 , 并不是完全清理
CMS的核心算法就是三色标记 。
三色标记:是一种逻辑上的抽象 。将每个内存对象分成三种颜色: 黑色:表示自己和成员变量都已经标记完毕 。灰色:自己
标记完了 , 但是成员变量还没有完全标记完 。白色:自己未标记完.
CMS通过增量标记的方式来解决漏标的问题 。
双亲委派模型的好处:
主要是为了安全性 , 避免用户自己编写的类动态替换Java的一些核心类 , 比如 。
同时也避免了类的重复加载 , 因为JVM中区分不同类 , 不仅仅是根据类名 , 相同的class文件被不同的加载就是不同的两个类
说说类加载器双亲委派模型
JVM中存在三个默认的类加载器:
的父加载器是,的父加载器是 。
JVM在加载一个类时 , 会调用的方法来加载这个类 , 不过在这个方法中 , 会先使用的方法来加载类 , 同样的方法中会先使用来加载类 , 如果加载到了就直接成功 , 如果没有加载到 , 那么就会自己尝试加载该类 , 如果没有加载到 , 那么则会由来加载这个类 。
所以 , 双亲委派指得是 , JVM在加载类时 , 会委派给Ext和进行加载 , 如果没加载到才由自己进行加载 。中为什么要使用自定义类加载器
一个中可以部署多个应用 , 而每个应用中都存在很多类 , 并且各个应用中的类是独立的 , 全类名是可以相同的 , 比如一个订单系统中可能存在com..User类 , 一个库存系统中可能也存在com..User类 , 一个 , 不管内部部署了多少应用 , 启动之后就是一个Java进程 , 也就是一个JVM , 所以如果中只存在一个类加载器 , 比如默认的 , 那么就只能加载一个com..User类 , 这是有问题的 , 而在中会为部署的每个应用都生成一个类加载器 , 名字叫做 。这样中的每个应用都可以使用自己的类加载器 。
- x86实模式保护模式
- 四 python学习组合
- 多层神经网络实现猫图片的二分类问题
- 管理类联考——写作——考点+记忆篇——论说文——记忆
- Pytorch之shuffleNet图像分类
- KNN算法 C语言实现对鸢尾花的分类 机器学习
- Pytorch之CIFAR10分类卷积神经网络
- Pytorch之ResNet图像分类
- gensim的LDA做文档主题分类的练习 模型加载和预测
- java认知描述_Java技术:JVM的初步认识