这几天研习了一下《深入理解Java虚拟机》这本书,算是补补课、充充电,有边看边记笔记的习惯(不然看完还是忘),分享给大家。稍微有些长,但还是没有各个方面覆盖到,希望最好能够帮助到一些人。
一、JVM:
虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。
二、Java内存区域与内存溢出异常:
1,运行时数据区域
①、程序计数器(Program Counter Register)
②、java虚拟机栈(Java Virtual Machine Stacks)
就是平时说的堆内存、栈内存中的栈,存放了各种基本数据类型、对象引用。
如果线程请求的栈深度大于虚拟机锁允许的深度,报StackOverflowError。
如果虚拟机栈可以动态扩展时无法申请到足够内存,报OutOfMemeryError。
③、本地方法栈(Native Method Stack)
与虚拟机栈作用类似,区别是虚拟机栈为虚拟机执行的Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。
也会报StackOverflowError和OutOfMemeryError
④、Java堆(Java Heap)
Java Heap是Java虚拟机锁管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建,所有的对象实例以及数组都要在堆上分配。
Java Heap是垃圾收集器管理的主要区域,所以也叫做“GC堆”。
如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemeryError
堆内存被划分为两个区:新生代(Young Generation)和老年代(Old Generation)
新生代又被分为三个区:Eden、From Survivor、To Survivor
新生代中98%的对象都是“朝生夕死”的,所以不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
Eden和Survivor比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。我们没有办法保证每次回收都只有不多于10%的对象存货,所以当Survivor空间不够用时,需要依赖其他内存(指老年代)进行分配担保(Handle Promotion)。
⑤、方法区(Method Area)—永久代
系统分配的一个内存逻辑区域,是用来存储类型信息的(可以理解为类的描述信息)
特点:方法区是线程安全的、方法区的大小不固定、方法区也可被垃圾收集
存放的内容:类的全限定名(全路径名)、直接超类的全限定名、访问修饰符、是类还是接口等等
当方法区无法满足内存分配需求时,将抛出OutOfMemeryError
⑥、运行时常量池(Runtime Constant Pool)
方法区的一部分,当常量池无法再申请到内存时会抛出OutOfMemeryError
三、虚拟机类加载机制
1,类加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括加载Loading、验证Verification、准备Preparation、解析Resolution、初始化Initialization、使用Using、卸载Unloading这7个阶段。其中验证、准备、解析3个部分统称为连接Linking
①、加载Loading
加载阶段,虚拟机需要完成3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
②、验证Verification
验证是连接阶段的第一步,主要用来确保加载进来的字节流符合JVM规范。
大致上完成4个阶段的检验动作:
- 文件格式验证
- 元数据验证(是否符合JAVA语言规范)
- 字节码验证(确定程序语义合法,符合逻辑)
- 符号引用验证(确保下一阶段的解析能正常进行)
③、准备Preparation
准备是连接阶段的第二步,为静态变量分配内存并设置类变量初始值的阶段,比如为基本数据类型来赋初始值。
④、解析Resolution
解析是连接阶段的第三步,是虚拟机将常量池内的符号引用替换为直接引用的过程。
⑤、初始化Initialization
初始化阶段是类加载过程的最后一步,主要是根据程序中的赋值语句主动为类变量赋值
什么时候需要对类进行初始化?
①、使用new该类实例化对象的时候;
②、读取或设置类静态字段的时候(但被final修饰的字段,在编译器时就被放入常量池的静态字段除外static final);
③、调用类静态方法的时候;
④、使用反射Class.forName(“xxxx”)对类进行反射调用的时候,该类需要初始化;
⑤、 初始化一个类的时候,有父类,先初始化父类(注:1. 接口除外,父接口在调用的时候才会被初始化;2.子类引用父类静态字段,只会引发父类初始化);
⑥、 被标明为启动类的类(即包含main()方法的类)要初始化;
⑦、当使用JDK1.7的动态语言支持时,如果一个java.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
2,类加载器 ClassLoader
①、概念
ClassLoader的作用就是将class文件加载到JVM中去,而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的。
②、Java提供的三个ClassLoader
从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类的加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且都继承于抽象类java.lang.ClassLoader。
划得更细致一些,有以下三种:
A、Bootstrap ClassLoader(启动类加载器)
负责加载JDK的核心类库,如rt.jar、resources.jar、charsets.jar等(<JAVA_HOME>\lib目录中的)
Bootstrap ClassLoader不继承自ClassLoader也不是普通Java类,底层由C++编写,已经潜入到了JVM内核当中
B、Extension ClassLoader(扩展类加载器)
负责加载JDK的扩展类库(<JAVA_HOME>\lib\ext目录中的)
C、Application ClassLoader(应用程序类加载器或系统类加载器)
负责加载用户路径Classpath上所指定的类库
3,双亲委派模型
①、双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载
java.lang.ClassLoader.loadClass():先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载
②、为什么要用双亲委托这种模型?
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。(一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。比如java.lang.Object,他存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有这种模型,由各个类自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将一片混乱)
③、如何判定两个class是相同的?
不仅要判断两个class类名是否相同,还要判断是否由同一个类加载器实例加载
④、自定义类加载器
分为两步:1)继承java.lang.ClassLoader,2)重写父类的findClass方法
四、虚拟机字节码执行引擎
没看,这部分希望以后有时间补上吧
五、Java内存模型
1,主内存与工作内存
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
2,内存间的交互操作
Java内存模型定义了8种原子操作来完成主内存与工作内存的交互
lock(锁定)、unlock(解锁)、read(读取)、load(载入)、
use(使用)、assign(赋值)、store(存储)、write(写入)
如果要把一个变量从主内存复制到工作内存:顺序地执行read和load操作
如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作
3,volatile
关键字volatile可以说是java虚拟机提供的最轻量级的同步机制。
volatile变量对所有线程是立即可见的,对volatile变量所有的写操作都能立刻反应到其他线程中,换句话说,volatile变量在各个线程中是一致的,但是基于volatile变量的运算在并发下不一定是安全的
什么时候适用Volatile:
①、运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
②、变量不需要与其他的状态变量共同参与不变约束。
六、垃圾收集器与内存分配策略
1,如何确定对象已死
①引用计数法:对象加个计数器,有地方引用就+1,引用失效就-1;为0就是对象不再使用。不推荐使用,主要原因是它很难解决对象之间相互循环引用的问题
②可达性分析算法:通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的
java中,可作为GC Roots的对象包括下面几种:
A、虚拟机栈(栈帧中的本地变量表)中引用的对象
B、方法区中类静态属性引用的对象
C、方法区中常量引用的对象
D、本地方法栈中JNI(一般说Native方法)引用的对象
【网上答案:GC Roots对象】
* 类,由系统类加载器加载的类。这些类从不会被卸载,它们可以通过静态属性的方式持有对象的引用。注意,一般情况下由自定义的类加载器加载的类不能成为GC Roots
* 线程,存活的线程
* Java方法栈中的局部变量或者参数
* JNI方法栈中的局部变量或者参数
* JNI全局引用
* 用做同步监控的对象
* 被JVM持有的对象,这些对象由于特殊的目的不被GC回收。这些对象可能是系统的类加载器,一些重要的异常处理类,一些为处理异常预留的对象,以及一些正在执行类加载的自定义的类加载器。但是具体有哪些前面提到的对象依赖于具体的JVM实现
2,垃圾收集算法
①、标记-清除(Mark-Sweep)算法
缺点:A、效率问题,标记和清除两个过程的效率都不高
B、空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到最够的连续内存而不得不提前出发另一次垃圾收集动作
②、复制(Copying)算法
将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的内存用完了,就将还存活这的对象复制到另外一块上面,然后再把已经使用过的内存空间一次清理掉。现在商业虚拟机都是用这种算法来回收新生代。
优点:每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:对象存活率较高时就要进行较多的复制操作,效率将会变低
③、标记-整理(Makr-Compact)算法
标记过程仍然与“标记-清除”算法一样,单后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
④、分代收集(Generational Collection)算法
当前商业虚拟机的垃圾收集都采用此算法,此算法根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存货对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或“标记-整理”算法来进行回收。
3,垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
①、Serial收集器
最基本、最悠久的收集器,是一个单线程的收集器,在进行垃圾收集的时候,必须暂停其他所有的工作线程,直到它收集结束,即“Stop The World”
优点:简单高效(与其他收集器的单线程比);只要不是频繁发生,停顿时间是可接受的
②、ParNew收集器
Serial收集器的多线程版本
③、Parallel Scavenge收集器
一个新生代收集器、复制算法收集器、并行的多线程收集器,也称为“吞吐量优先”收集器
该收集器的目标是达到一个可控制的吞吐量(Throughput),吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
④、Serial Old收集器
Serial收集器的老年代版本,单线程收集器,使用“标记-整理”算法
⑤、Parallel Old收集器
Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
⑥、CMS(Concurrent Mark Sweep)收集器
一种以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法实现。
整个过程分为四步:初始标记(CMS initial mark)、并发标记(CMS concurrent mark)、重新标记(CMS remark)、并发标记(CMS concurrent mark)
优点:并发收集、低停顿
缺点: A、对CPU资源非常敏感
B、无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生
C、由于是基于“标记-清除”算法实现的,所以收集结束会有大量空间碎片产生
⑦、G1(Garbage-First)收集器
目前最前沿的收集器,与其他GC收集器相比,G1具备如下特点:并行与并发、分代收集、空间整合、可预测的停顿
注:
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户仍然处于等待状态
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上
4,内存分配策略
对象的内存分配,往大方向将,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB(Thread Local Allocation Buffer本地线程分配缓冲)上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数设置。
①、对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC
②、大对象直接进入来年代
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组
③、长期存活的对象将进入老年代
虚拟机给每个对象顶一个了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加一岁,当它的年龄增加到一定程度(默认15岁),就回被晋升到老年代中。对象晋升老年代的年龄阈值,是-XX:MaxTenuringThreshold
④、动态对象年龄判定
并不一定第③点中的对象年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
⑤、空间分配担保
if (老年代最大可用的连续空间 > 新生代所有对象总空间) {
//Minor GC安全
} else {
if (HandlePromotionFailure是否允许担保失败) {
if (老年代最大可用的连续空间 > 历次晋升到老年代对象的平均大小) {
//进行一次Minor GC
} else {
//进行一次Full GC
}
} else {
//进行一次Full GC
}
}
注:
新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快
老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(非绝对)。Major GC的速度一般会比Minor GC慢10倍以上。
问题:
一、Dalvik和ART
Dalvik:Dalvik基于寄存器,JVM基于堆栈。
它可以支持已转换为.dex(即「Dalvik Executable」)格式的Java应用程序的运行。.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统
在Dalvik下,应用每次运行的时候,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码,这会拖慢应用的运行效率
ART:Android Runtime,应用在第一次安装的时候,字节码就会预先编译成机器码,使其成为真正的本地应用,这个过程叫做预编译(AOT,Ahead-Of-Time)。android5.0发布。
优点:系统性能提升;应用启动更快、运行更快、体验更流畅、触感反馈更及时;更长的电池续航能力;支持更低的硬件。
缺点:字节码变为机器码之后,占用的存储空间更大;应用的安装时间会变长。
二、内存对象的循环应用以及如何避免
如果对象没有被GC roots引用,那么GC的时候会清理掉。
三、System.gc()
源码解析:https://blog.csdn.net/yewei02538/article/details/52386642
四、GC在什么时候、对什么东西、做了什么?
在什么时候:eden满了minor gc,升到老年代的对象大于老年代剩余空间full gc,或者小于时被HandlePromotionFailure参数强制full gc;gc与非gc时间耗时超过了GCTimeRatio的限制引发OOM,调优诸如通过NewRatio控制新生代老年代比例,通过MaxTenuringThreshold控制进入老年前生存次数等……
对什么东西:利用可达性分析算法,从GC Root开始查找,对象引用链没有任何GC Root的时候,清除这些对象(从root搜索不到,而且经过第一次标记、清理后,仍然没有复活的对象)
做了什么:能说出诸如新生代做的是复制清理、from survivor、to survivor是干啥用的、老年代做的是标记清理、标记清理后碎片要不要整理、复制清理和标记清理有有什么优劣势等。还能讲清楚串行、并行(整理/不整理碎片)、CMS等搜集器可作用的年代、特点、优劣势,并且能说明控制/调整收集器选择的方式。