在谈JVM的这些问题前,我们先来复习一下有关线程和进程的关系
进程:
进程可以看作是程序的执行过程。
一个程序的运行需要CPU时间、内存空间、文件以及I/O等资源。
操作系统就是以进程为单位来分配这些资源的,所以说进程是分配资源的基本单位。
线程:
线程从属于进程,只能在进程的内部活动,多个线程共享进程所拥有的的资源。如果把进程看作是完成许多功能的任务的集合,那么线程就是集合中的一个任务元素,负责具体的功能。
虽然CPU、内存、I/O等资源分配给了进程,但实际上真正利用这些资源并在CPU上执行的却是线程,即真正完成程序功能的是线程。
因为进程作为这些资源的拥有者,它的负载很重,在进程的创建、切换、删除过程中的时间和空间开销都很大。
所以目前主流的操作系统都只将进程作为资源的拥有者,而把CPU调度和运行的属性赋予了线程。
比如打开浏览器程序,会产生相应的进程,浏览器进程中包含有许多线程,如HTTP请求线程,I/O线程,渲染线程,事件响应线程等。
浏览器进程拥有着内存和I/O资源等,但当我们在浏览器中输入文字时,真正使用I/O资源接收我们输入的文字,并在CPU处理文字的却是浏览器进程中的I/O线程。
即真正完成浏览器文字输入功能的是线程。
现代很多操作系统支持让一个进程包含多个线程,从而提高程序的并行程度和资源的利用率。
我们知道Java语言是需要运行在JVM上的。
实际上,JVM也是一个软件程序,这就意味着它执行起来也会在操作系统中创建进程,即JVM进程,通常又叫JVM实例。
而我们所写的main方法,实际上就是JVM进程中主线程的所在。
从操作系统的角度来看,我们常说的Java程序,应该包括JVM和我们编写的Java代码。
当我们写完Java代码,并编译成class文件后,使用Java命令执行main方法;或者直接在IDE启动main方法时,JVM程序就会执行,操作系统会将其从磁盘中装入内存,并创建一个JVM进程,随后启动主线程,主线程会去调用某个类的 main 方法,因此这个主线程就是我们写的main方法所在。
实际上,JVM本身就是一个多线程应用,即使我们在代码中并没有手动的创建线程,JVM进程也并不是只有一个主线程,而是也会有其他线程。
这些线程完成着JVM的功能,如GC线程负责回收JVM使用过程中的垃圾对象。
JVM进程启动完成后,必然会有的线程如下:
至此,我们知道了,启动一个Java程序,本质上就是启动JVM程序,并在操作系统中创建一个JVM进程。
这个JVM进程会由操作系统分配许多资源,如内存、I/O等。JVM进程中包含有许多线程,这些线程共享JVM进程分配到的资源,同时这些线程也是CPU核心上执行的实体,它们完成着JVM所具有的功能。
同时我们通过实验也能发现,启动多少个java程序,就会创建多少个JVM进程(JVM实例)。
每个实例都是独立的,互不影响,即一个程序(一个JVM软件程序)可以被多个进程共用(创建多个JVM进程或者说JVM实例)
总的来说,JVM大致可以分为四部分:
- 栈
- 堆
- 方法区
- 程序计数器
大家对java中的变量应该都不陌生吧!变量分为全局变量、局部变量、静态变量;
注意:
我们这里说的是JVM中的栈和堆,和操作系统的栈和堆、数据结构中的栈和堆是不同的。
大家以后再被问到栈和堆的时候,一定要搞清楚是哪里面的栈和堆。
举例说明:
代码示例:
public class test { public int a; // 全局变量(也可叫做test这个类的成员变量) public static int b; // 静态变量 public void method() { int c = 0; // 局部变量 System.out.println("这是test这个类的一个普通成员方法"); } public static void run() { System.out.println("这是一个静态方法!"); // 静态方法不需要借助实例化对象 } public static void main(String[] args) { test test1 = new test(); // test1 是test类的一个实例化对象 test test2 = new test(); // test2 是另一个实例化对象 } }
局部变量就是存储在栈上的,
全局变量属于我们new出来的实例化对象的一部分,储存在堆上,
静态变量就比较特殊了,他存储在方法区中。
那么我们栈上还有什么东西呢?
还有我们类中各个方法间的调用关系。
在回到堆中,我们到我们new出来的实例化对象是存储在堆上的,我们的实例化出来的每个对象都有他对应的变量和方法(即我们的成员方法和成员变量),他们自然和对象一起都储存在堆上。
那静态变量和静态方法呢?
他们不属于我们实例化出来的对象,他们是在我们我们这个类创建出来的时候就存在了,不依托实例化对象。他们属于类对象,我们方法区里放到就是类对象。
那么类对象里有啥呢?
包括类是啥名字
继承自谁,实现了那些接口
有啥属性,属性名是啥,类型是啥,访问权限是啥
有什么方法,方法名是?参数是?返回值?访问权限?方法内部的指令是?方法里面干了什么?
总之类对象对我们这个类做了一个整体的描述,
一个类可以有多个实例化对象,但只有一个类对象
(即每个静态变量和静态方法在不同的实例化对象中也只有一个)。
总结一下
- 栈(方法之间的调用关系、局部变量)
- 堆(实例化出来的对象:全局变量(也叫成员变量)、成员方法)
- 方法区(类对象:静态变量、静态方法....)
程序计数器:
占地最小的区域:只是单纯的保存当前代码执行到哪个指令了(存储了下一个要执行的指令地址)写好的代码,是被加载到内存里的(类对象中),既然是在内存中,每个指令都有对应的"地址"
程序计数器就是记录了当前执行到哪条指令了相当于一个"书签"这个也是和线程调度是有关系
就像我们上面说的,当我们启动一个java,启动main方法的时候,我们JVM程序就会执行,就会创建出来一个JVM进程,同时还会有多个线程来负责完成JVM的工作。
那么我想问,是每个线程都有上面这四个区域吗?
不是的,我们的方法区 和 堆是整个JAVA进程中只有一份的(该进程内的多个线程共享这一份资源),但程序计数器和栈,则是每个线程都有一份。
为啥呢??
操作系统cpu等相关资源分配给进程,该进程内的所有线程共享该进程的所有资源,线程是执行的基本单位。
每个线程在执行各自执行各自的代码,各自是一个执行流,所有说每个线程都需要知道接下来要执行的指令是什么(程序计数器的功能)?每个线程也需要记录下当前的调用栈(存在方法区)
图示说明:
Java程序启动的时候,就需要让JVM把class文件给读进内存并进行一系列后续的工作。
类加载流程:
1、加载 找到class文件,打开文件,读文件,创建空的类对象
2、链接
- 验证 —— 检查.class文件格式是否符合规范要求(JVM规范中明确描述了)
- 准备 —— 给静态变量分配内存空间,空间里填充0值。
- 解析 —— 把字符串常量进行初始化,把“符合引用替换成直接引用”
3、初始化
针对类的静态成员进行初始化,执行静态代码块,如果这个类的父类还没加载,也要去加载父类。
面试官最爱考的就是
——“双亲委派模型”(描述了是类加载中的加载阶段,去那些目录里找.class文件)
为了理解所谓的双亲委派模型,我们需要知道:
类加载器——JVM中特殊的模块,功能就是负责把类给加载起来,完成类加载的工作。
图示说明:
大家还记得吗?在C语言中,我们通过malloc动态申请内存(申请的内存是在堆中),每次申请完后都要我们手动释放内存(free)。
如果不释放就回造成内存泄漏等严重问题。但是如果光指望我们程序员手动释放内存,那显然是不靠谱的。
为在Java中就由机器负责回收不再使用的内存空间——这种机制就被称为内存回收机制(garbage collection简称GC)
1、垃圾回收中,回收的是什么?
2、如何确定该对象是需要回收的?
那么我们知道了回收的单位是对象,那么我们如何具体确定某个对象就是垃圾(不再使用了呢)呢?
确定是不是垃圾,有很多种办法。其中Java里主要使用的是“可达行分析”这种办法,
在别的编程语言中(比如Python)中使用的是“引用计数这种方法”。
注意:
在《深入理解Java虚拟机》这本书中,这两种办法都有提到。
- 那么如果我们在面试中被问到:在垃圾回收机制中,如何判断对象是不是垃圾?你可以两个都说。
- 如果问的是:在JVM中,如何判断对象是不是垃圾,你只需回答“可达性分析”就行。
引用计数法
使用额外的计数器,来记录对于某个对象来说,有多少引用指向他。
要想使用对象,就需要有引用指向他,如果没用引用了——引用计数为0了,说明该对象无法被使用了,也就是需要回收的垃圾了。
可达性分析(Java真正采取的方法)
可达性,什么意思呢?
就是以代码中的一些特殊变量为起点,然后以起点触发,看看那些对象都能被访问到。
只要对象能访问到,就标记为“可达”,当完成一圈标记后,剩下的就是“不可达“的了,也就是要回收的垃圾了!!!
我们对上面的一些名词做一些解释
一、什么样的变量可以可以称为起点(GCRoot)呢?
1、局部变量表中的对对象的引用(栈里面的局部变量,栈有多个,每个线程一个,每个栈里面又有很多的栈帧,每个栈帧里有一个自己的局部变量表。)
意思就是:所有线程的所有栈的所有栈帧的所有的局部变量表中的全部的变量,都可视为GCRoot.
2、常量池中对应的对象
3、方法区中,静态引用类型的成员
二、什么叫做”能够被访问到?
图示说明:
可达性分析相比于引用计数来说,不会占用额外的内存空间;
也不会涉及到循环引用的问题;
3、确定要回收的对象后,如何具体进行垃圾回收?
有以下几个笔记经典的垃圾回收算法(策略)
1、标记-清除
2、复制算法(为了解决内存碎片化问题)
3、标记整理(类似于顺序表删除元素)
4、分代回收
分代回收总结:
- 在伊甸区,新生的对象,如果活过了一轮GC(一轮GC以及把大部分对象给回收了,活过一轮GC的是少数),就把这些幸存的对象放到幸存区(因为对象少,所以幸存区的空间不大)
- 幸存区的对象使用复制算法,因为数量较少,即使浪费了一半的内存也没关系。
- 老年代的对象使用标记整理算法,因为老年代对象他被回收的频率不高,就可以接收标记整理带来的开销
图示总结说明:
1.堆内存为两个区:新生代和老年代;
2.新生代默认占堆内存的1/3,老年代默认占堆内存的2/3;
3.新生代又分为Eden区、Survivor From区、Survivor To区默认比例是8:1:1 ;
4.工作过程:
- 所有新创建的对象都在Eden区,当Eden区内存满后将Eden区+Survivor From区存活的对象复制到Survivor To区;
- 清空Eden区与Survivor From区;
- 同时Survivor From与Survivor To分区进行交换;
- 每次Minor Gc存活对象年龄加1,当年龄达到15(默认值)岁时,被移到老年代;
- 当Eden的空间无法容纳新创建的对象时,这些对象直接被移至老年代;
- 当老年代空间占用达到阈值时,触发Major GC;
- 以上流程循环执行。
GC Roots的判断
题目:
解析:
JVM的实现
题目:
属于JVM在Java中的实现的是
解析:
类加载器
题目:
解析:
垃圾回收器和垃圾回收算法的区别
题目:
解析:
JVM内存划分
题目:
解析: