《深入理解java虚拟机》笔记

二、java内存区域与内存溢出异常

0.在内存管理领域,java与c/c++不同的是,在java虚拟机自动内存管理机制下,java不需要手动去为对象写配对的free内存的代码,不容易出现内存泄漏和内存溢出问题。

1.程序计数器:一小块的内存空间,可看作当前线程所执行的字节码的行号指示器。
每条线程都有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,称为“线程私有内存”。
2.java虚拟机栈描述的是java方法执行的内存模型:每个方法执行时都会创建一个栈帧。(Stack Frame)。

虚拟机栈用于存储局部变量表,操作数栈,动态链接,方法出口等。是线程隔离的。

在方法内的变量就叫局部变量,在方法外的变量叫全局变量。
3.本地方法栈,类似虚拟机栈。而本地方法栈,主要用于Native方法服务,也就是JNI相关的。

最常见的JNI是System.currentTimeMills();获取系统时间。
4.java堆:所有线程共享的一块内存模型,在虚拟机启动时创建。
堆用于存放对象实例。堆是垃圾收集器管理的主要区域。如果堆中没有内存可以继续分配,就会抛出OutOfMemoryError。

成员变量(也叫属性变量)是对象实例的一部分。因此成员变量也是存储在jvm堆里面的。
5.方法区:与java堆一样,是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

静态域:位于方法区的一块内存。存放类中以static声明的静态成员变量。所以方法区又叫静态区。
常量池:常量池是方法区的一部分内存。常量池在编译期间就将一部分数据存放于该区域,包含基本数据类型如int、long等以final声明的常量值,和String字符串、特别注意的是对于方法运行期位于栈中的局部变量String常量的值可以通过 String.intern()方法将该值置入到常量池中。

7.直接内存:本地直接内存。不受java堆影响。但是内存不够时也会报OOM异常。

8.关于对象的创建,虚拟机遇到new指令时,会先检查这个类是否已被加载、解析、初始化过。如果没有,就执行类加载机制。接下来虚拟机会为新对象分配内存。

虚拟机会对对象进行设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码等。

9.对象的内存布局分为3个区域:对象头、实例数据、对齐填充。

10.对象的访问方式,取决于虚拟机实现而定。对象的访问方式有使用句柄、直接指针两种。使用直接指针访问的好处是速度更快。

Sun HotSpot虚拟机是采用指针访问。

11.OutOfMemory (内存溢出)。

12.堆内存溢出。

解决方法:

  • 调整虚拟机的堆参数(-Xmx 与-Xms)
  • 通过内存映像分析工具,弄清楚是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)

13.关于虚拟机栈和本地方法栈,在java中描述了两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,将会抛出StackOverflowError异常(栈溢出)
  • 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常(内存溢出)

14.解决StackOverflowError异常, 提高-Xss参数,增加栈内存容量。

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

1.GC(Garbage Collection,垃圾收集)需要完成的3件事情:哪些内存需要回收?什么时候回收?如何回收?

2.GC发生在java堆和方法区。一个接口中的多个实现类需要的内存不一样,程序处于运行期间才会知道会创建哪些对象,这部分的内存和回收都是动态的。

3.引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器值就加一;当引用失效时,计数器值就减一。任何时候计数器为0的对象就是不可能再被使用的。

但是,主流java虚拟机没有选用它来管理内存,因为它很难解决对象之间互相循环引用的问题。

4.可达性分析算法 :从节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到起点没有任何引用链时,则证明此对象是不可用的。

5.java中的引用分为强引用,软引用,弱引用,虚引用。

6.finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。

finalize()方法至多由GC执行一次。

7.垃圾收集器:Serial收集器、ParNew收集器、Parallel Scavenge收集器、Serial Old收集器、Parallel Old收集器、CMS收集器、G1收集器

8.  JVM中的堆,一般分为三大部分:新生代、老年代、永久代。

老年代:主要存放应用程序中生命周期长的内存对象。大对象也会直接进入老年代。

新生代:主要是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收

新生代又分为 Eden区、ServivorFrom、ServivorTo三个区。

  •    Eden区:Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
  •    ServivorTo:保留了一次MinorGC过程中的幸存者。
  •    ServivorFrom:上一次GC的幸存者,作为这一次GC的被扫描者。

MinorGC的过程:MinorGC采用复制算法。

MajorGC采用标记—清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。

当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。

指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域. 它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。

在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。

9.垃圾收集算法 :垃圾收集算法介绍

Java有四种类型的垃圾回收器:

  1. 串行垃圾回收器(Serial Garbage Collector)
  2. 并行垃圾回收器(Parallel Garbage Collector)
  3. 并发标记扫描垃圾回收器(CMS Garbage Collector)
  4. G1垃圾回收器(G1 Garbage Collector)

垃圾回收器种类: 垃圾回收器种类

 四、虚拟机性能监控与故障处理工具

1.虚拟机分析数据包括:运行日志、异常堆栈、GC日志、线程快照(threaddump/javacore文件)、堆转储快照(heapdump/hprof文件)等

六、类文件结构

1.由于jvm,java程序可以一次编写,到处运行。

2.jvm具有平台无关性、语言无关性。

3.语言无关性:jvm不和任何语言绑定,只与"class文件"(字节码)这种特定的二进制文件格式所关联。

java,jRuby,Groovy这些语言都可以经过编译器变成字节码(.class文件)

4.Class文件是一组以8位字节为基础单位的二进制流。

Class文件的头4个字节称为魔数。魔数的作用是确定这个文件是否为一个能被虚拟机接受的Class文件。

0xCAFEBABE是Class文件的魔数。这个值在1991年就确定下来。很有浪漫气息~

5.Class文件的第5、6个字节是次版本号。第7、8个字节是主版本号。

6.Class类文件结构包括常量池、访问标志、类索引(父索引、接口索引集合)、字段表集合、方法表集合、属性表集合

7.字节码指令。

七、虚拟机类加载机制

1.虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。

2.虚拟机类加载机制的生命周期:加载,验证,准备,解析,初始化,使用,卸载。

其中,验证,准备,解析这三个过程又称为“连接”。  

3.加载,验证,准备之后,(解析可能在初始化前也可能在初始化后)。有且只有5种情况必须对类进行“初始化”:

  •  遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,如果类没有初始化,必须进行初始化。
  • 使用reflect包的方法对类进行反射调用时,如果类没有初始化,必须先初始化。
  • 当初始化一个类时,如果发现其父类还没有进行过初始化,需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的main类,虚拟机会先初始化这个主类。
  • 当使用jdk1.7的动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

4.加载。加载要完成以下3件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行数据结构。
  • 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。

5.验证。验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,保证安全。

可以从文件格式验证、元数据验证、字节码验证。符号引用验证。

6.准备。 7.解析。

8.初始化。

<clinit>()方法:类构造器方法

<init>()方法:  实例构造器方法  or  类的构造函数

8.1静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句可以赋值,但是不能访问。

8.2<clinit>()方法与类的构造函数(or 说实例构造器方法<init>()方法)不同。它不需要显示的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()已经之行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
8.3由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块先于子类的变量赋值操作。
8.4、<clinit>()对于类和接口来说,并不是必需的。因为如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
8.5、接口中不能使用静态语句块,但仍然有变量初始化的赋值操作。因此接口和类一样都会生成<clinit>()。只有当父接口中定义的变量使用是,父接口才会初始化。另外,接口的实现类在初始化时,也一样不会执行接口的<clinit>()方法。
8.6、虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类中的<clinit>()方法有很耗时的操作,就可能造成多个线程阻塞,在实际应用中,这种阻塞是很隐蔽的。
注:需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出<clinit>()方法后,其他线程不会再执行<clinit>()方法。同一个类加载器,一个类型只会初始化一次。

 

八、类加载器

1.JVM提供了3种类加载器: BootstrapClassLoader、 ExtClassLoader、 AppClassLoader分别加载Java核心类库、扩展类库以及应用的类路径( CLASSPATH)下的类库。JVM通过双亲委派模型进行类的加载,我们也可以通过继承 java.lang.classloader实现自己的类加载器。

2.何为双亲委派模型?当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。
3.采用双亲委派模型的一个好处是保证使用不同类加载器最终得到的都是同一个对象,这样就可以保证Java 核心库的类型安全,比如,加载位于rt.jar包中的 java.lang.Object类,不管是哪个加载器加载这个类,最终都是委托给顶层的BootstrapClassLoader来加载的,这样就可以保证任何的类加载器最终得到的都是同样一个Object对象。
 
参考资料 :
《深入理解java虚拟机》
纯洁的微笑--jvm系列

posted on 2018-09-11 23:10  乐之者v  阅读(404)  评论(0编辑  收藏  举报

导航