《深入理解Java虚拟机》学习笔记

时间:2021-08-04 10:59:42

《深入理解Java虚拟机》学习笔记

一、走近Java

Java的优点:

  (1)提供了相对安全的内存管理和访问机制,避免了绝大部分的内存泄露和指针越界问题。

  (2)实现了热点代码检测和运行时编译及优化。

 

Java的一些历史:

  98年出了JDK1.2,java技术体系被拆分为3个方向:

  J2SE:面向桌面应用开发

  J2ME:面向手机等移动终端

  J2EE:面向企业级开发

 

  99年发布的HotSpot虚拟机成为了JDK1.3之后的默认虚拟机。

  JDK1.5在java语法易用性上做出了非常大的改进:自动装箱,泛型,枚举,可变长参数,foreach循环,动态注解等。还提供了并发包。

  从JDK1.5开始,公开版本号改为JDK5 JDK6,JDK1.5这样的名称只有在程序员内部使用的开发版本号才继续沿用。

  从JDK1.6开始,终结了J2EE这样的命名方式,采用Java EE 6,Java SE 6这样的命名方式。

 

  Java 8中,将提供Lambda支持,将会极大改善目前java不适合函数式编程的现状。

  函数式编程接近自然语言,易于编程,适合并行运行。

 

  目前Java程序运行在64位的虚拟机上需要付出较大的额外代价,许多企业应用仍然选择使用虚拟集群等方式继续在32位虚拟机中进行部署。

 

二、Java内存区域

  C/C++,需要担负每一个对象生命开始到终结的维护责任。

  java在虚拟机自动内存管理机制的帮助下,不需要为每一个对象去写delete/free代码,不容易出现内存泄露,内存溢出问题。

 

运行时数据区域:

  包括程序计数器,虚拟机栈,本地方法栈,方法区,堆

《深入理解Java虚拟机》学习笔记

 

程序计数器:

  是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。

  正在执行java方法的话,计数器记录的是虚拟机字节码指令的地址。如果还是Native方法,则为空。

  这个内存区域是唯一一个在虚拟机中没有规定任何OutOfMemoryError情况的区域。

 

Java虚拟机栈:

  也是线程私有的。

  每个方法在执行的时候会创建一个栈帧,存储了局部变量表,操作数栈,动态链表,方法出口等。

  每个方法从调用到执行完毕,对应一个栈帧在虚拟机栈中的入栈和出栈。

  通常所说的栈,一般是指虚拟机栈中的局部变量表部分。

  局部变量表所需的内存在编译期间完成分配。

  如果线程请求的栈深度大于虚拟机所允许的深度,则*Error。

  如果虚拟机栈可以动态扩展,扩展到无法申请足够的内存,则OutOfMemoryError。

 

本地方法栈:

  和虚拟机栈类似,主要为虚拟机使用到的Native方法服务。

  也会抛出*Error和OutOfMemoryError。

 

Java堆:

  被所有线程共享的一块内存区域,在虚拟机启动时创建,用于存放对象实例。

  堆可以按照可扩展来实现(通过-Xmx和-Xms来控制)

  当堆中没有内存可以分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。

 

方法区:

  被所有线程共享的一块内存区域。

  用于存储已被虚拟机加载的类信息,常量,静态变量等。

  这个区域的内存回收目标主要针对常量池的回收和对类型的卸载。

  当方法区无法满足内存分配需求时,则抛出OutOfMemoryError异常。

 

运行时常量池:

  是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。

  当常量池无法再申请到内存时,则抛出OutOfMemoryError异常。

 

直接内存:

  不是运行时数据区的一部分,但也可能抛出OutOfMemoryError异常。

  在JDK1.4中新加入的NOI类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数直接分配堆外内存,

然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

 

对象的创建:

  指针碰撞:所有用过的内存在一边,空闲内存在另一边,中间放着一个指针作为分界点的指示器,

分配内存就是把指针往空闲内存那边挪一段与对象大小相等的距离。在使用Serial,ParNew等收集器,

(也就是用复制算法,标记-整理算法的收集器),分配算法通常采用指针碰撞。

  空闲列表:虚拟机维护一个列表,记录哪些内存是可用的,分配的时候从列表中找到一块足够大的空间划分给对象,并更新列表。

使用CMS这种基于标记-清除算法的收集器,通常用空闲列表。

 

  内存分配完之后,虚拟机要将分配到的内存空间都初始化为零值(不包括对象头),保证了对象的实例字段在Java代码中可以不赋初始值就直接使用。

  

 

对象的内存布局:

  对象在内存中可分为3个部分,对象头,实例数据,对齐填充。

  对象头的第一部分用于存储对象自身的运行时数据,如对象的哈希码,GC分代年龄,锁状态标志,线程持有的锁等。

  另一部分是类型指针,即对象指向它的类元数据的指针,通过这个来确定这个对象是哪个类的实例。

  实例数据是对象真正存储的有效信息。

 

对象的访问定位:

  程序要通过栈上的reference数据来操作堆上的具体对象。对象的访问方式有使用句柄直接指针

  使用句柄:java堆会划分一块内存作为句柄池,reference中存的是对象的句柄地址,而句柄中包含了对象的实例数据的地址和类型数据的地址(在方法区)

优点:对象被移动,reference不用修改,只会改变句柄中保存的地址。

  使用直接指针:reference中存的是对象的地址,对象中分一小块内存保存类型数据的地址。优点:速度快。

 

 三、垃圾收集器与内存分配策略

   垃圾回收主要针对java堆和方法区。

判断对象是否还存活:

  引用计数算法:每当一个地方引用一个对象,计数器就加1,引用失效的时候,计数器就减1。当计数器为0的时候就表示对象不会再被使用。

缺点:难以解决对象之间相互循环引用的问题。

  可达性分析算法(主流):用过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,

当一个对象到GC Roots没有任何引用链相连时,则此对象是不可用的。

 

四大引用

  jdk1.2之后,将引用分为强引用,软引用,弱引用,虚引用,4种强度依次逐渐减弱。

  强引用:  强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,

Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 

  软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。

只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。

  弱引用:弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。

在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

  虚引用:“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。

 

回收方法区:

  主要回收两部分:废弃常量无用的类

  无用的类:该类所有的实例都已经被回收,java堆中不存在该类的任何实例。

加载该类的ClassLoader已经被回收。

该类对应的java.lang.Class对象没有在任何地方被引用,无法通过反射访问该类方法。

  废弃常量:例如“abc”常量没有任何对象引用它,也没有其他地方引用了这个字面量。

 

垃圾收集算法:

  标记-清除算法:首先标记所有需要回收的对象,标记完后在同一回收。

缺点:效率不高。标记清除后会产生大量不连续的内存碎片。空间碎片太多会导致以后分配较大对象时,需要提前触发一次垃圾回收。

  复制算法:用于回收新生代。将一块内存上还存活的对象复制到另一块上,再一次清除之前的那块。

通常用一块较大的Eden空间,和两块较小的Survivor空间。

  标记-整理算法:用于回收老年代。标记后,让存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

  分代收集算法:将java堆分为新生代和老年代,新生代只有少量存活,采用复制算法。

而老年代有大批存活,所以采用标记-整理算法或者标记-清除算法。

 

 

  

 

 

 

 

 

(未完待续。。。)