JVM
程序计数器:当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,【线程私有】。
Java虚拟栈:存放基本数据类型、对象的引用、方法出口等,【线程私有】。
本地方法栈:和虚拟栈相似,只不过它服务于Native方法,【线程私有】。
方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,各【线程共享】
Java堆:java内存最大的一块,所有对象实例、数组都存放在java堆,GC回收的地方,【线程共享】。
Java堆
Java堆是java虚拟机所管理内存中最大的一块内存空间,处于物理上不连续的内存空间,只要逻辑连续即可,主要用于存放各种类的实例对象。该区域被所有线程共享,在虚拟机启动时创建,用来存放对象的实例,几乎所有的对象以及数组都在这里分配内存(栈上分配、标量替换优化技术的例外)。
堆的结构
新生代 ( Young )
Eden
From Survivor(S0)
To Survivor(S1)
年轻代空间要点:
大多数新建的对象都位于Eden区。
当Eden区被对象填满时,就会执行Minor GC,并把所有存活下来的对象转移到其中一个survivor区。
Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区。
经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间,通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。
老年代 ( Old )
什么样的对象可以进入老年代?大对象。需要连续大量内存空间的Java对象
长期存活的对象
young GC后S区容纳不下的对象
动态对象进行年龄判定进入老年代
为什么这样划分?
为了使jvm能够更好的管理内存中的对象,包括内存的分配以及回收
新生代按eden和两个survivor的分法的优点有以下几个好处:
有效空间增大,eden+1个survivor
利于对象代的计算,当一个对象在S0/S1中达到设置的XX:MaxTenuringThreshold值后,会将其挪到老年代中,即只需扫描其中一个survivor。如果没有S0/S1,直接分成两个区,该如何计算对象经过了多少次GC还没被释放。
两个Survivor区可解决内存碎片化
重点: 堆(Heap)和JVM栈是程序运行的关键
栈是运行时的单位(解决程序的运行问题,即程序如何执行,或者说如何处理数据),而堆是存储的单位(解决的是数据存储的问题,即数据怎么放、放在哪儿)
堆存储的是对象。栈存储的是基本数据类型和堆中对象的引用;(参数传递的值传递和引用传递)
那为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据,分工明确,处理逻辑更为清晰体现了“分而治之”以及“隔离”的思想。
堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。这样共享的方式有很多收益:提供了一种有效的数据交互方式(如:共享内存);堆中的共享常量和缓存可以被所有栈访问,节省了空间。
栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。
堆和栈的结合完美体现了面向对象的设计。当我们将对象拆开,你会发现,对象的属性即是数据,存放在堆中;而对象的行为(方法)即是运行逻辑,放在栈中。因此编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。
堆栈相关参数
核心参数
-Xms
堆内存初始大小,单位m、g-Xmx
堆内存最大允许大小,一般不要大于物理内存的80%-Xmn
年轻代内存初始大小-Xss
每个线程的堆栈大小,即JVM栈的大小-Xms
和-Xmx
一般设置相等
设置参数的技巧
每次GC 后会调整堆的大小,【为了防止动态调整带来的性能损耗】,一般设置-Xms、-Xmx 相等
推荐使用的是-Xmn参数,原因是这个参数很简洁,相当于一次性设定NewSize和MaxNewSIze,而且两者相等
JVM对象
创建对象的方式
使用new关键字。 调用无参或有参构造器函数创建
使用Class的newInstance方法。调用无参或有参构造器函数创建,且需要是publi的构造函数
使用Constructor类的newInstance方法。调用有参和私有private构造器函数创建,实用性更广
使用Clone方法。 不调用任何参构造器函数,且对象需要实现Cloneable接口并实现其定义的clone方法,且默认为浅复制
第三方库Objenesis。 利用了asm字节码技术,动态生成Constructor对象
jvm对象分配
在虚拟机层面上创建对象的步骤
对象分配内存方式
指针碰撞
空闲列表
垃圾回收算法分类
1. 标记-清除(Mark—Sweep): 被誉为现代垃圾回收算法的思想基础
【标记-清除算法】采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,
并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片
2. 复制算法(Copying)
该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。建立在存活对象少,垃圾对象多的前提下。此算法每次只处理正在使用中的对象,因此复制成本比较小,
同时复制过去后还能进行相应的内存整理,不会出现碎片问题。但缺点也是很明显,就是需要两倍内存空间。
3. 标记-整理(或标记-压缩算法,Mark-Compact,又或者叫标记清除压缩MarkSweepCompact)
此算法是结合了“标记-清除”和“复制算法”两个算法的优点。避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,
并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,
一般增加句柄和句柄表。
4. 分代回收策略(Generational Collecting)
新生代由于其对象存活时间短,且需要经常gc,因此采用效率较高的复制算法,其将内存区分为一个eden区和两个suvivor区,默认eden区和survivor区的比例是8:1,分配内存时先分配eden区,当eden区满时,使用复制算法进行gc,将存活对象复制到一个survivor区,当一个survivor区满时,将其存活对象复制到另一个区中,当对象存活时间大于某一阈值时,将其放入老年代。老年代和永久代因为其存活对象时间长,因此使用标记清除或标记整理算法
该算法思路
不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率
JVM七种垃圾收集器
Serial 收集器 复制算法,单线程,新生代)
ParNew 收集器(复制算法,多线程,新生代)
Parallel Scavenge 收集器(多线程,复制算法,高吞吐量,新生代)
Serial Old 收集器(标记-整理算法,老年代)
Parallel Old 收集器(标记-整理算法,注重吞吐量的场景下,jdk8默认采用 Parallel Scavenge + Parallel Old 的组合,老年代)
CMS 收集器(标记-清除算法,老年代,垃圾回收线程几乎能做到与用户线程同时工作,吞吐量低,内存碎片)以牺牲吞吐量为代价来获得最短回收停顿时间-XX:+UseConcMarkSweepGC
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代) ,jdk1.9 默认垃圾收集器G1应用程序对停顿比较敏感
在JVM中,有相对较多存活时间较长的对象(老年代比较大)会更适合使用CMS
触发GC的场景
触发FullGC的原因
显式调用system.gc()
Heap inspection。 主动触发Full GC,(执行jmap -histo:live [pid])来避免碎片问题,一般在空闲时间对JVM进行一次内存释放
老年代空间不足
永久代(方法区)空间不足
JVM自身固定评率的Full GC
堆内存中分配的很大的对象以致超过了老年代神域的空间,此时会触发Full GC
触发Young GC的原因
Eden空间不足
Full GC
如果判断一个对象是否存活?
一般判断对象是否存活有两种算法,一种是引用计数,另外一种是可达性分析。
java是根据什么来执行可达性分析的? 根据GC ROOTS。GC ROOTS可以的对象有:【虚拟机栈中的引用对象】,【方法区的类变量的引用】,【方法区中的常量引用】,【本地方法栈中的对象引用】。
GC Roots有以下几种:
系统类加载器加载的对象
处于激活状态的线程
JNI栈中的对象
正在被用于同步的各种锁对象
JVM自身持有的对象,比如系统类加载器等。
JVM三种类加载器
启动类加载器(home) 加载jvm核心类库,如java.lang.*等
扩展类加载器(ext), 父加载器为启动类加载器,从jre/lib/ext下加载类库
应用程序类加载器(用户classpath路径) 父加载器为扩展类加载器,从环境变量中加载类
JVM 类加载顺序
加载:获取类的二进制字节流,将其静态存储结构转化为方法区的运行时数据结构
校验:文件格式验证,元数据验证,字节码验证,符号引用验证
准备:在方法区中对类的static变量分配内存并设置类变量数据类型默认的初始值,不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中
解析:将常量池内的符号引用替换为直接引用的过程
符号引用:字符串,能根据这个字符串定位到指定的数据,比如java/lang/StringBuilder
直接引用:内存地址
初始化:为类的静态变量赋予正确的初始值(Java代码中被显式地赋予的值)
JVM类加载是如何进行的?
双亲委派机制
类加载器收到类加载的请求
把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器
启动类加载器检查能不能加载,能就加载(结束);否则,抛出异常,通知子加载器进行加载
保障类的唯一性和安全性以及保证JDK核心类的优先加载
双亲委派模型有啥作用?
保证java基础类在不同的环境还是同一个Class对象,避免出现了自定义类覆盖基础类的情况,导致出现安全问题。还可以避免类的重复加载。
如何打破双亲委派模型?
自定义类加载器,继承ClassLoader类重写loadClass方法;
SPI(Service Provider interface)
服务提供接口(服务发现机制):
通过加载ClassPath下META_INF/services,自动加载文件里所定义的类
通过ServiceLoader.load/Service.providers方法通过反射拿到实现类的实例
SPI应用应用于JDBC获取数据库驱动连接过程就是应用这一机制
apache最早提供的common-logging只有接口.没有实现..发现日志的提供商通过SPI来具体找到日志提供商实现类
tomcat是如何打破双亲委派模型?
tomcat有着特殊性,它需要容纳多个应用,需要做到应用级别的隔离,而且需要减少重复性加载,所以划分为:
/common 容器和应用共享的类信息,
/server容器本身的类信息,
/share应用通用的类信息,
/WEB-INF/lib应用级别的类信息。
整体可以分为:
boostrapClassLoader->ExtensionClassLoader->ApplicationClassLoader->CommonClassLoader
->CatalinaClassLoader(容器本身的加载器)/ShareClassLoader(共享的)->WebAppClassLoader。
虽然第一眼是满足双亲委派模型的,但是不是的,因为双亲委派模型是要先提交给父类装载,而tomcat是优先判断是否是自己负责的文件位置,进行加载的。
双亲委派机制缺陷
双亲委派核心是越基础的类由越上层的加载器进行加载, 基础的类总是作为被调用代码调用的API,无法实现基础类调用用户的代码….
JNDI服务它的代码由启动类加载器去加载,但是他需要调独立厂商实现的应用程序,如何解决?
线程上下文件类加载器(Thread Context ClassLoader), JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC
堆外内存的优缺点
堆外内存,其实就是不受JVM控制的内存。
Ehcache中的一些版本,各种 NIO 框架,Dubbo,Memcache 等中会用到,NIO包下ByteBuffer来创建堆外内存。
相比于堆内内存有几个优势:
减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作。
加快了复制的速度。因为堆内在 flush 到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了复制这项工作。
可以扩展至更大的内存空间。比如超过 1TB 甚至比主存还大的空间。
缺点:
堆外内存难以控制,如果内存泄漏,那么很难排查,通过-XX:MaxDirectMemerySize来指定,当达到阈值的时候,调用system.gc来进行一次full gc
堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合
排查命令
jstat查看内存回收概况,实时查看各个分区的分配回收情况
jmap查看内存栈,查看内存中对象占用大小
jstack查看线程栈,死锁,性能瓶颈
GC种类
相关面试题
什么情况下会发生栈内存溢出?
思路
描述栈定义,再描述为什么会溢出,再说明一下相关配置参数
根据思路回答
栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常,方法递归调用产生这种结果。
如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory 异常。(线程启动过多)
参数 -Xss 去调整JVM栈的大小
线上出现堆内存溢出,怎么排查?(涉及到Jvm)
GC种类
Major GC: 老年代的垃圾收集叫做Major GC,Major GC通常是跟full GC是等价的,收集整个GC堆
分代GC:
Young GC:只收集年轻代的GC
Old GC:只收集年老代的GC(只有CMS的concurrent collection是这个模式)
Mixed GC:收集整个young gen以及部分old gen的GC(只有G1有这个模式)
Full GC: Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。
MAT分析工具
JVM在线看GC的命令
JVM相关的启动参数:
-XX:+HeapDumpOnOutOfMemoryError
: 从字面就可以很容易的理解,在发生OutOfMemoryError异常时,进行堆的Dump,这样就可以获取异常时的内存快照了。-XX:HeapDumpPath=D:\heap-dump\`: 这个也很好理解,就是配置HeapDump的路径,方便我们管理,这里我们配置为D:\heap-dump,当然你也可以根据自己的需要,定义为其他的目录。 - 如何排查 - jps: 虚拟机进程状况工具。使用jps找出这个进程在本地虚拟机的唯一ID,因为在后面的排查过程中都是需要这个VMID来确定要监控的是哪一个虚拟机进程。 -
jps [ options ] [ hostid ]`jstat:虚拟机统计信息监视工具。查看已使用空间站总空间的百分比;各个JVM内存占比及垃圾回收次数及耗时.
jstat [ option vmid [interval[s|ms] [count]] ]
,例如,jstat -gcutil 20954 1000
gcutil指:已使用空间站总空间的百分比。20954指:pid。1000指:每1000毫秒查询一次,一直查。
找出导致频繁Full GC的原因
把堆dump下来再用MAT等工具进行分析,但dump堆要花较长的时间,并且文件巨大,再从服务器上拖回本地导入工具,这个过程有些折腾,不到万不得已最好别这么干。
更轻量级的在线分析,使用“Java内存影像工具:jmap”生成堆转储快照(一般称为headdump或dump文件)。
jmap [ option ] vmid
, 例如: jmap -histo:live 20954