学习java虚拟机
- 1、类加载的五个过程:加载、验证、准备、解析、初始化
- 2、类的加载器的分类
- 2.1、双亲委派机制
- 3、类的加载器的分类
- 4、Java虚拟机栈
- 5、Java本地方法栈
- 6、Java虚拟机栈
- 7、Java堆
- 8、Java方法区
- 9、Java常量池
- 10、Java对象创建步骤
- 11、Java对象的内存结果
- 12、Java访问对象
- 13、JavaGC垃圾回收
- 13.1、引用计数法
- 13.2、Java可达性分析法
- 13.3、Java标记清除
- 13.4、Java标记复制算法(处理新生代,小部分移动大部分回收)
- 13.5、Java标记整理
- 14、JVM垃圾收集器
- 14.1、Serial收集器
- 14.2、ParNew收集器
- 14.3、Parallel Scavenge收集器
- 14.3、CMS垃圾收集器
- 14.4、G1垃圾收集器
- 15、Java内存分配
- 15.1、Java堆内存区域的划分以及作用讲解
- 15.2、Jvm大对象分配原则
- 15.3、逃逸分析和栈上分配
- 16、Java虚拟机工具
- 16.1、使用虚拟机工具jps
- 16.2、使用虚拟机工具jstat与jinfo
- 16.3、使用虚拟机工具jmap
- 16.4、使用虚拟机工具jhat
- 16.5、使用虚拟机工具jstack
- 面试题模块
- 线程死锁是什么?线程状态有哪些?他们之间是怎么切换的
- 关于频繁下载FullGC的解决方法
1、类加载的五个过程:加载、验证、准备、解析、初始化
类的个生命周期如下图:
-
加载:根据查找路径找到相应的class文件,然后导入。类的加载方式分为
隐式加载和显示加载两种。隐式加载指的是程序在使用new关键词创建对象时,会隐式的调用类的加载器把对应的类加载到jvm中。显示加载指的是通过直接调用class.forName()方法来把所需的类加载到jvm中。 -
检查:检查夹加载的class文件的正确性。
-
准备;给类中的静态变量分配内存空间。
-
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址。
-
初始化:对静态变量和静态代码块执行初始化工作。
2、类的加载器的分类
- 隔离加载类
- 修改类的加载方式
- 扩展加载源
- 防止源码泄露
2.1、双亲委派机制
为什么要设计这种机制
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
3、类的加载器的分类
栈是运行时的单位,而堆是存储的单位
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
4、Java虚拟机栈
是什么
用于作用于方法执行的一块java内存区域
为什么
每个方法在执行的同时都会创建一个栈帧(Stack Framel)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
特点
局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)以及对象引用(reference类型)
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
5、Java本地方法栈
是什么
用于作用域本地方法执行的一块Java内存区域什么是本地方法?
为什么
与Java虚拟机栈相同,每个方法在执行的同时都会创建一个栈帧(Stack Framel)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
特点
Hotshot将Java虚拟机栈和本地方法栈合二为一
6、Java虚拟机栈
是什么
程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
为什么
为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为"线程私有""的内存
特点
内存区域中唯一一个没有规定任何OutOfMemoryError 情况的区域
7、Java堆
是什么
是Java内存区域中一块用来存放对象实例的区域,几乎所有的对象实例都在这里分配内存
为什么
此内存区域的唯一目的就是存放对象实例
Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块Java堆是被所有线程共享的一块内存区域
特点
Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做"GC堆”(Garbage
Java堆可以分成新生代和老年代新生代可分为To Space、From Space、Eden
8、Java方法区
是什么
是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
什么是类信息:类版本号、方法、接口
为什么
内存中存放类信息、静态变量等数据,属于线程共享的一块区域
Hotspot使用永久代来实现方法区JRockit、IBM J9VM Java堆一样管理这部分内存
特点
并非数据进入了方法区就如永久代的名字一样"永久"存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载
方法区也会抛出OutofMemoryError,当它无法满足内存分配需求时
9、Java常量池
运行时常量池的模拟
public class A {public static void main(String[] args) {String a = "abc";String b = "abc";System.out.printiln(a==b) ;//trueString c = new String("abc"); System.out.println(a==c);//false由于c内存分到到堆里,而a和b是在常量池中System.out.println(a=-c.intern());//true将c的内存移动到方法区运行时常量池中
)
是什么
运行时常量池是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
特点
运行时常量池是方法区的一部分,受到方法区内存的限制,当常量池再申请到内存时会抛出OutOfMemoryError异常
10、Java对象创建步骤
对象创建的流程步骤包括哪些:
虚拟机遇到一条new指令时,首先检查这个对应的类能否在常量池中定位到一个类的符号引用
判断这个类是否已被加载、解析和初始化
为这个新生对象在Java堆中分配内存空间,其中Java堆分配内存空间的方式主要有以下两种
1、指针碰撞
1.1、分配内存空间包括开辟—块内存和移动指针两个步骤
1.2、非原子步骤可能出现并发问题,Java虚拟机采用GAS配上失败重试的方式保证更新操作的原子性
2、空闲列表
2.1、分配内存空间包括开辟一块内存和修改空闲列表两个步骤
2.2、非原子步骤可能出现并发问题,Java虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
将分配到的内存空间都初始化为零值
设置对象头相关数据
1、GC分代年龄
2、对象的哈希码hashCode
3、元数据信息
执行对象<init>方法
11、Java对象的内存结果
对象头用于存储对象的元数据信息:
Mark Word部分数据的长度在32位和64位虚拟机(未开启压缩指针)中分别为32bit和64bit,存储对象自身的运行时数据如哈希值等。Mark Word一般被设计为非固定的数据结构,以便存储更多的数据信息和复用自己的存储空间。
类型指针指向它的类元数据的指针,用于判断对象属于哪个类的实例。
实例数据存储的是真正有效数据,如各种字段内容,各字段的分配策略为longs/doubles、ints,shorts/chars、bytes/boolean、oops(ordinary object pointers),相同宽度的字段总是被分配到一起,便于之后取数据。父类定义的变量会出现在子类定义的变量的前面。
对齐填充部分仅仅起到占位符的作用
12、Java访问对象
当我们在堆上创建一个对象实例后,就要通过虚拟机栈中的reference类型数据来操作堆上的对象。现在主流的访问方式有两种(HotSpot虚拟机采用的是第二种)∶
1.使用句柄访问对象。即reference中存储的是对象句柄的地址,而句柄中包含了对象实例数据与类型数据
的具体地址信息,相当于二级指针。
2.直接指针访问对象。即reference中存储的就是对象地址,相当于一级指针。
对比
垃圾回收分析∶方式①当垃圾回收移动对象时,reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址;方式②垃圾回收时需要修改reference中存储的地址。
访问效率分析,l方式二优于方式一,因为方式二只进行了一次指针定位,节省了时间开销,而这也是HotSpot采用的实现方式。
13、JavaGC垃圾回收
战略意义 能做出一个需求的同时也要懂得其对应的战略意义
为什么要垃圾回收?
Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解。由于有个垃圾回收机制,Java中的对象不再有"作用域"的概念,只有对象的引用才有"作用域"。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存
垃圾回收的过程是怎样的?
如果让你考虑垃圾回收算法你会怎么设计
完成哪些对象回收那些对象不回收的功能需求
对象是否存活判断
堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量赋值为这个对象的引用时,计数加1 (a= b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1
13.1、引用计数法
优缺点
引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.
13.2、Java可达性分析法
可达性分析算法的概念(又叫跟搜索法
根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点
java中可作为GC Root的对象有
虚拟机栈中引用的对象(本地变量表)
本地方法栈中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
13.3、Java标记清除
最基础的收集算法是"标记-清除”(Mark-Sweep)算法,如同它的名字一样,算法分为"标记"和“清除"两个阶段:
1.1、首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程其实在前–节讲述对象标记判定时已经介绍过了。
1.2、它的主要不足有两个:
一个是效率问题,标记和清除两个过程的效率都不高;
另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
13.4、Java标记复制算法(处理新生代,小部分移动大部分回收)
为了解决效率问题,一种称为“复制”"(Copying)的收集算法出现了,它将可用内存按量划分为大小相等的两块,每次只使用其中的一块
当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效
现在的商业虚拟机都采用这种收集算法来回收新生代,研究表明,新生代中的对象98%是“朝生夕死""的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot 虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被"浪费"。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
13.5、Java标记整理
标记整理算法解决了什么问题
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法
标记-整理
根据老年代的特点,有人提出了另外一种"标记-整理(Mark-Compact)算法,标记过程仍然与“标记-清除"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集
一般把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用"标记-清理"或者"标记一整理"算法来进行回收
14、JVM垃圾收集器
14.1、Serial收集器
是什么
收集算法是内存收到的方法论,垃圾回收器是内存回收的具体实现。
Serial是一个单线程的垃圾收集器
特点
“Stop The World”,它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。在用户不可见的情况下把用户正常工作的线程全部停掉
14.2、ParNew收集器
是什么
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial 收集器可用的所有控制参数(例如:-XX: SurvivorRatio、-XX: PretenureSize’ Threshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial 收集器完全一样,在实现上,这两种收集器也共用了相当多的代码
特点
ParNew收集器除了多线程收集之外,其他与Serial 收集器相比并没有太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial 收集器外,目前只有它能与CMS 收集器配合工作。
使用-XX: ParallelGCThreads参数来限制垃圾收集的线程数
多线程操作存在上下文切换的问题,所以建议将-XX: ParallelGCThreads设置成和CPU核数相同,如果设置太多的话就会产生上下文切换消耗
并发与并行的概念讲解
并行(Parallel)∶指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent)︰指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上
14.3、Parallel Scavenge收集器
是什么
Parallel Scavenge 收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器
由于与吞吐量关系密切,Parallel Scavenge 收集器也经常称为"吞吐量优先"收集器
吞吐量是什么? CPU用于运行用户代码的时间与CPU总时间的比值,99%时间执行用户线程,1%时间回收垃圾,这时候吞吐量就是99%
特点
Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge 收集器的目标则是达到个可控制的吞吐
(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应调节策略
-XX:MaxGCPauseMillis参数GC停顿时间,50OMB ——>300MB,这个参数配置太小的话会发生频繁GC
-XX:GCTimeRatio参数,99%
Serial old收集器,它是一个单线程收集器,使用"标记–整理"算法
Parrallel old收集器Parallel Scavenge收集器的老年代版本,使用多线程+标记整理算法
14.3、CMS垃圾收集器
是什么
CMS (Concurrent Mark Sweep)收集器是-种以获取最短回收停顿时间为目标的收集器。
目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
CMS 收集器是基于“标记-清除"算法实现的
步骤流程
初始标记(CMS initial mark)----标记一下 GC Roots能直接关联到的对象,速度很快
并发标记(CMS concurrent mark -…----并发标记阶段就是进行GC RootsTracing 的过程
重新标记(CMS remark) -.-…-为了修正并发标记期间因用户程序导致标记产生变动的标记记录
并发清除(CMS concurrent sweep)
CMS垃圾收集器缺点
对CPU资源非常敏感
无法处理浮动垃圾,程序在进行并发清除阶段用户线程所产生的新垃圾
标记-清除暂时空间碎片
14.4、G1垃圾收集器
是什么
G1是一款面向服务端应用的垃圾收集器
特点
G1中每个Region都有一个与之对应的Remembered Set,当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏﹐检查Reference引用的对象是否处于不同的Region
G1收集器的运作大致可划分为以下几个步骤
初始标记(Initial Marking) --标记一下GC Roots能直接关联到的对象
并发标记(Concurrent Marking)—从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行
最终标记(Final Marking)–为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中
筛选回收(Live Data Counting and Evacuation)
G1的优势有哪些
空间整合:基于"标记一整理"算法实现为主和Region之间采用复制算法实现的垃圾收集
可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型
在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java雄划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔高的了,它们都是一部分Region(不需要连续)的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Regions里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region (这也就是Garbage-Firsti名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高
15、Java内存分配
15.1、Java堆内存区域的划分以及作用讲解
对象分配的规则有哪些
对象主要分配在新生代的Eden区上
如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配
少数情况下也可能会直接分配在老年代中
GC参数指定垃圾回收
-Xms20 M、-Xmx20 M、-Xmn1 0M这3个参数限制了Java堆大小为20 MB,不可扩展,其中10 MB分配给新生代,剩下的10MB分配给老年代。-Xx: SurvivorRatio=8决定了新生代中Eden 区与两个Survivor区的空间比例是8:1
新生代与老年代
新生代GC(Minor GC)∶指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度老年代 GC(Major GC/ Full GC)∶指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在 Parallel Scavenge 收集器的收集策略里就有直接进行Major GCE的策略选择过程)。Major GCl的速度一般会比 Minor Gct慢10倍以上。
15.2、Jvm大对象分配原则
是什么
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组
虚拟机提供了一个-XX: PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制
实战代码演练大对象配置
-verbose:gc -XX:+PrintGCDetails开启GC日志打印
-Xms20 M设置JVM初始内存为20M
-Xmx20 M设置JVM最大内存为20M
-Xmn10 M设置年轻代内存大小为10M
15.3、逃逸分析和栈上分配
逃逸分析
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸
栈上分配
栈上分配就是把方法中的变量和对象分配到栈上,方法执行完后自动销毁,而不需要垃圾回收的介入,从而提高系统性能。
-XX:+DoEscapeAnalysis开启逃逸分析(jdk1.8默认开启,其它版本未测试)
-XX :-DoEscapeAnalysis关闭逃逸分析
两种逃逸的举例
JDK1.8默认开启逃逸分析
public class TestEscape{public static 0bject obj ;public void variableEscape(){obj = new 0bject(); //发生逃逸)public Object methodEscape(){return new Object(); //方法逃逸}public static void alloc()ibyte[] b = new byte[2];b[0]=1;)public static void main(String[] args){long start =System.currentTimeMillis();for (int i =0; i < 108080880 ; i++) {alloc();}long end = System.currentTimeMillis();System.out.println(end-start);))
16、Java虚拟机工具
16.1、使用虚拟机工具jps
16.2、使用虚拟机工具jstat与jinfo
16.3、使用虚拟机工具jmap
16.4、使用虚拟机工具jhat
16.5、使用虚拟机工具jstack
面试题模块
线程死锁是什么?线程状态有哪些?他们之间是怎么切换的
线程死锁是什么
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
Blocked状态与Waiting状态的区别
WAITING状态属于主动地显式地申请的阻塞,BLOCKED 则属于被动的阻塞
线程状态分类
NEW
RUNNABLE
BLOCKED 一个正在阻塞等待一个监视器的线程处于这一状态
WAITING一个正在无限期等待另一个线程执行一个特别的动作的线程处于这一状态
TIMED_WAITING一个正在限时等待另一个线程执行一个动作的线程处于这一状态
TERMINATED
关于频繁下载FullGC的解决方法
FullGC与MinorGC讲解