本文基于周志明写的《深入理解java虚拟机》,汇聚书本知识点和常见面试题
电子版链接:https://pan.baidu.com/s/1BnVE5yeI60jbR-wxLDGgZw
-
- 第一部分:走进Java
- 第二部分:自动内存管理机制
- 第三部分:虚拟机执行子系统
- 第四部分:程序编译与代码优化
- 第五部分:高效并发
- 笔试题汇总
-
- 1.描述执行引擎的架构设计
- 2.描述JVM体系结构
- 3.描述JVM编译优化
- 4.什么是类加载器,ClassLoader(类加载器)有哪些
- 5.描述ClassLoader的作用(什么是类加载器)和加载过程
- 6.JVM加载class文件到内存的两种方式
- 7.加载类错误分析及其解决
- 8.Java应不应该动态加载类(JVM能不能动态加载类)
- 9.Java中哪些组件需要使用内存
- 10.描述JVM内存结构及内存溢出。
- 11.描述JVM内存分配策略
- 12.描述JVM如何检测垃圾
- 13.哪些元素可作为GC Roots
- 14.描述分代垃圾收集算法的思路:
- 15.描述基于分代的堆结构及其比例。
- 16.描述垃圾收集算法
- 17.描述新生代和老年代的回收策略
- 18.描述CMS垃圾收集器
- 19.对象创建方法,对象的内存分配,对象的访问定位。
- 20.GC收集器有哪些?CMS收集器与G1收集器的特点。
- 21.几种常用的内存调试工具
- 22.类加载的五个过程:加载、验证、准备、解析、初始化。
- 23.“地球人都知道,Java有个东西叫垃圾收集器,它让创建的对象不需要像c/cpp那样delete、free掉,你能不能谈谈,GC是在什么时候,对什么东西,做了什么事情?”
- 24.jvm中一次完整的GC流程(从ygc到fgc)是怎样的,重点讲讲对象如何晋升到老年代等
- 25.java内存模型
- 26.类加载器双亲委派模型机制?
-
第一部分:走进Java
1.走进Java
1.讲了什么
- java技术体系如何实现
- 了解java的特性实现原理
- java现在未来趋势
- 独立编译OpenJDK7
2.精华笔记
- Java优点:一次编写到处运行;提供相对安全的内存管理和访问机制;避免绝大部分的内存泄露和指针越界问题;实现热点代码检测;运行时编译及优化,随着运行时间的增加获得更高的性能;完善的应用程序接口;开源社区第三方类库
- jdk:java程序设计语言+java虚拟机+javaAPI类库—-java开发的最小环境
- jre:javaAPI类库中的javaseapi子集+java虚拟机—-java程序运行的标准环境
- jdk1.5更新的内容:自动装箱、泛型、动态注解、枚举、可变长参数、遍历循环等
- jdk1.6更新内容:普通对象指针压缩功能;植入压缩指令节省内存消耗
- jdk1.7~jdk1.8更新内容:二进制的原声支持;switch支持字符串;<>操作符;异常处理改进;简化变长参数方法调用;面向资源的try/catch/finally、lambda表达式
- java程序运行在64位虚拟机需要付出比较大的代价:消耗更多的内存、运行速度全面落后于32位
3.疑问
- 虚拟机只有java吗?–很多其他语言都要虚拟机,相当于一个进程
- 测试switch字符串;参数变长;trycatch;的变化
- 64位系统和32位的区别,为什么64虚拟机慢
- 什么是虚拟集群
第二部分:自动内存管理机制
2.Java内存区域与内存溢出异常
1.讲了什么
- 了解虚拟机如何使用内存
- 虚拟机内存各个区域、作用、服务对象、可能产生的问题
2.精华笔记
虚拟机自动内存管理机制作用:我们new操作后,不需要手动释放内存,不容易出现内存泄露和内存溢出
java虚拟机在执行java程序的过程中会把管理的内存划分位若干个不同的区域:方法区;虚拟机栈;本地方法栈;堆;程序计数器
java内存模型
内存区域–程序计数器:
- 作用:当前线程所执行的字节码的行号指示器
- 引申概念:
- 字节码解释器的工作就是通过改变计数器的值来选取下一条需要执行的字节码指令(cpu指示器),分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成
- java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,每条线程都需要一个独立的程序计数器,各个线程之间的计数器互不影响,独立储存,我们称这类内存区域为“线程私有”的内存
- 如果线程正在执行一个java方法,计数器记录的是虚拟机字节码指令;如果执行natvie方法,则计数器的值位空,此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
java虚拟机栈:
- 作用:描述java方法执行的内存模型,每个方法被执行的时候都会同时创建一个栈帧,用于储存局部变量表,操作栈,动态链接,方法出入口等信息。每个方法被调用直至完成的过程,就对应栈帧从虚拟机从入栈到出栈的过程。
- 引申概念:
- 与程序计数器一样也是线程私有的
- 生命周期与线程相同
- 人们常把java内存区域粗糙地划分位堆和栈,栈指的就是虚拟机栈,或者说是虚拟机栈中的局部变量表部分
- 局部变量表:存放编译器的各种基本数据类型(8种)、对象引用类型、returnAddress类型(指向了一条字节码指令的地址)。
- 64位的long、double占用两个局部变量空间,其余数据类型只占用一个
- 局部变量表所需的内存空间在编译器完成分配,当进入一个方法时,这个方法需要在帧分配多大的局部变量空间完全确定,在方法运行期间不会改变局部变量表的大小
- java虚拟机规范规定了这个区域的两种异常情况:线程请求的栈深度大于虚拟机所允许的深度会跑出StackOverflowEorror异常;如果虚拟机可以动态拓展,当拓展无法申请足够的内存时会抛出OutOfMemoryError异常
- 栈帧是方法运行期基础的数据结构
本地方法栈:
- 作用:虚拟机栈为虚拟机执行java方法(字节码)服务,而本地方法栈为虚拟机使用道德native方法服务
- 引申概念:虚拟机规范没有强制规定本地方法栈的语言、使用方式,数据结构;具体虚拟机可以自由实现它,有的虚拟机直接把它和虚拟机栈合二为一
java堆:
作用:所有new出来的对象内存都分配在堆中,堆被分成年轻代(Yong Generation)和老年代(Old Generation),新生代又分成Eden和两个Survivor区。
引申概念:
java堆是java虚拟机所管理的内存中最大的一块
java堆是所有线程共享的一块内存区域,在虚拟机启动时创建
所有的对象实例以及数组都要在堆分配,但随着jit编译器的发展和逃逸分析技术成熟,栈上分配、标量替换优化技术导致对象分配在堆变得不那么“绝对”
堆的分类:堆是垃圾回收器管理主要区域,所以也称GC堆,从内存回收角度,垃圾收集器基本采用分代收集算法,所以java堆还可以细分新生代和老年代,细致一点还有Eden空间、From Survivor空间、To Survivor空间等;从内存分配角度看,线程共享的java堆可能划分出多个线程私有的分配缓存区。堆的分类与内容无关,无论那个区域储存的都是对象实例,划分只是更好回收更快回收内存
java堆可以是物理上不连续的内存空间,只要逻辑连续就可以了
-Xmx和-Xms控制虚拟机可拓展
在JVM运行时,可以通过配置以下参数改变整个JVM堆的配置比例
JVM运行时堆的大小
-Xms堆的最小值
-Xmx堆空间的最大值
新生代堆空间大小调整
-XX:NewSize新生代的最小值
-XX:MaxNewSize新生代的最大值
-XX:NewRatio设置新生代与老年代在堆空间的大小
-XX:SurvivorRatio新生代中Eden所占区域的大小
永久代大小调整
-XX:MaxPermSize
其他
-XX:MaxTenuringThreshold,设置将新生代对象转到老年代时需要经过多少次垃圾回收,但是仍然没有被回收
方法区:
- 作用:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
- 引申概念:
- 与堆一样,是各个线程共享的内存区域
- java虚拟机把它划分位堆的逻辑部分,但它却又一个别名叫”non-heap”
- 很多人把方法区称为”永久代”,但本质上两者不等价,仅仅因为hotspot虚拟机设计团队把GC分代收集拓展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机,不存在永生代
- 方法区不需要连续的内存、可以选择固定大小、可拓展还可以不实现垃圾收集,相对而言,垃圾收集行为在这个区域比较少出现
- 这个区域内存回收目标主要是对常量池的回收和堆类型的卸载;一般来说这个区域回收十分必要,在sun公司曾经出现若干个重大bug就是由于此区域未完全回收导致的内存泄露。
- 当方法区无法满足内存需求时,讲抛出OutOfMemoryError异常
- 运行时常量池:方法区的一部分,用于存放编译器生成的各种字面量和符号引用,这部分会在类加载后存放到方法区的运行时常量池
- 运行时常量池相对于Class文件常量池的另外一个特征是具备动态性,java语言并不要求长常量一定只能编译器产生,可以再运行期将新的常量放入池中,这种特性被开发人员利用得比较多的便是OutOfMemoryError异常。
非虚拟机内存区域–直接内存:
- 作用:
- 引申概念:
- 它并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域,但它被经常使用,可能导致OutOfMemoryError异常
- NIO引入基于通道与缓冲区的I/O方式,它可以使用函数库直接分配堆外内存,然后通过java堆的directbytebuffer对象作为这块内存的引用进行操作,这样避免在java堆和native堆中来回复制数据,提高性能
- 管理员配置虚拟机参数时,不能忽略直接内存,防止各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致OutOfMemoryError异常
- 对象访问:分析Object obj = new Object()的内存关系,Object obj 会反映到java栈的本地变量表中,作为一个引用对象类型出现。而new Object()会反映到java堆中,形成一块Object类型所有实例数据值的结构化内存(这个对象里面包含的基本类型的数据等),这块内存长度不固定。java堆中还必须包括能查找到此对象的数据类型的地址信息(如对象类型,父类,实现的接口,方法等),这些数据类型储存在方法区
对象引用和对象之间的访问方式有两种:使用句柄和直接指针
句柄访问方式:java堆中会划分一块内存作为句柄池,对象引用中储存的就是对象的句柄地址,而句柄中包含了对象实例数据和对象类型数据各自的具体地址信息
指针访问方式:java堆对象的布局就必须考虑如何放置访问类型数据的相关信息,对象引用直接存储对象地址
句柄好处:对象被移动时(垃圾收集时移动对象很频繁)只改变句柄中实例数据指针,而对象引用本身不需要被修改
直接指针好处:速度快,节省了一次指针定位开销
出现OutOfMemoryError:java.lang.OutOfMemoryError: Java heap space解决方法。首先确认内存中的对象是否必要(分清楚内存泄露还是内存溢出)–使用Eclipse Memory Analyzer打开堆转存快照文件。如果是内存泄露,进一步通过工具查看泄露对象到GC Roots的引用链,找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收它们。如果不存在泄露,那就是内存对象还必须存活,那就检查下虚拟机堆参数(-Xmx与-Xms)与机器物理内存对比看是否还可以调大;从代码上看是否某些对象生命周期过长、持续状态时间过长的情况,尝试减少程序运行期内存消耗
hotspot虚拟机不区分本地方法栈和虚拟机栈,-Xoss参数(设置本地方法栈大小)虽然存在,但无效;栈容量只由-Xss参数设定
在单个线程下,无论是栈帧太大还是虚拟机容量太小,当内存无法分配时,虚拟机抛出的都是StackOverflowError异常;在多线程中,不断建立线程能造成内存溢出
在开发高并发的项目时,出现栈超出异常有错误堆栈可以阅读比较容易找问题,但如果是建立过多线程导致内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,只能通过减少最大对和减少栈容量换取更多的线程。
参数代码:
- -XX:PermSize和-XX:MaxPermSize**方法区**容量
- -Xmx和-Xms**堆**最大容量和堆最小容量
- -Xss**栈**内存容量
- -Xoss**本地方法栈**容量(已失效)
- -XX:MaxDirectMemorySizez**直接内存**,如果不指定直接内存同堆最大容量-Xmx一样
3.疑问
- 字节码是二进制吗?–10110
- 什么是虚拟机的概念模型?
- 虚拟机字节码指令是什么?
- -Xmx和-Xms有什么用?
- NIO类是什么,使用了解一下?
- java堆和native堆区别?
- 本地变量表和局部变量表区别?
- 读完本书再回来看下实战部分,了解大概什么代码,有触发OOM问题
3.垃圾收集器与内存分配策略
1.讲了什么
- java垃圾收集机制如何解决内存溢出异常
- 为什么了解GC和内存分配–当需要排查各种内存溢出,内存泄露问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化”的技术进行监控和调节
- 垃圾收集算法
- 几款jdk1.6垃圾收集器特点和运作原理
- 验证java虚拟机自动内存分配及回收的主要规则
- 虚拟机提供不同的收集器,参数组合,没有最有的调优方法,所以需要学习虚拟机内存知识,了解每个收集器的行为,优势劣势,调节参数
2.精华笔记
虚拟机栈、本地方法栈、程序计数器随线程而生灭;唯独java堆和方法区这部分的内存动态需要监管
如何判断一个对象是否存活:引用计数算法、根搜索算法
- 引用计数算法:给对象一个引用计数器,每当有一个地方引用它时,计数器的值+1;当引用失效时,计数器-1;在任何时刻计数器为0的对象就是不可能再被引用了。
- 根搜索算法:通过一系列”GC Roots”为起始点,从这些节点往下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明对象不可用。
GC Roots对象有那些:
- 虚拟机栈的引用对象
- 方法区类静态属性引用的对象
- 方法区常量引用的对象
- 本地方法栈JNI(Native方法)的引用对象
引用概念拓展:
- 强引用:类似Object obj = new Object();只要强引用在,垃圾回收期永远不会回收被引用的对象
- 软引用:还有用,但非必须的对象;在系统内存溢出前,将会把这些对象列入回收范围并二次回收;如果第二次回收还是没内存才报异常
- 弱引用:非必须对象;弱引用关联的对象只能生存到下一次垃圾回收发生之前
- 虚引用:最弱的一种引用关系;虚引用无法获得实例对象,唯一目的是希望能在这个对象回收之前收到系统通知
根搜索算法不可到达的对象,不一定马上死去,需要经过两次标记过程。当发现不可达,会标记第一次并进行一轮筛选是否有必要执行finalize()方法;如果有必要执行,这个对象会被放到F-Queue队列排队中等待死亡(根据优先级调用finalize()方法),如果期间重新被引用,则逃脱死亡,如果没必要执行,没有说明~~
一个对象的finalize只会执行一次,也就是只能自救一次
finalize()运行代价高,不确定性大
新生代进行一次垃圾收集能回收70%-95%内存,而永久代远远低于次
永久代回收两部分内容:废弃常量和无用的类
如何判定废弃常量:一个String”abc”进入常量池,当没有任何对象引用它,就会被请出常量池
如何判断无用的类:该类所有实例已经被回收;加载该类的ClassLoader被回收;该类的java.lang.Class对象没有任何地方引用,即没有反射这个类—-满足上面的条件就可以被回收,但不是必然被回收3
垃圾收集算法:
- 标记-请除算法:
- 过程:先标记所有需要回收的对象,标记完成后统一回收所有标记对象
- 优缺:效率不高;标记清除产生大量不连续片段,当程序需要下次需要分配较大内存时无法找到足够大的连续内存而不得不触发另一次垃圾收集动作。
- 复制算法:
- 过程:把内存划分为可用的两块,每次只用其中一块,当一块内存用完,就将还存活的对象复制到另一块上面,然后把已经使用过的内存空间一次清理掉
- 优缺:把内存缩小原来一半,代价太高,而且在存活对象比较多的时候,复制操作变多,效率变低
- 使用情况:现在基本采用这种方法回收新生代,但不需要1:1平分内存,而是分为一块较大的Eden空间和两块小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor存活对象拷贝到另一块Survivor,最后清理掉刚刚使用的Eden、Survivor。当Survivor空间不够时,需要依赖其他内存(老年代)进行分配担保。
- 延伸知识:
- 分配担保:Survivor没有足够空间存放存活对象,会把这些对象通过分配担保机制进入老年代
- 标记-整理算法:
- 过程:跟标记清除的标记部分一样,清除部分替换为让所有存活的对象都向一段移动,然后直接清除端边界以外的内存
- 分代收集算法
- 过程:根据对象存活周期不同把内存分为新生代和老年代,根据各个年代的特征采用最适当的收集算法;新生代有大批对象死去,少量存活采用复制算法;老年代存活率高,用标记整理/标记清除
- 使用情况:当前虚拟机垃圾收集都采用这种算法
- 标记-请除算法:
垃圾收集器
Serial收集器:
- 过程:单线程收集器,在进行垃圾回收的时候,必须停掉用户的线程
- 优缺:运行时必须让用户等待差评;优点是简单高效(与其他收集器单线程比)
- 使用情况:虚拟机运行在Client模式下默认的新生代收集器
ParNew收集器
- 过程:Serial的多线程版
- 优缺:除了Serial只有它能与CMS收集器配合工作;对付老年代无法与Parallel Scavenge配合工作
- 使用情况:Server模式下虚拟机**首选新生代收集器**3.疑问
Parallel Scavenge收集器
- 过程:新生代收集器,复制算法,并行多线程
- 优缺:达到可控制吞吐量(吞吐量:运行用户代码时间/运行用户代码时间+垃圾收集)
Serial Old收集器
- 过程:Serial的老年代版本,单线称收集器,标记整理算法
- 优缺:
- 使用情况:Server模式使用
Parallel Old收集器
- 过程:Parallel Scavenge老年代版本,多线程,标记整理算法
CMS收集器
- 过程:4个步骤:初始标记–并发标记–重新标记–并发清除
- 优缺:重视服务响应速度,并发收集,低停顿了;无法处理浮动垃圾;标记清除算法造成大量碎片
G1收集器
- 过程:将整个java堆划分为多个固定大小的独立区域(Region),并跟踪区域的垃圾堆积程度,在后台维护一个优先列表,根据允许的收集时间,优先收回垃圾最多的区域
- 优缺:标记整理算法;精确控制停顿
- 使用情况:收集器最前沿(JDK1.7)成果
内存分配和回收策略
(1)对象优先分配在Eden,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
(2)大对象(比如很长的字符串、数组)直接进入老年代,这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存) (提醒一句,编程时少创建“短命大对象”,大对象容易让内存还有不少的时候触发GC来安置他们)
(3)长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
(4)幸存区相同年龄对象的占幸存区空间的多于其一半,将进入老年代
(5)空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
java垃圾回收
GC介绍
Minor GC
在年轻代
Young space
(包括Eden区和Survivor区)中的垃圾回收称之为 Minor GC. 这个定义既清晰又无异议。 但仍有一些有趣的关于Minor GC事件的东西你需要了解:- Minor GC总是在不能为新的对象分配空间的时候触发, 例如 Eden区满了,分配空间的越快,Minor GC越频繁。
- 当内存池慢了后, 它的完整的内容会被复制出去,指针可以从0开始重新跟踪空闲内存。所以取代传统的标记-交换-压缩(Mark, Sweep , Compact), Eden区和Survivor区使用标记-复制方式(Mark , Copy). 因此在Eden区和Survivor区无内存碎片。写指针总是指向内存池的顶部。
- 在Minor GC时, 年老代(Tenured generation)可以被忽略. 年老代对年轻代的引用被认为是实际的GC根root。 在标记阶段年轻代对年老代的引用可以被简单的忽略。
- 出于常理, 所有的Minor GC都会触发stop-the-world暂停, 它意味着会暂停应用的所有线程. 对于大部分应用而言,暂停的延迟可以忽略不计。这是因为Eden中大部分的对象都可以垃圾回收掉,而不会被复制到Survivor/Old区。但如果相反,大部分的新对象不能被回收, Minor GC暂停会占用更多的时间。
综上所述,
Minor GC
概念相当清晰 – 每次Minor GC只会清理年轻代.Major GC 清理年老区(Tenured space).
Full GC 清理整个内存堆 – 既包括年轻代也包括年老代
4.虚拟机性能监控与故障处理工具
- 了解内存分析工具和调优案例
- 给系统定位问题的时候,知识经验是关键基础,数据是依据,工具是运用知识处理数据的手段。
- 介绍了JDK发布的6个命令行工具与2个可视化故障处理工具
5.调优案例分析与实战
- 传授经验案例,通过实战获得故障处理和调优的经验
- 虚拟机的内存管理和垃圾收集是虚拟机重要组成部分,对程序的性能和稳定性又非常大的影响
第三部分:虚拟机执行子系统
6.类文件结构
- 解析Class文件的数据结构–java技术体系的基础支柱–了解Class文件结构对后面进一步了解虚拟机执行引擎有用
- 了解Class文件各个组成部分,每个部分的定义,数据结构(储存格式)和使用方法
- 了解Class数据如何存储和访问
7.虚拟机类加载机制
- 以动态的、运行时的角度看字节码流在虚拟机执行引擎中怎么解释执行
- 虚拟机如何加载Class文件
- Class文件中的信息在进入虚拟机会发生什么变化
- 在类加载过程的”加载”、”验证”、“准备”、”解析”、”初始化”时,虚拟机进行了什么操作
- 类加载器的原理以及对虚拟机的意义
8.虚拟机字节码执行引擎
- 虚拟机如何执行定义在Class文件里的字节码
- 从概念模型的角度讲方法调用和字节码的执行
- 虚拟机执行代码如何找到正确方法
- 如何执行方法内的字节码
- 执行时涉及到的内存结构
9.类加载及执行子系统的案例与实战
- 虚拟机如何运行Class文件的概念模型
第四部分:程序编译与代码优化
10.早起(编译器)优化
- 介绍源代码编译成字节码和从字节码编译成本地机器码的过程
11.晚期(运行期)优化
- 探索即时编译器的运作过程
- 了解虚拟机的热点探索方法
- hotspot即时编译器、编译出发条件、以及如何从虚拟机外部观察和分析JIT编译的数据和结果
- 编译器优化技术
第五部分:高效并发
12.Java内存模型与线程
- 虚拟机如何实现多线程
- 多线程的共享和竞争数据引发的问题和解决方案
- 了解虚拟机Java内存模型的结构和操作
- 讲解原子性、可见性、有序性在java内存模型的体现
- 先行发生原则的规则和使用
- 虚拟机如何实现并发
13.线程安全与锁优化
- 线程安全所涉及的概念和分类、同步实现的方式
- 虚拟机底层运作原理
- 虚拟机实现高效并发所做的一些列锁优化措施
笔试题汇总
1.描述执行引擎的架构设计
创建新线程时,JVM会为这个线程创建一个栈,同时分配一个PC寄存器(指向第一行可执行的代码)。调用新方法时会在这个栈上创建新的栈帧数据结构。执行完成后方法对应的栈帧将消失,PC寄存器被销毁,局部变量区所有值被释放,被JVM回收。
2.描述JVM体系结构
(1)类加载器:JVM启动时或者类运行时将需要的class加载到JVM中。每个被装载的类的类型对应一个Class实例,唯一表示该类,存于堆中。
(2)执行引擎:负责执行JVM的字节码指令(CPU)。执行引擎是JVM的核心部分,作用是解析字节码指令,得到执行结果(实现方式:直接执行,JIT(just in time)即时编译转成本地代码执行,寄存器芯片模式执行,基于栈执行)。本质上就是一个个方法串起来的流程。每个Java线程就是一个执行引擎的实例,一个JVM实例中会有多个执行引擎在工作,有的执行用户程序,有的执行JVM内部程序(GC).
(3)内存区:模拟物理机的存储、记录和调度等功能模块,如寄存器或者PC指针记录器。存储执行引擎执行时所需要存储的数据。
(4)本地方法接口:调用操作系统本地方法返回结果
3.描述JVM编译优化
早期(编译器):
很少;编译时,为节省常量池空间,能确定的相同常量只用一个引用地址。
晚期(运行期):
方法内联:去除方法调用的成本;为其他优化建立良好基础,便于在更大范围采取连续优化的手段。
冗余访问消除:公共子表达式消除
复写传播:完全相等的变量可替代
无用代码消除:清除永远不会执行的代码
(1)公共子表达式消除(语言无关):如果公共子表达式已经计算过了,并且没有变化,那就没有必要再次计算,可用结果替换。
(2)数组边界检查消除(语言相关):限定循环变量在取值范围之间,可节省多次条件判断。
(3)方法内联(最重要):去除方法调用的成本;为其他优化建立良好基础,便于在更大范围采取连续优化的手段。
(4)逃逸分析(最前沿):分析对象的动态作用域;变量作为调用参数传递到其他方法中-方法逃逸;被外部线程访问-线程逃逸。
栈上分配-减少垃圾系统收集压力
同步消除-如果无法逃逸出线程,则可以消除同步
标量替换-将变量恢复原始类型来访问
小抄:final修饰的局部变量和参数,在常量池中没有符号引用,没有访问标识,对运行期是没有任何影响的,仅仅保证其编译期间的不变性。
4.什么是类加载器,ClassLoader(类加载器)有哪些
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。
(1)Bootstrap ClassLoader(启动类加载器):完全由JVM控制,加载JVM自身工作需要的类(JAVA_HOME/lib)
(2)Extension ClassLoader(扩展类加载器):属于JVM自身一部分,不是JVM自身实现的(JAVA_HOME/lib/ext)
(3)Appclication ClassLoader(应用程序类加载器):父类是Extension ClassLoader,加载Classpath(用户类路径)上的类库
5.描述ClassLoader的作用(什么是类加载器)和加载过程
将Class文件加载到JVM中、审查每个类由谁加载(父优先的等级加载机制)、将Class字节码重新解析成JVM统一要求的对象(Class对象)格式。
.class->findclass->Liking:Class规范验证、准备、解析->类属性初始化赋值(static块的执行)->Class对象(这也就是为什么静态块只执行一次)
5.描述JVM类加载机制
ClassLoader首先不会自己尝试去加载类,而是把这个请求委托给父类加载器完成,每一个层次都是。只有当父加载器反馈无法完成请求时(在搜索范围内没有找到所需的类),子加载器才会尝试加载(等级加载机制、父优先、双亲委派)。
好处:类随着它的加载器一起具有一种带有优先级的层次关系;保证同一个类只能被一个加载器加载。
6.JVM加载class文件到内存的两种方式
(1)隐式加载:继承或者引用的类不在内存中
(2)显式加载:代码中通过调用ClassLoader加载
7.加载类错误分析及其解决
(1)ClassNotFoundException:没有找到对应的字节码(.class)文件;检查classpath下有无对应文件
(2)NoClassDefFoundError:隐式加载时没有找到,ClassNotFoundException引发NoClassDefFoundError;确保每个类引用的类都在classpath下
(3)UnsatisfiedLinkError:(未满足链接错误)删除了JVM的某个lib文件或者解析native标识的方法时找不到对应的本地库文件
(4)ClassCastException:强制类型转换时出现这个错误;容器类型最好显示指明其所包含对象类型、先instanceof检查是不是目标类型,再类型转换
(5)ExceptionInitializerError:给类的静态属性赋值时
8.Java应不应该动态加载类(JVM能不能动态加载类)
JVM中对象只有一份,不能被替换,对象的引用关系只有对象的创建者持有和使用,JVM不可干预对象的引用关系,因为JVM不知道对象是怎么被使用的,JVM不知道对象的运行时类型,只知道编译时类型。
但是可以不保存对象的状态,对象创建和使用后就被释放掉,下次修改后,对象就是新的了(JSP)。
9.Java中哪些组件需要使用内存
(1)Java堆:存储Java对象
(2)线程:Java运行程序的实体
(3)类和类加载器:存储在堆中,这部分区域叫永久代(PermGen区)
(4)NIO:基于通道和缓冲区来执行I/O的新方式。
(5)JNI:本地代码可以调用Java方法,Java方法也可以调用本地代码
10.描述JVM内存结构及内存溢出。
JVM是按照运行时数据的存储结构来划分内存结构的。
PC寄存器数据:严格来说是一个数据结构,保存当前正在执行的程序的内存地址。为了线程切换后能恢复到正确的执行位置,线程私有。不会内存溢出。
(1)Java栈:方法执行的内存模型,存储线程执行所需要的数据。线程私有。
–OutOfMemoryError:JVM扩展栈时无法申请到足够的空间。一个不断调用自身而不会终止的方法。
–StackOverflowError:请求的栈深度大于JVM所允许的栈深度。创建足够多的线程。
(2)堆:存储对象,每一个存在堆中Java对象都是这个对象的类的副本,复制包括继承自他父类的所有非静态属性。线程共享。
–OutOfMemoryError:对象数量到达堆容量限制。可通过不断向ArrayList中添加对象实现。
(3)方法区:存储类结构信息。包括常量池(编译期生产的各种字面量和符号引用)和运行时常量池。线程共享。
–OutOfMemoryError:同运行时常量池。
(4)本地方法栈:与Java栈类似,为JVM运行Native方法准备的空间。线程私有。(C栈)OutOfMemoryError和StackOverflowError同JVM栈。
(5)运行时常量池:代表运行时每个class文件中的常量表。运行期间产生的新的常量放入运行时常量池。
–OutOfMemoryError:不断向List中添加字符串,然后String.inern(),PermGen Space(运行时常量池属于方法区)。
(6)本地直接内存:即NIO。
–OutOfMemoryError:通过直接向操作系统申请分配内存。
11.描述JVM内存分配策略
(1)对象优先分配在Eden,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
(2)大对象(比如很长的字符串、数组)直接进入老年代,这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存) (提醒一句,编程时少创建“短命大对象”,大对象容易让内存还有不少的时候触发GC来安置他们)
(3)长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
(4)幸存区相同年龄对象的占幸存区空间的多于其一半,将进入老年代
(5)空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
12.描述JVM如何检测垃圾
通过可达性分析算法,通过一些列称为GC Roots的对象作为起始点,从这些起始点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达),则证明这个对象是不可用的。
使用可达性分析算法而不是引用计数算法。因为引用计数算法很难解决对象之间相互循环引用的问题。
13.哪些元素可作为GC Roots
(1)JVM栈(栈帧中的本地变量表)中的引用
(2)方法区中类静态属性引用
(3)方法区中常量引用
(4)本地方法栈中JNI(一般的Native方法)引用
14.描述分代垃圾收集算法的思路:
把对象按照寿命长短来分组,分为年轻代和老年代,新创建的在老年代,经历几次回收后仍然存活的对象进入老年代,老年代的垃圾频率不像年轻代那样频繁,减少每次收集都去扫描所有对象的数量,提高垃圾回收效率。
15.描述基于分代的堆结构及其比例。
(1)年轻代(Young区-1/4):Eden+Survior(From+To)(1/8,这个比例保证只有10%的空间被浪费,保证每次回收都只有不多于10%的对象存活)
(2)老年代(Old区 ):存放几次垃圾收集后存活的对象
(3)永久区(Perm区):存放类的Class对象
16.描述垃圾收集算法
(1)标记-清除算法:首先标记处所要回收的对象,标记完成后统一清除。缺点:标记效率低,清除效率低,回收结束后会产生大量不连续的内存碎片(没有足够连续空间分配内存,提前触发另一次垃圾回收)。适用于对象存活率高的老年代。
(2)复制算法(Survivor的from和to区,from和to会互换角色):
将内存容量划分大小相等的两块,每次只使用其中一块。一块用完,就将存活的对象复制到另一块,然后把使用过的一块一次清除。不用考虑内存碎片,每次只要移动顶端指针,按顺序分配内存即可,实现简单运行高效。适用于新生代。
缺点:内存缩小为原来的一般,代价高。浪费50%的空间。
(3)标记-整理算法:
标记完成后,将存活的对象移动到一端,然后清除边界以外的内存。适用于对象存活率高的老年代。
(4)分代收集算法
根据对象存活周期不同把内存分为新生代和老年代,根据各个年代的特征采用最适当的收集算法;新生代有大批对象死去,少量存活采用复制算法;老年代存活率高,用标记整理/标记清除
17.描述新生代和老年代的回收策略
18.描述CMS垃圾收集器
CMS 收集器:Concurrent Mark Sweep 并发标记-清除。重视响应速度,适用于互联网和B/S系统的服务端上。初始标记还是需要Stop the world 但是速度很快。缺点:CPU资源敏感,无法浮动处理垃圾,会有大量空间碎片产生。
19.对象创建方法,对象的内存分配,对象的访问定位。
创建:
\1. 类加载检查
JVM遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类的加载过程。
\2. 对象分配内存
对象所需内存的大小在类加载完成后便完全确定(对象内存布局),为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
根据Java堆中是否规整有两种内存的分配方式:(Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定)
指针碰撞(Bump the pointer)
Java堆中的内存是规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存也就是把指针向空闲空间那边移动一段与内存大小相等的距离。例如:Serial、ParNew等收集器。
空闲列表(Free List)
Java堆中的内存不是规整的,已使用的内存和空闲的内存相互交错,就没有办法简单的进行指针碰撞了。虚拟机必须维护一张列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。例如:CMS这种基于Mark-Sweep算法的收集器。
\3. 并发处理
对象创建在虚拟机中时非常频繁的行为,即使是仅仅修改一个指针指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
同步
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)
把内存分配的动作按照线程划分为在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存(TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配。只有TLAB用完并分配新的TLAB时,才需要同步锁定。
\4. 内存空间初始化
虚拟机将分配到的内存空间都初始化为零值(不包括对象头),如果使用了TLAB,这一工作过程也可以提前至TLAB分配时进行。
内存空间初始化保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
注意:类的成员变量可以不显示地初始化(Java虚拟机都会先自动给它初始化为默认值)。方法中的局部变量如果只负责接收一个表达式的值,可以不初始化,但是参与运算和直接输出等其它情况的局部变量需要初始化。
\5. 对象设置
虚拟机对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
\6. 执行init( )
在上面的工作都完成之后,从虚拟机的角度看,一个新的对象已经产生了。但是从Java程序的角度看,对象的创建才刚刚开始init()方法还没有执行,所有的字段都还是零。
所以,一般来说(由字节码中是否跟随invokespecial指令所决定),执行new指令之后会接着执行init()方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算产生出来。
访问定位:句柄或者直接指针。
20.GC收集器有哪些?CMS收集器与G1收集器的特点。
串行垃圾回收器(Serial Garbage Collector)
并行垃圾回收器(Parallel Garbage Collector)
并发标记扫描垃圾回收器(CMS Garbage Collector)
G1垃圾回收器(G1 GarbageCollector)
并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
当标记的引用对象在tenured区域;
在进行垃圾回收的时候,堆内存的数据被并发的改变。
相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。
通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域
21.几种常用的内存调试工具
jmap、jstack、jconsole。
22.类加载的五个过程:加载、验证、准备、解析、初始化。
加载:
在加载阶段,虚拟机主要完成三件事:
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。
3.在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区域数据的访问入口。
验证:
验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害。如果验证失败,就会抛出一个java.lang.VerifyError异常或其子类异常。验证过程分为四个阶段:
1.文件格式验证:验证字节流文件是否符合Class文件格式的规范,并且能被当前虚拟机正确的处理。
2.元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范。
3.字节码验证:主要是进行数据流和控制流的分析,保证被校验类的方法在运行时不会危害虚拟机。
4.符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。
准备:
准备阶段为变量分配内存并设置类变量的初始化。在这个阶段分配的仅为类的变量(static修饰的变量),而不包括类的实例变量。对已非final的变量,JVM会将其设置成“零值”,而不是其赋值语句的值:
pirvate static int size = 12;
那么在这个阶段,size的值为0,而不是12。 final修饰的类变量将会赋值成真实的值。
解析:
解析过程是将常量池内的符号引用替换成直接引用。主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。
初始化:
在准备阶段,类变量已经经过一次初始化了,在这个阶段,则是根据程序员通过程序制定的计划去初始化类的变量和其他资源。这些资源有static{}块,构造函数,父类的初始化等。
至于使用和卸载阶段阶段,这里不再过多说明,使用过程就是根据程序定义的行为执行,卸载由GC完成。
23.“地球人都知道,Java有个东西叫垃圾收集器,它让创建的对象不需要像c/cpp那样delete、free掉,你能不能谈谈,GC是在什么时候,对什么东西,做了什么事情?”
什么时候 :eden满了minor gc,升到老年代的对象大于老年代剩余空间full gc,或者小于时被HandlePromotionFailure参数强制full gc;gc与非gc时间耗时超过了GCTimeRatio的限制引发OOM,调优诸如通过NewRatio控制新生代老年代比例,通过MaxTenuringThreshold控制进入老年前生存次数等
对什么东西:从root搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象
做什么事情:能说出诸如新生代做的是复制清理、from survivor、to survivor是干啥用的、老年代做的是标记清理、标记清理后碎片要不要整理、复制清理和标记清理有有什么优劣势等;还能讲清楚串行、并行(整理/不整理碎片)、CMS等搜集器可作用的年代、特点、优劣势,并且能说明控制/调整收集器选择的方式
24.jvm中一次完整的GC流程(从ygc到fgc)是怎样的,重点讲讲对象如何晋升到老年代等
答:对象优先在新生代区中分配,若没有足够空间,Minor GC;
大对象(需要大量连续内存空间)直接进入老年态;长期存活的对象进入老年态。如果对象在新生代出生并经过第一次MGC后仍然存活,年龄+1,若年龄超过一定限制(15),则被晋升到老年态。
25.java内存模型
java内存模型(JMM)是线程间通信的控制机制.JMM定义了主内存和线程之间抽象关系。线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。Java内存模型的抽象示意图如下:
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
\1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
\2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。**
26.类加载器双亲委派模型机制?
当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载。
JVM相关链接: