前言
《深入理解Java虚拟机》周志明著第3版这本书的第一章讲的是各种JVM由来与历史,就直接跳过,直接从第二章Java内存区域讲起,我会把我对这章的理解在本文中做一个归纳,做一个更为通俗解释,我看到许多博文根本就讲错了,大家都是阅读很多篇文章总结,但可能自己看的文章都是错的,这个理解基础还是要看官方权威书籍
注意:这里说的Java虚拟机默认是HotSpot虚拟机
本章内容
1、JVM内存是如何划分的(Java内存区域)
2、JVM内存溢出异常(需自己在工作项目中实战体会)
注意:内存溢出,可能是栈内存,可能是堆内存,可能是方法区内存,内存溢出的排查,这个需要自己在工作中亲身体会,本文着重讲JVM内存区域,全是干货,静心阅读,会收获满满。
一、JVM内存区域
对于Java程序员来说,虚拟机有自动内存管理机制,所以Java程序员就只需要写代码逻辑,控制内存交给了JVM,既然JVM是管理内存的,那么JVM内存区域是什么,有哪几部分,分别干什么的呢?如下图
JVM内存区域(运行时数据区)有五部分,分别是:
(1)方法区
(2)Java虚拟机栈
(3)本地方法栈
(4)堆
(5)程序计数器
下面对这五大块做一个介绍
1.1 程序计数器
程序计数器是一块很小的内存空间,一个字节码文件是一行一行读取指令,字节码解释器通过改变这个程序计数器的值来选取下一条执行字节码指令,通过控制程序计数器可以实现分支、循环、异常处理这种开发中常用的语法,这是在代码执行层面,程序计数器作用就是控制指令的执行,选取下一条字节码指令执行。另外一个是站在多线程的层面,由于JVM的多线程是由时间片轮转的方式来获取处理器的时间片,假设线程A要执行2ms结束,但只有1ms的时间片,那么执行完1ms时间后,CPU就要给其他线程了,程序计数器此时就会保留当前线程A的执行状态,以便其他线程执行后,再轮到线程A执行的时候,线程A的程序计数器能将其保留的上次执行状态恢复,然后接着执行剩下的1ms,所以在这儿来说,程序计数器就是起到线程恢复的作用。
1.2 Java虚拟机栈
从上面我们知道JVM是管理内存的,那内存主要是两部分,栈内存和堆内存,这也是Java程序员最关注的两部分,占内存的绝大部分,甚至有人笼统的把Java内存就看作栈内存和堆内存,其他内存可以忽略,但其实方法区和程序计数器也是占内存的,虽然不多。我们这里的栈内存,指的就是Java虚拟机栈(后面统称JVM栈)的内存,JVM栈中基本存的就是8大基本数据类型、对象的引用和实例方法,如Student stu = new Student();
这是一个创建学生对象实例的语句,stu就是对象的引用,他是一个地址,存在JVM栈中,又如你自己写的一个test()方法也会压入栈中执行,当这个方法执行完后就会从栈中弹出。
1.3 本地方法栈
本地,英文就是Native,本地方法栈和JVM栈区别就是JVM栈执行的是Java方法,就是编译成.class文件中的方法指令,而有些东西是java提供的内置工具类方法做不到的,于是就提供了一个本地方法服务,用native关键字修饰,这些方法是超出了java方法的范围,只能用本地native方法调用底层C语言的库执行,因为如果要调用一些硬件以及驱动本地的一些东西才要用native方法,这种情况比较少,绝大多数情况直接用java提供的方法库就能解决,但有时你用的是java方法,他底层调用的还是native方法,只不过这种需要调用native方法的情况很少。比如我们启动线程,他其实底层源码是调用了native方法,因为操作线程需要调用底层C语言去执行线程操作,Java本身不能直接操作线程,线程是属于操作系统层面的,Java无法直接访问到操作系统底层。
点击start()看底层源码,发现用native关键字修饰
1.4 堆
堆很重要,堆在内存区域占大部分,且其中内容可以被线程共享访问,因为我们new对象,为对象分配空间,分配内存就在这儿,也可以用Student stu = new Student();
举例,如下图,对象引用(地址)stu在代码执行后存在栈内存(JVM栈)中,然后这个引用指向创建的Student对象实例,Java中几乎所有的对象实例都会在堆中分配内存,以后会讲到的垃圾收集器进行垃圾收集也是在堆中进行。垃圾收集就是将已经不用的一些对象,将其清除掉,以便腾出更多内存。这就像我们生活中家庭清除不用的生活垃圾一样,将不用的桌椅板凳或者厨余垃圾扔掉,可以腾出更多可用位置放其他有用的东西。
堆内存中又可以分为新生代区和老年代区,简单解释一下,新生代就是new创建对象实例分配内存后出生的区域,当垃圾收集器(常称GC,英文缩写)来清理一次没用的对象,还会留下一些还需要用的对象实例,重复清理多次,如果对象还存在,认为他是"老骨头"了,就会把这个对象放到老年代,启动强GC将其清理,之前新生代的垃圾收集器可以理解为弱清理。细分堆的目的是为了更好的回收内存,如果堆内存满了,也就是创建实例过多或者其他情况,把堆内存都撑爆了,无法再扩展新对象实例了,那么Java虚拟机就会抛出OutOfMemoryError异常,也就是简称OOM(内存溢出),这个等自己在实际工作开发中碰到实战,或者你可以自己定义一个while(true)无限循环new Student()创建对象,就会看到这个异常,不过这没啥意义,实际工作中需要多线程并发时,就可能出现这个OOM,所以我说等你实践再体会,这会涉及JVM调优。
1.5 方法区
最后一个部分是方法区,方法区也可以被线程共享访问,它用于存储一些不变的静态东西,比如类信息、常量、静态变量(如static)、编译的字节码数据,其中常量是放在常量池中,如int a = 1,这个1就在常量池中,常量池是方法区的一部分。方法区在JDK8以前大家都喜欢将方法区称为永久代,JDK8开始称呼变成了元空间,注意这里不是永久代就是方法区,只是用永久代来实现方法区而已,实际上永久代和元空间都是具体实现技术(技术比较复杂,你只要知道是个技术就行),人们这么称呼习惯了而已,谈到元空间,就会想到是JDK8开始的方法区。为什么后面改成由元空间实现呢?因为永久代实现会更容易遇到内存溢出(OOM)的问题,所以改为元空间技术实现,这个技术就是改用了本地内存实现元空间,本地内存多大啊,就完全不用担心内存不够发生OOM问题了。
举例:一个对象的创建过程
经过上面讲解,大家都应该对JVM内存区域有大致认知了,我们再用一段代码的执行来生动说明一下整个过程,假设有Student.java文件,里面有一个Student stu1 = new Student();
语句,看看怎么执行的,首先编译器编译成字节码,然后进入JVM,JVM读取字节码指令(通过程序计数器),JVM遇到一条new指令时,会先进行类加载检查,先检查这个类是否已经被加载、初始化过,如果没有就进行加载初始化。初始化就是创建对象实例的过程,创建对象实例的引用stu1会压入栈中(JVM栈),该引用指向在堆中分配好内存的实例,也就是我们之前讲过的1.4 堆部分,而在Java语言层面我们知道创建了一个Student()对象,然后new Student()时候会自动调用它的空参构造器初始化Student(){}
,在JVM层面就是执行一个<init>()
指令,这是一个初始化方法,初始化后,这样一个Student对象就真正在内存中创建完毕。
注意:对象刚创建分配内存后出现的区域一般为新生代,不过也有例外,就是创建的对象过大,新生代内存不够,就会分配在老年代,不过这种情况比较少,一般创建对象占用都不大
以上就是《深入理解Java虚拟机》这本书第二章讲的主要内容,本章另外一部分主要将内存溢出异常,这个我开头就说了要真实遇到去解决才印象深刻。
《深入理解Java虚拟机系列二》— 垃圾回收算法