I hope I have something for you.

JAVA基础学习笔记--JVM基础总结

Posted on By Panda Pan

写在最前

最近复习了一下Java的面试题,总结了一些初级JAVA面试的尿性,做了些笔记,现在把他PO出来。方便以后来看。

Java 8大数据类型

类型 byte short int long double float char boolean
bit大小 8 16 32 64 64 32 16 1
字节大小 1 2 4 8 8 4 2 1

不同类型的变量在内存中分配的字节数不同,同时存储方式也是不同的。

数据存储原理

基本类型数据的存储原理: 基本数据类型都不存在‘引用’的概念,基本数据类型都是直接存储在内存种的内存栈中,数据本身的值也是存储在栈空间的。

引用类型数据的存储原理: 引用类型都继承与Object类,都是按照Java里面存储对象的内存模型进行数据存储的,使用堆栈搭配进行数据存储。“引用”是存储在有序的内存栈中,但是对象本身的值是存放在堆上的。

“==”和Equals的区别

“==”比较的是内存地址是否相等,equals比较的是两个值是否相等。

来看看String的Equals方法源代码

```
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

``` 从源代码我们可以看到,先判断两个对象再物理地址上是否相等, 然后比较两者的长度,再将字符串分割成一个一个的char类型数据, 因为char是基本数据类型,直接比较两个字符串的Unicode。

堆栈的区别

  1. 栈空间由操作系统自动分配和释放
  2. 基本数据类型时存放在栈中的
  3. 声明的局部变量是存放在栈中的
  4. 每个线程包含一个栈区,每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
  5. 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

1.堆的空间需手动申请和释放

  1. 引用对象类型的值是存放在堆中的,但是其引用地址还是存放在栈中
  2. 当我们new 一个对象的时候,该对象被分配到堆空间中。用完之后靠垃圾回收机制不定期自动消除。
  3. 被final修饰的局部变量是存放在堆中的。

注(要考):栈是一个线性的集合,按照后进先出的方法进行处理,存放的是基本数据类型和引用。 jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。

方法区

方法区又叫静态区 ,跟堆一样,他也是被所有线程共享。方法区包含所有的class和static变量。 方法区中包含的都是在整个程序中永远唯一的元素

例子:

堆栈示例

  1. JVM执行main()函数,在栈内存中开辟一个空间,存放x变量(x变量是局部变量)。   同时,在堆内存中也开辟一个空间,存放new int[3]数组,堆内存会自动内存首地址值,如0x0045。   数组在栈内存中的地址值,会附给x,这样x也有地址值。所以,x就指向(引用)了这个数组。此时,所有元素均未附值,但都有默认初始化值0。

  2. 在栈内存定义了新的数组变量内存y,同时将x的值0x0045附给了y。所以,y也指向了堆内存中的同一个数组。

  3. 赋值 100

  4. 则变量x不再指向栈内存中的数组了。但是,变量y仍然指向,所以数组不消失。即,JVM的垃圾回收机制不会对其处理

重点 要考 :在JAVA8及以上的版本, 基本数据类型的数组是引用到栈空间的, 对象数据类型的数组引用在堆空间的。

因为基本类型数据的局部变量是存放在栈中,如果将该局部变量指向了数据的元素,则该元素的内存就在栈空间上。

JVM

既然说到了Java虚拟机,那就继续深入说说JVM里面的重点。

JVM 主要分为5个区域:

  1. Java虚拟机栈

    该区域是线程私有的,每个线程不对该区域共享。 每个方法在执行的时候都会创建一个栈帧,存储了局部变量表,操作数栈,动态链接和方法返回地址等。 每个方法的调用和结束,就对应了一个栈帧的出栈和入栈。

    在Windows下, 栈是向低地址扩展的数据结构,是一块连续的内存区域。 这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的, 在WINDOWS下,栈的大小是固定的(是一个编译时就确定的常数), 如果申请的空间超过栈的剩余空间时,将提示overflow。

  2. 堆内存是线程共享的,在虚拟机启动或者new新的对象时,存放对应的对象实例。

    所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。

  3. 方法区

    方法区也是线程共享的,用于存放已被虚拟机加载的类,常量和静态变量。

    在JVM启动时创建,它存储了运行时常量池、字段和方法信息、静态变量以及被JVM载入的所有类和接口的方法的字节码。

  4. 程序计数器

  5. 本地方法栈

    为非Java编写的本地代程定义的栈空间,也就是说它基本上是用于通过JNI(Java Native Interface)方式调用和执行的C/C++代码。根据具体情况,C栈或C++栈将会被创建。

  6. 其他隐含寄存器

栈溢出和堆溢出

  1. 在递归方法调用的时候如果递归不当将会导致栈溢出,因为一直在不断的调用方法,一直在向栈中压栈

  2. 在不断创建新对象的时候,将导致堆溢出。常见于循环操作不当,一直在循环导致新对象一直在创建,导致堆内存溢出。

类加载原理

Java提供了动态加载的特性,只有在运行时第一次遇到类时才会去加载和链接,而非在编译时加载它。JVM的类加载器负责类的动态加载过程。Java类加载器的特点如下:

  1. 层次结构:Java的类加载器按是父子关系的层次结构组织的。Boostrap类加载器处于层次结构的顶层,是所有类加载器的父类。

  2. 代理模型:基于类加载器的层次组织结构,类加载器之间是可以进行代理的。当一个类需要被加载,会先去请求父加载器判断该类是否已经被加载。如果父类加器已加载了该类,那它就可以直接使用而无需再次加载。如果尚未加载,才需要当前类加载器来加载此类。

  3. 可见性限制:子类加载器可以从父类加载器中获取类,反之则不行。

  4. 不能卸载: 类加载器可以载入类却不能卸载它。但是可以通过删除类加载器的方式卸载类。

当JVM请示类加载器加载一个类时,加载器总是按照从类加载器缓存、父类加载器以及自己加载器的顺序查找和加载类。

1. Bootstrap加载器:Bootstrap加载器在运行JVM时创建,用于加载Java APIs,包括Object类。不像其他的类加载器由Java代码实现,Bootstrap加载器是由native代码实现的。
2. 扩展加载器(Extension class loader):扩展加载器用于加载除基本Java APIs以外扩展类。也用于加载各种安全扩展功能。
3. 系统加载器(System class loader):如果说Bootstrap和Extension加载器用于加载JVM运行时组件,那么系统加载器加载的则是应用程序相关的类。它会加载用户指定的CLASSPATH里的类。
4. 用户自定义加载器:这个是由用户的程序代码创建的类加载器。

类执行机制

JVM执行class字节码,线程创建后,都会产生程序计数器(PC)和栈(Stack)。

程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。栈的结构如下图所示:

栈帧

JVM内存管理

说到JVM,那就需要说说最重要的部分了,JVM的垃圾回收机制。

JVM内存接口如下图所示:

jvm

堆内存与栈内存需要说明

  1. 基础数据类型直接在栈空间分配,方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收。

  2. 引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量 。 方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完成后从栈空间回收,堆空间区域等待GC回收。

  3. 方法调用时传入的literal参数,先在栈空间分配,在方法调用完成后从栈空间收回。字符串常量、static在DATA区域分配,this在堆空间分配。

  4. 数组既在栈空间分配数组名称,又在堆空间分配数组实际的大小。

JAVA异常分类

JAVA异常分为两大类:

  1. 检出异常:必须通过try catch才能通过编译的异常。 如:IOException,SQLException等。
  2. 非检出异常,由程序缺陷或者系统缺陷导致的异常,如NullPointException,RunTimeException等。

ERROR 和 Exception的区别

ERROR: 表示恢复很困难的一种情况,如内存溢出,程序不能自己处理的问题。 OutofMemeryError:Java heap space等。 Exception: 表示一种设计或者实现上某种情况下的缺陷,如果程序正常时可以避免这些问题。

内存溢出的几种情况

导致OutOfMemoryError异常的常见原因有以下几种:

  1. 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
  2. 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  3. 代码中存在死循环或循环产生过多重复的对象实体;
  4. 使用的第三方软件中的BUG;
  5. 启动参数内存值设定的过小;

垃圾回收机制

垃圾收集器在对堆区和方法区进行回收前, 首先要确定这些区域的对象哪些可以被回收,哪些暂时还不能回收, 这就要用到判断对象是否存活的算法!(面试官肯定没少问你吧)

引用计数算法

算法分析

引用计数是垃圾收集器中的早期策略。 在这种方法中,堆中每个对象实例都有一个引用计数。 当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。 当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1)

但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。 任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

引用算法的优缺点

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。

    public class ReferenceFindTest {
        public static void main(String[] args) {
            MyObject object1 = new MyObject();
            MyObject object2 = new MyObject();
              
            object1.object = object2;
            object2.object = object1;
              
            object1 = null;
            object2 = null;
        }
    }

这段代码是用来验证引用计数算法不能检测出循环引用。 最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问, 但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

可达性算法

算法分析

可达性分析算法是从离散数学中的图论引入的。

程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点。

当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。

在JAVA语言中,可作为GC ROOT 的节点可以由下面几个对象组成:

a) 虚拟机栈中引用的对象(栈帧中的本地变量表);

b) 方法区中类静态属性引用的对象;

c) 方法区中常量引用的对象;

d) 本地方法栈中JNI(Native方法)引用的对象。

引用的分类

强引用

强引用在代码中是普遍存在的,类似 Object o = new Object(); 这类的引用,只要强引用还在,垃圾收集器就永远不会回收掉该对象。

软引用

用来描述一些还有用但并非必须的对象。

对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。 如果这次回收后还没有足够的内存,才会抛出内存溢出异常。

弱引用

也是用来描述非必需对象的,但是它的强度比软引用更弱一些。

被弱引用关联的对象只能生存到下一次垃圾收集发生之前。 当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

虚引用

也叫幽灵引用或幻影引用(名字真会取,很魔幻的样子),是最弱的一种引用关系。

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的作用是能在这个对象被收集器回收时收到一个系统通知。

垃圾回收前的准备工作

在可达性算法中不可达的对象,至少需要经历两次标记才会被回收。

第一次标记: 在对象不可达 GC Root的时候,该对象就会被第一次标记。

第二次标记: 第一次标记后,会进一步进行筛选,看该对象是否有必要执行finalize()方法,在该方法中没有重新引用将会被二次标记。

## 方法区如何判断是否需要回收

方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可通过引用的可达性来判断, 但是对于无用的类则需要同时满足下面3个条件:

. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;

. 加载该类的ClassLoader已经被回收;

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

常见的垃圾回收算法

标记-清除算法

标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后, 再扫描整个空间中未被标记的对象,进行回收,如下图所示。 标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理, 在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

标记-清除算法

复制算法

复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。

它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了, 基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象, 并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面, 原来的对象面变成了空闲面,程序会在新的对象面中分配内存。

标记-整理算法

标记-整理算法采用标记-清除算法一样的方式进行对象的标记, 但在清除时不同,在回收不存活的对象占用的空间后, 会将所有的存活对象往左端空闲空间移动,并更新对应的指针。

分代收集算法(重中之重)

它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。

一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。

老年代的特点是每次垃圾收集时只有少量对象需要被回收, 而新生代的特点是每次垃圾回收时都有大量的对象需要被回收, 那么就可以根据不同代的特点采取最适合的收集算法。

space

新生代(Young Generation)回收算法

所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。

在一个对象被创建的时候,它首先是存放在Eden中的, 等Eden满了触发一次MinorGC ,Eden中的存活的对象会被移动到第一块存活区(S0)(From Space),然后清空Eden。

等第一块存活区(S0)(From Space)满了,就会再次触发MinorGC,会将Eden和S0中的存活对象复制到S1,然后清空Eden和S0. 这个时候S0已经空了,再将S1中存活的对象复制到S0中,保证S1中为空。如此往返达到16次的对象,该对象则会被送往年老代

年老代(Old Generation)的回收算法

在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

持久代(Permanent Generation)的回收算法

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。

但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

典型的垃圾收集器

  1. Parallel Scavenge

    新生代的并行收集器,回收期间不需要暂停其他线程,采用Copying算法。该收集器与前两个收集器不同,主要为了达到一个可控的吞吐量。

  2. Parallel Old

    Parallel Scavenge的老生代版本,采用Mark-Compact算法和多线程。

  3. CMS

    Current Mark Sweep收集器是一种以最小回收时间停顿为目标的并发回收器,因而采用Mark-Sweep算法。

参考文档

JAVA中分为基本数据类型及引用数据类型

JAVA中的栈和堆举例

关于Jvm知识看这一篇就够了

扒一扒JVM的垃圾回收机制,下次面试你准备好了吗

Java基础:JVM垃圾回收算法