JAVA虚拟机总结(JVM)

一、总纲

  以下是有关JVM比较重要的知识模块,在日常开发以及面试过程中较大可能会使用到。深入了解这几大模块,希望在Java领域能够有更大的突破。

二、JVM的组成部分

  jvm主要有类加载器、运行时数据区、执行引擎和本地库接口组成。在我们平时开发过程中,主要关注点是运行时数据区,这也是面试过程中总会被提问的内容。

类加载器

  类加载全过程:加载、验证、准备、解析和初始化。

  加载j阶段:1.获取类二进制字节流;2.将字节流转化为方法区的运行时数据;3.内存生成java.lang.Class对象,作为访问方法区的入口。

  初始化阶段:初始化类变量和其他资源,与准备阶段的初始化零值不同,此时是根据开发人员主观计划去初始化。初始化阶段是执行类构造器<clinit>()方法的过程,<clinit>()方法的执行过程的一些特点如下:

  1. 静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态代码块中可以赋值,但是不能访问。
1 public class Test {
2     static{
3         i = 89;
4         System.out.println(i);//此处提示Illegal forward reference
5     }
6     static int i = 2;
7 }

  2.<clinit>()方法与类的构造函数不同,它不需要显式调用父类构造器,虚拟机保证在子类<clinit>()方法执行之前,父类的<clinit>()方法已经执行完。Object是所有类的父类,所以第一个被执行的是Object的<clinit>()方法。

  3.<clinit>()方法对于类和接口是非必须的。如果一个类中没有静态代码块或者对变量的赋值操作,编译器可以不为这个类生成<clinit>()方法。

  4.接口与类不一样的是执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口定义的变量使用时,父接口才会初始化。

  5.虚拟机保证一个类中的<clinit>()方法在多线程中被正确加锁、同步,只能有一个线程执行<clinit>()方法方法,其他线程需要阻塞等待。

 

  类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。我们在使用new创建一个对象时,会先在常量池中寻找这个类是否被加载过,如果未曾被加载,就会先执行相应的类加载、解析、初始化过程。jvm中类加载器主要分为三种:

  • 根类加载器(也叫启动类加载器)
  • 扩展类加载器
  • 系统类加载器(也叫应用程序类加载器)

类加载器之间存在层次关系,称为双亲委派模型。启动类加载器<——扩展类加载器<——应用程序类加载器<——自定义类加载器。除了顶层的启动类加载器之外,其他类加载器都有自己的父类加载器。加载顺序以上层类加载器优先,当上层加载器反馈无法完成加载请求时,才会尝试自己加载。一个类在加载时都是由同一个类加载器加载,保证了其唯一性。

运行时数据区

  • 程序计数器
  • java虚拟机栈
  • 本地方法栈
  • java堆
  • 方法区

程序计数器

  是当前线程所执行的字节码的行号指示器,通过改变计数器的值来觉得程序下一条需要执行的字节码指令,如分支、循环、跳转、线程恢复等。

虚拟机栈

  是线程私有的,生命周期与线程相同,是存储当前线程运行方法所需要的指令、数据、返回地址等

本地方法栈

  为虚拟机中使用到的带native的方法服务

java堆(Heap)

  是虚拟机管理的最大一块内存,被所有线程共享。所有对象实例和数组都在堆上分配。java堆是垃圾回收器管理的主要区域,

方法区

  用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。有的还把方法区成为“永久代”。

执行引擎

  输入的是字节码文件,处理过程是字节码解析的等效过程,输出是执行结果。

本地库接口

  本地库接口是是本地方法调用底层由其他语言实现的接口。

三、自动内存管理机制

   相比较其他面向对象的语言,java语言最大的特点之一是能够自动进行内存的管理,不用开发人员手动操作。在讨论虚拟机运行错误的过程中,我们往往会遇到的两种错误是内存溢出OutOfMemoryError和堆栈溢出StackOverFlowError。

OutOfMemoryError:当无法申请到内存时出现的错误,集中在虚拟机栈、本地方法栈、堆、方法区。

StackOverFlowError:栈太大或者虚拟机栈容量太小,导致内存无法分配。线程请求的栈深度大于虚拟机所允许的深度,主要在虚拟机栈和本地方法栈。

 

  上面介绍了运行时数据区域的几大主要模块,其中作为内存中最大一块,java堆是自动内存管理的主要区域。本章总结的就是针对java进行垃圾回收的各种相关知识点。

对象创建

  1. 检查类是否已被加载(从常量池中检查)、解析和初始化过,如果没有就先进行类加载;
  2. 为新生对象分配内存
  3. 分配的内存空间初始化零值
  4. 对对象进行必要的设置(哈希码、GC等信息放在对象头)
  5. 执行init方法,按照程序猿的意愿进行初始化

  对象在内存中的布局:对象头、实例数据、对齐填充数据。

  对象的访问定位:为了使用对象,程序通过栈上的引用数据操作堆上的对象。主流的访问方式有两种:句柄和直接指针。

句柄访问:堆中分配一块内存作为句柄池,存储的是对象的句柄地址,其中包含了对象实例数据类型数据各自的具体地址信息。

直接指针访问:引用中存储的直接就是对象地址

垃圾收集GC

判断

需要GC的方法:引用计数法和可达性分析法

引用计数法:一个对象每次被引用,就在引用计数器中加1,当引用无效时,减1,判断引用计数器是否大于0。主流的java虚拟机没有选用引用计数算法来管理内存,因为很难解决对象之间相互循环引用的问题。

可达性分析算法:通过一系列称为GC Roots的对象作为起始点,从这些节点开始往下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明对象是不可用的。可被称为GC Roots的对象包括了:

1.虚拟机栈中引用的对象

2.方法区中类静态属性引用的对象(static)

3.方法区中常量引用的对象(final)

4.本地方法栈用引用的对象

垃圾收集算法

  • 标记清除算法
  • 复制算法
  • 标记整理

垃圾收集器

  垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。

 

Serial收集器(复制算法)

这是一个单线程的收集器,在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。优点:简单而高效(单个CPU环境,没有线程交互的开销)

Serial Old收集器(标记整理算法)

是Serial收集器的老年代版本,有两大用途:1.在JDK1.5及之前的版本中搭配Parallel Scavenge一起使用;2.作为CMS收集器的后备预案

ParNew收集器(复制算法)

与Serial收集器的区别是,ParNew收集器是多线程的。

Parallel Scavenge收集器(复制算法)

并行多线程收集器,是吞吐量优先的收集器,可以通过参数设置控制吞吐量。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。另外这收集器以及G1收集器实现的框架与其他收集器有所不同,所以不能与老年代的CMS搭配使用。

Parallel Old收集器(标记整理算法)

是Parallel Scavenge收集器的老年代版本,解决了Parallel Scavenge只能搭配Serial Old的尴尬。

CMS收集器(标记清除)

这是并发收集、低停顿的收集器。是现如今互联网应用重视服务的响应速度,停顿时间短,给用户带来较好体验,所以CMS是比较受青睐的收集器。实现步骤比前面几种收集器更复杂一些。

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

初始标记和重新标记仍然需要停掉所有线程,初始标记仅仅是标记GC Roots能直接关联到的对象,速度超快;并发标记是进行GC Roots Tracing的过程;而重新标记修正因并发标记期间因程序继续运行导致标记产生变动的那些对象。耗时最长的是并发标记和并发清除,不过因为这都是和用户线程一起工作,所以整体上看CMS收集器的回收过程是和用户线程一起并发执行的,体现出了其优点:并发、低停顿。

虽然是很优秀的收集器,但是也存在它本身的一些缺点

1.对CPU资源非常敏感,因为占用一部分线程而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程是=(CPU+3)/4。

2.CMS无法处理浮动垃圾,可能会因为收集失败导致一次Full GC 的产生。何为浮动垃圾?并发收集阶段,程序产生的新垃圾,这垃圾是在标记过程之后,CMS无法及时处理掉它们,只好留到下一次的GC时再清理。

3.标记清除收集结束之后会有大量的空间碎片产生。

G1收集器

是当今收集器技术发展最前沿成果之一,是一款面向服务端应用的垃圾收集器,与其他收集器相比,具备的特点有

  • 并行与并发
  • 分代收集,可以独立管理整理GC堆
  • 空间整合,整体看是标记整理,从局部看也有复制算法实现
  • 可预测的停顿,有计划地避免在整个堆中进行全区域的垃圾收集,跟着各个Region里堆积的价值大小

实现步骤大概可以分为

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

内存分配和回收策略

自动内存管理解决的两个问题是:给对象分配内存和回收分配给对象的内存。

对象优先在Eden分配,当Eden区没有足够的空间进行分配时,触发一次Minor GC。可通过参数-XX:+PrintGCDetails打印出GC日志。

private static final int  _1MB = 1024 * 1024;
    /**
     * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
     */
    @Test
    public void testMinorGC1(){
        byte[] a1,a2,a3,a4;
        a1 = new byte[2 * _1MB];
        a2 = new byte[2 * _1MB];
        a3 = new byte[2 * _1MB];
        a4 = new byte[4 * _1MB];

    }

java堆大小为20MB,且不可扩展,其中新生代10MB,新生代中Eden区与一个Survivor比例是8:1。GC结束之后4MB被分配在Eden区,6MB分配在老年代。当然4MB会不会直接进入老年代,是跟虚拟机中设置的参数有关,相当于一个阈值。大对象直接进入老年代,如果是Serial或者ParNew收集器,可通过XX:PretenureSizeThreshold=3145728设置大小(1024*1024*3=3145728)

  新生代中的对象不可能永远都停留在新生代,所以虚拟机给每个对象定义对象年龄计数器。对象在Eden区新建并经过第一次minorGC后还存活,并且能被Survivor容纳,将移动到Survivor,对象年龄值为1,对象没熬过一次minorGC,年龄就增加1,当达到固定值(默认是15)时,就被晋升到老年代。这个阈值可以通过参数-XX:MaxTenuringThreshold设置。

  性能监控和故障处理工具

  1. jps:虚拟机进程状况工具
  2. jstat:用于监视虚拟机各种运行状态信息的命令行工具,如:jstat -gc 2764 250 20表示每250毫秒查询一次进程2764,共查询20次
  3. jinfo:实时查看和调整虚拟机的参数;
  4. jmap:生成堆转储快照(一般为heapdump或dump文件)
  5. jstack:java堆跟踪工具,生成虚拟机当前时刻的线程快照,排查线程间死锁、死循环。

可视化工具:

  1. JConsole
  2. VisualVM多合一故障处理工具

 

 

四、JAVA内存模型

待完善

五、线程安全和锁优化

待完善

posted @ 2020-03-10 17:00  ch_0213  阅读(144)  评论(0)    收藏  举报