内存划分
说一下jvm的主要组成部分?及其作用?
-
类加载器:加载类文件到内存。
-
本地库接口:本地接口的作用是融合不同的语言为java所用。
-
运行时数据区:
- 程序计数器:指示java虚拟机下一条需要执行的字节码指令。
- 虚拟机栈:虚拟机栈中执行每个方法的时候,都会创建一个栈用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
- 堆:堆是java对象的存储区域,任何new字段分配的java对象实例和数组,都被分配在堆上。jdk1.7以后,运行时常量池从方法区移到了堆上。
- 方法区:用于存储已被虚拟机加载的类信息,常量,静态变量。
- 本地方法栈:与虚拟机栈发挥的作用相似,相比于虚拟机栈为java方法服务,本地方法栈为虚拟机使用的Native方法服务,执行每个本地方法的时候,都会创建一个栈用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
-
执行引擎:也叫解释器,负责解释命令,交由操作系统执行。
说一下堆栈的区别?
- 地址增长方向:栈由高到低,堆由低到高
- 释放方式:栈由编译器自动释放,堆由程序员人工释放
- 局部变量存在栈中,动态数据存在堆中
Java虚拟机为新生对象分配内存有哪些方式?
- 指针碰撞
- 空闲列表
选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又又采取的垃圾收集器是否带有压缩整理功能决定。
内存分配并发问题如何解决?
- CAS+失败重试
CAS是一种乐观锁的实现方式,每次不加锁假设没有冲突而去完成内存分配,当发生冲突时就重试,直到成功为止。 - TLAB
为每个线程预先在Eden区分配一块内存,JVM在给对象分配内存时,首先在TLAB分配,当本地缓冲区用完了,再使用上述CAS+失败重试的方式。
Java对象的内存布局
- 对象头:包括运行时数据和类型指针
- 实例数据:程序代码中所定义的各种类型的字段内容
- 对齐填充
Java对象的访问定位方式
- 使用句柄访问:Java栈本地变量表中的reference -> 句柄池 -> 对象实例数据、对象类型数据
- 直接指针访问:Java栈本地变量表中的reference -> 对象实例数据 -> 对象类型数据
HotSpot使用直接指针访问
类加载过程
1.加载
加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
-
从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
-
从JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
-
通过网络加载class文件。
-
把一个Java源文件动态编译,并执行加载。
类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。
2.链接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。
**1)验证:**验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
四种验证做进一步说明:
**文件格式验证:**主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
**元数据验证:**对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
**字节码验证:**最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
**符号引用验证:**主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
2)**准备:**类准备阶段负责为类的静态变量分配内存,并设置默认初始值。
3)**解析:**将类的二进制数据中的符号引用替换成直接引用。说明一下:符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
3.初始化
初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。
什么时候类加载,类加载时机
- 创建类的实例,也就是new一个对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(Class.forName(“com.lyj.load”))
- 初始化一个类的子类(会首先初始化子类的父类)
- JVM启动时标明的启动类,即文件名和类名相同的那个类
双亲委派机制
Java虚拟机对类加载是按需加载的,需要哪个类文件才去加载并生成class对象,而加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,是一种任务委派模式。
双亲委派机制工作原理
–1.如果一个类加载器收到了类加载请求,并不会自己去加载,而是将请求委托给父类加载器去执行;
–2.如果父类加载器还有父类加载器,继续向上委托,依次递归,直到启动了加载器;
–3.如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器不能完成,子加载器才会尝试自己去加载,这就是双亲委派模式
JVM中表示两个class对象是否为同一个类
-1.在jvm中表示两个class文件是否为同一个类存在两个必要条件
—1.类的完整类名必须一致,包括包名
—2.即使是类的完整类名,同时要求加载这个类的类加载器必要相同,是启动类加载器还是定义类加载器
-2.对类加载器的引用,jvm必须知道一个类型是有启动类加载器加载的还是由用户类加载器加载的,如果一个类型由用户类加载器加载的,那么jvm会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用时候,Jvm需要保证两个类型的加载器是相同的。
垃圾回收
对象是否存活的判定
- 引用计数算法
- 在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加一;当引用失效时,计数器就减一;任何时刻计数器为零的对象就是不可能再被使用的。
- 这种方法很简单,效率也很高,但是需要大量额外处理,比如难以解决对象之间的相互引用问题。
- 可达性分析算法–主流JVM中使用的方法
- 该算法的基本思路是通过一系列称为“GC Roots”的根对象作为起始节点集,根据对象之间的引用关系生成引用链,不在引用链中的对象就会被判定为可回收对象。
垃圾收集算法
- 分代收集理论
- 分代收集理论建立在三个假说之上:
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数
- 根据以上三个假说,Java堆中被划分为新生代和老年代(1:2),新生代又被划分为1个伊甸园和2个幸存区(8:1:1)
- 标记-清除算法
- 首先标记所有被引用的对象,在标记完成之后,对堆内存从头到尾进行线性的遍历,如果发现哪个对象没有标记,就将它回收。
- 缺点:执行效率不稳定,如果存在大量需要被标记的对象,那么标记和清除两个过程的执行效率都会随对象数量的增长而增长;容易产生大量的内存碎片,而且需要维护一个地址列表。
- 标记-复制算法
- 将内存按容量划分为大小相等的两块,每次只使用其中一块,当其中一块内存使用完了之后,将存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次性清理掉。
- 优点:不用考虑内存碎片的问题
- 缺点:如果存在大量存活的对象,那么复制过程将产生大量的开销;可用内存缩小为原来的一半
- 标记-整理算法
- 在标记清除的基础上进行改进,首先还是从根节点开始标记所有被引用对象,让所有存活的对象向内存空间的一端移动,然后清理掉边界以外的对象。
- 与标记-清除算法相比,移动内存能提供更高的吞吐量,而且JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表少了很多开销。
- 优点:1)消除标记-清除算法当中,内存区域不连续,有碎片的缺点,需要给新对象分配内存时,JVM只需要有一个内存的起始地址即可。 2)消除了复制算法当中,内存减半的高额代价。
- 缺点:从效率上来说,标记-整理算法要低于复制算法。 从移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址 移动过程中,需要全程暂停用户应用程序。
内存溢出与内存泄露
- 内存溢出oom:表示没有空闲内存,并且垃圾收集器也无法提供更多内存。
主要的原因有:
— 1.java虚拟机的堆内存设置不够。可以通过参数-Xms、-Xmx来调整
— 2.代码中创建了大量大对象,并且长时间不能被垃圾收集器收集,存在被引用
在抛出oom之前,通常垃圾收集器会被处罚,尽其所能去清理出空间 - 内存泄露memoryleak:只要对象不会再被程序用到了,但是gc又不能回收他们的情况。尽管内存泄露并不会立马引起程序奔溃,但是一旦发生内存泄漏,程序中可用的内存就会被慢慢蚕食,直至耗尽内存,最终出现oom,导致程序崩溃。
—1. 单例模式:单例对象的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,那么会导致内存泄露的产生。
—2. 一些提供close的资源未关闭导致内存泄露:比如数据库连接,网络连接和Io连接必须要手动关闭close,否则是不会被回收的。
如何解决oom
1、要解决OOM异常或heap space的异常,一般的手段是首先通过内存映像分析工具如Eclipse
Memory Analyzer)对dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory overf1ow)
2、如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息,以及GC Roots引用链的信息,就可以比较准确地定位出泄漏代码的位置。
3、如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-xms),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
引用
-1. 强引用:最传统的引用定义,是在程序代码中普遍存在的引用复制,无论任何情况下只要强引用关系还存在,垃圾收集器就无法对它进行回收。
当在Java语言中使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。
-2. 软引用:内存不足的时候,就会对它进行垃圾回收,如果内存一直充足,就不会对它进行回收。软引用通常用来实现内存敏感的缓存。 比如:高速缓存就用到了软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足的时候就清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
-3. 弱引用:只能存活到下一次垃圾回收之前。在系统进行GC的时候,只要发现弱引用,都会将回收掉只被弱引用关联的对象。
软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。
-4. 虚引用:为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。不能单独使用,也无法通过虚引用来获取被引用的对象。
虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。
经典的垃圾收集器
-
Serial收集器
—是一个单线程收集器,作为hotspot中client模式下的默认新生代垃圾收集器,采用的是复制算法、串行回收和stw机制的方式执行内存回收。
优点:对于限定单个cpu的环境来说,Serial收集器由于没有线程交互的开销,所以效率高。
新生代、单线程、复制算法、暂停所有用户线程、客户端下默认新生代收集器 -
ParNew收集器
—ParNew收集器是Serial收集器的多线程版本。采用并行回收的方式执行内存回收,是Server模式下新生代的默认垃圾收集器。
—对于新生代,回收次数频繁,使用并行方式高效。
—对于老年代,回收次数少,使用串行方式节省资源。(cpu并行需要切换线程,串行可以省去切换线程的资源)
– 可以判断纪念馆parnew收集器的回收率在任何场景下都比serial效率高吗?
----在多cpu的环境下,parnew收集器可以更快的完成垃圾收集,提升程序的吞吐量。
----但是在单cpu的环境下,parnew收集器不比serial收集器高校。Serial收集器是基于串行回收的,不需要频繁的切换任务。
ParNew GC能与CMS收集器配合工作
新生代、多线程、复制算法、暂停所有用户线程、JDK 7前服务端模式下首选新生代收集器、(除了多线程以外,基本与Serial没有区别)
-
Parallel Scavenge收集器
— 基于并行回收,采用了复制算法和stw机制。
与ParNew不同,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量,适合在后台运算而不需要太多交互的任务。
和Parallel olld收集器联合使用
新生代、多线程、复制算法、关注吞吐量可控性 -
Serial Old收集器
老年代、单线程、复制算法、暂停所有用户线程、客户端下默认老年代收集器、Serial收集器的老年代版本 -
Parallel Old收集器
老年代、多线程、复制算法、关注吞吐量可控性 -
CMS收集器(Concurrent Mark Sweep)低延迟
–CMS收集器的关注点是尽可能缩短垃圾收集器时用户线程的停顿时间。停顿时间越短就越适合与用户交互的程序。CMS的垃圾收集算法采用的是标记-清除算法,并且也会采用stw的机制。
– 初始标记:在这个阶段,用户线程会因为stw短暂的停顿,主要是标记与GC ROOTS直接关联到的对象,速度非常快。
– 并发标记:从GC ROOTS的直接关联的对象开始遍历,整个过程比较长,但是不需要停顿用户现场,可以与垃圾收集线程并发运行。
– 重新标记:主要是为了修正在并发标记期间,由于用户程序继续运作导致标记产生变动,要比初始标记时间要长,但比并发标记时间短。
– 并发清除:清除那些未被标记的对象,释放内存空间,在这个阶段是可以与用户线程同时并发的。
在cms回收过程中,还应该确保应用程序用户线程有足够的内存使用,所以cms不会像其他收集器那样等到老年代满了才进行垃圾回收,而是当堆内存达到一定的值,便开始进行回收,以确保应用程序在cms工作过程中依然有足够的内存空间支持用户线程运行。当出现concurrent mode failure的时候,虚拟机会临时启动Serial old收集器来重新进行老年代垃圾回收。
缺点:
– 1.会产生内存碎片,导致并发清除后,用户线程可用的空间不足。
– 2.对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程导致应用程序编码,总吞吐量降低。
– 3.cms无法处理浮动垃圾。 可能出现"Concurrent mode failure"失败而导致再一次full gc的产生。在并发标记阶段,由于用户线程并不有停顿,所以会产生新的垃圾对象,cms将无法对这些对象进行标记,最终导致这些新产生的垃圾对象没有及时被回收,只能在下一次执行gc的时候回收。
老年代、步骤:初始标记 -> 并发标记 -> 重新标记 ->并发清除、并发低停顿收集器
补充:在jdk1.5及之前当堆内存使用率达到68%时,就会执行一次cms回收。在jdk1.6以后,默认值为92%。
- Garbage First收集器(G1)
- 基于Region的内存布局,G1将内存“化整为零”,优先处理回收价值收益最大的Region
- 初始标记 -> 并发标记 -> 最终标记 -> 筛选回收
G1收集器主要是针对多核cpu及大容量内存的机器,以极高概率满足gc停顿时间的同时,还兼具有高吞吐量的性能特征。以其他gc收集器相比,G1使用了全新的分区算法。
– 1.并行与并发
—并行性:G1在回收期间,可以有多个gc线程同时工作。
—并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行。并发交替执行,让人感觉不到stw。
– 2.分代收集
—从分代上看,G1依然属于分代型垃圾收集器,会区分年轻代和老年代。从堆的结构上上,不要求整个eden区、年轻代或者老年代都是连续的,也不在坚持固定大小和固定数量。
—将堆空间分为若干个区域,这些区域中包含了了逻辑上的年轻代和老年代。
– 3.空间整合
—G1将内存划分为一个个的region。内存的回收是以region为基本单位的。Region之间是复制算法,但是整体上实际可看做是标记-压缩算法。
– 4.可预测的停顿时间模型(软实时soft real-time)
–G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为m毫秒的时间段内,消耗在垃圾收集器上的时间不得超过n毫秒。
— 由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
— G1跟踪各个Region里面的垃圾堆的价值大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的region。
– G1的设计原则就是简化jvm性能调优,开发人员只需要简单的三步即可完成调优:
— 1.开启G1垃圾收集器
— 2.设置堆的最大内存
— 3.设置最大的停顿时间
– G1适用的场景
— 1.面向服务端应用
— 2.最主要的应用是需要低GC延迟
— 3.用来替换掉1.5版本中cms收集器,在以下的情况时,使用G1可能比CMS好:
— 1.超过50%的Java堆被活动数据所占用时;
— 2.对象分配频率或年代提升频率变化很大;
— 3.GC停顿时间过长,长于0.5到1秒
-
ZGC收集器
-
Shenandoah收集器
-
Epsilon收集器
垃圾收集器的组合
1.针对单线程,一般都将Serial与Serial Old组合使用。
2.针对多线程,一般可以将ParNew/Serial Old组合使用。
3.针对于吞吐量,一般将Parallel Scavenge和Parallel Old组合使用。
4.针对于响应速度,一般将CMS和Parnew联合使用,当CMS期间,发出concurrent mode failure时,改用serial old来替换cms。
????????????补充
排故工具:
- jps:虚拟机进程状况工具
- jstat:虚拟机统计信息监视工具
- jinfo:Java配置信息工具
- jmap:Java内存映像工具
-jconsole:分析内存信息 - jhat:虚拟机堆转储快照分析工具
- jstack:Java堆栈跟踪工具
如何调优
主要从内存占用、延迟、吞吐量三个方面进行调优。
和CAP原则一样,同时满足一个程序内存占用小、延迟低、高吞吐量是不可能的,程序的目标不同,调优时所考虑的方向也不同,在调优之前,必须要结合实际场景,有明确的优化目标,最后进行测试,通过各种监控工具确认调优后的结果是否符合目标。
– 1.jvm调优工具
— 1.调优可以依赖、参考的数据有系统运行日志、堆栈错误信息、gc日志、线程快照、堆转快照等。
— 2.用jps可以查看虚拟机启动的进行
— 3.用jstat监视虚拟机信息
— 4.jmap -dump 可以转存储堆内存快照到指定文件
— 5.jconsole分析内存信息
– 2.Jvm调优经验
— 1.-Xms和-Xmx的值设置成相等,堆大小默认为-Xms指定的大小,默认空闲堆内存小于40%时,jvm会扩大堆到-Xmx指定的大小;空闲堆内存大于70%时,jvm会减小堆到-Xms指定的大小。如果在full gc后满足不了内存需求会动态调整,这个阶段比较耗费资源。
— 2.新生代尽量设置大一些,让对象在新生代多存活一段时间,每次Minor GC的时候都要尽可能多的收集垃圾对象,防止或延迟对象进入老年代的机会,以减少应用程序发生full gc的频率。
— 3.老年代如果使用cms收集器,新生代可研不用太大,因为cms的并行收集速度也很快,收集过程比较耗时的并发表及和并发清除阶段都可以与用户线程并行执行。
— 4.方法区大小的设置。1.6之前的需要考虑系统运行时动态增加的常量、静态变量等。1.7只要差不多能装下启动时和后期动态加载的类信息就行。
代码实现方面,性能出现问题比如程序等待、内存泄漏除了JVM配置可能存在问题,代码实现上也有很大关系:
— 1.避免创建过大的对象及数组:过大的对象或数组在新生代没有足够空间容纳时会直接进入老年代,如果是短命的大对象,会提前出发Full GC。
— 2.避免同时加载大量数据,如一次从数据库中取出大量数据,或者一次从Excel中读取大量记录,可以分批读取,用完尽快清空引用。
— 3.当集合中有对象的引用,这些对象使用完之后要尽快把集合中的引用清空,这些无用对象尽快回收避免进入老年代。
— 4.可以在合适的场景(如实现缓存)采用软引用、弱引用,比如用软引用来为ObjectA分配实例:SoftReference objectA=new SoftReference(); 在发生内存溢出前,会将objectA列入回收范围进行二次回收,如果这次回收还没有足够内存,才会抛出内存溢出的异常。
避免产生死循环,产生死循环后,循环体内可能重复产生大量实例,导致内存空间被迅速占满。
— 5.尽量避免长时间等待外部资源(数据库、网络、设备资源等)的情况,缩小对象的生命周期,避免进入老年代,如果不能及时返回结果可以适当采用异步处理的方式等。