- Java自动管理堆和栈,程序员不能直接设置堆和栈
- 操作系统的堆和栈
- 堆:一般由程序员分配释放,若程序员不释放,则程序结束时可能由OS回收,分配方式类似于链表
- 栈:操作系统自动分配释放,存放函数的参数值、局部变量等。操作方式与数据结构中的栈相似
- JVM的内存是分布在操作系统的堆中
- 因为操作系统的栈是操作系统管理的,它随时会被回收
- 如果jvm放在栈中,那java的一个null对象就很难确定会被谁回收了。gc的就没有存在的意义了
- 而要对栈做到自动释放也是jvm需要考虑的,所以放在堆中就最合适不过了

- 程序员写好的类加载到虚拟机执行的过程是:当一个classLoder启动的时候,classLoader的生存地点在jvm中的堆,然后它会去主机硬盘上将A.class装载到jvm的方法区,方法区中的这个字节文件会被虚拟机拿来new A字节码(),然后在堆内存生成了一个A字节码的对象,然后A字节码这个内存文件有两个引用一个指向A的class对象,一个指向加载自己的classLoader
- 操作系统的堆和栈
- Java虚拟机
- 生命周期:起点是当一个Java应用main函数启动时虚拟机也同时被启动,而只有当在虚拟机实例中的所有非守护进程都结束时,虚拟机实例才结束生命
- main函数是Java应用的入口,main函数被执行时,Java虚拟机就启动了
- 启动了几个main函数就启动了几个Java应用,同时也就启动了几个Java虚拟机
- 虚拟机中有两种线程,一种叫守护线程,一种叫非守护线程(普通线程)
- 在main函数中启动的匿名线程也是非守护线程
- JVM结构
- 结构图
- 内部执行流程图
- 虚拟机栈、本地方法栈、程序计数器这三个模块是线程私有的
- 有多少线程就有多少个这三个模块
- 生命周期跟所属线程的生命周期一致
- 程序计数器是JVM内存中唯一不会报outOfMemoryError的区域
- 本地方法库接口:即操作系统所使用的编程语言的方法集,归属于操作系统
- 本地方法库保存在动态连接库中,即.dll(windows系统)文件中
- 在Java代码中通过System.loadLibrary()加载c语言库(本地方法库)直接与操作系统平台交互
- 程序计数器(Program Counter Register)
- 也叫PC寄存器,是一块较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器
- 在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
- 区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位),虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址
- 当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined
- 程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个
- 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
- Java虚拟机栈(Java Virtual Machine Stack)
- 线程私有的,它的生命周期与线程相同,每个线程都有一个
- JVM栈中每个栈帧存放的为当前线程中局部基本类型的变量(八种基本类型和reference (32 位以内的数据类型,具体根据JVM位数(64为还是32位)有关,因为一个solt(槽)占用32位的内存空间 )、部分的返回结果,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址
- 每一个方法从被调用直至执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
- 栈运行原理:栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈,B方法又调用了C方法,于是产生栈帧F3也被压入栈…… 依次执行完毕后,先弹出后进……F3栈帧,再弹出F2栈帧,再弹出F1栈帧
- JAVA虚拟机栈的最小单位可以理解为一个个栈帧,一个方法对应一个栈帧,一个栈帧可以执行很多指令
- 比如当出现main方法需要调用method1()方法的时候,操作指令就会触动这个动态链接就会找到方法区中对应的method1(),然后把method1()方法压入虚拟机栈中,执行method1栈帧的指令;此外如果指令表示的代码是个常量,这也是个动态链接,也会到方法区中的运行时常量池找到类加载时就专门存放变量的运行时常量池的数据

- 本地方法栈(Native Method Stack)
- jvm中的本地方法是指方法的修饰符是带有native的但是方法体不是用java代码写的一类方法,这类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的
- 作用同java虚拟机栈类似,区别是:虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务
- 是线程私有的,它的生命周期与线程相同,每个线程都有一个
- Java堆(Java Heap)
- 在虚拟机启动的时候创建,所有线程共享,Java虚拟机所管理的内存中最大的一块
- 唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存
- Java堆是垃圾收集器管理的主要区域
- 因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间
- java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制
- 如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
- 方法区(Method Area)
- 在虚拟机启动的时候创建,所有jvm线程共享
- 除了和堆一样不需要不连续的内存空间和可以固定大小或者可扩展外,还可以选择不实现垃圾收集
- 用于存放已被虚拟机加载的类信息、常量、静态变量、以及编译后的方法实现的二进制形式的机器指令集等数据
- 被装载的class的信息存储在Methodarea的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中
- 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放

- 执行return命令如果当前线程对应的栈中没有了栈帧,这个Java栈也将会被JVM撤销
- 类加载子系统(Class loader Subsystem)
- 根据给定的全限定名类名(如java.lang.Object)来装载class文件的内容到Runtimedataarea中的方法区
- Java程序员可以extends java.lang.ClassLoader类来写自己的Classloader
- 执行引擎(Execution Engine子系统)
- 负责执行指令集(类加载系统把代码逻辑都以指令的形式加载到了方法区
- 程序在JVM主要执行的过程是执行引擎与运行时数据区不断交互的过程
- 执行引擎拿到的方法区中的指令还是人能够看懂的,这里执行引擎的工作就是要把指令转成JVM执行的语言(也可以理解成操作系统的语言),最后操作系统语言再转成计算机机器码
- 本地方法(native)
- 本地方法就是带有native标识符修饰的方法
- native修饰的方法不提供方法体,但因其实现由非java代码在外部实现,因此不能与abstract连用
- 更多的本地方法最好是与jdk的执行引擎的解释器语言一致
- Windows、Linux、UNIX、Dos操作系统的核心代码大部分是使用C和C++编写,底层接口用汇编编写
- 为什么native方法修饰的修饰的方法PC程序计数器为undefined。在一开始类加载时,native修饰的方法就被保存在了本地方法栈中,当需要调用native方法时,调用的是一个指向本地方法栈中某方法的地址,然后执行方法直接与操作系统交互,返回运行结果。整个过程并没有经过执行引擎的解释器把字节码解释成操作系统语言,PC计数器也就没有起作用
- 结构图
- 双亲委派机制
- JVM在加载类时默认采用双亲委派机制
- 某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,若父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载

- eg:当JVM要加载Test.class时
- 首先会到自定义加载器中查找(其实是看运行时数据区的方法区有没有加载),看是否已经加载过,如果已经加载过,则返回字节码
- 如果自定义加载器没有加载过,则询问上一层加载器(即AppClassLoader)是否已经加载过Test.class
- 如果没有加载过,则询问上一层加载器(ExtClassLoader)是否已经加载过
- 如果没有加载过,则继续询问上一层加载(BoopStrap ClassLoader)是否已经加载过
- 如果BoopStrap ClassLoader依然没有加载过,则到自己指定类加载路径下(”sun.boot.class.path”)查看是否有Test.class字节码,有则返回,没有通知下一层加载器ExtClassLoader到自己指定的类加载路径下(java.ext.dirs)查看
- 依次类推,最后到自定义类加载器指定的路径还没有找到Test.class字节码,则抛出异常ClassNotFoundException
- 为什么要使用双亲委派
- 类加载器代码本身也是Java类,因此类加载器本身也是要被加载的,因此必须有第一个类加载器不是Java类,这就是bootStrap,是使用c++写的
- 虽说bootStrap、extclassLoader、appclassloader三个是父子类加载器关系,但是并没有使用继承,而是使用了组合关系
- 优点,具备了一种带优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,可以比较笼统的说像jdk自带的几个jar包肯定是位于最顶级的,再就是我们引用的包,最后是我们自己写的,保证了java程序的稳定性
- JVM在加载类时默认采用双亲委派机制
- GC垃圾回收机制
- 类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行
- 新生区
- 类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命
- 分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace)
- 所有的类都是在伊甸区被new出来的
- 幸存区有两个:0区(Survivor 0 space)和1区(Survivor 1 space)
- 当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园进行垃圾回收(Minor GC),将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生Major GC(FullGCC),进行养老区的内存清理。若养老区执行Full GC 之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”
- 如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够
- a.Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整
- b.代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
- 养老区
- 养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃
- 永久区
- 永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存
- 如果出现java.lang.OutOfMemoryError: PermGen space,说明永久代Perm内存设置不够
- a. 程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用
- b. 大量动态反射生成的类不断被加载,最终导致Perm区被占满
- Jdk1.6及之前:常量池分配在永久代
- Jdk1.7:有,但已经逐步“去永久代”
- Jdk1.8及之后:无(java.lang.OutOfMemoryError: PermGen space,这种错误将不会出现在JDK1.8中)
- 其他
- 方法区与堆的区别
- 方法区存放了类的信息,有类的静态变量、final类型变量、field自动信息、方法信息,处理逻辑的指令集,仔细想想一个类里面也就这些东西
- 堆中存放是对象和数组,咋一看好像方法区跟堆的作用是一样的
- 这里就关系到我们平时说的对象是类的实例,这里的对应关系就是 “方法区—类” “堆—对象”
- 从另一个角度理解,就是从前我们得知方法区中的类是唯一的,同步的。但是我们在代码中往往同一个类会new几次,也就是有多个实例,既然有多个实例,那么在堆中就会分配多个实例空间内存
- 带有main方法的Bootstrap.java加载过程
- 首先JVM会先将这个Bootstrap.class 信息加载到 内存中的方法区(Method Area)中
- Bootstrap.class 中包含了常量池信息,方法的定义 以及编译后的方法实现的二进制形式的机器指令,所有的线程共享一个方法区,从中读取方法定义和方法的指令集
- 接着,JVM会在Heap堆上为Bootstrap.class 创建一个Class
实例用来表示Bootstrap.class 的 类实例 - JVM开始执行main方法,这时会为main方法创建一个栈帧,以表示main方法的整个执行过程
- main方法在执行的过程之中,调用了greeting静态方法,则JVM会为greeting方法创建一个栈帧,推到虚拟机栈顶
- 当greeting方法运行完成后,则greeting方法出栈,main方法继续运行

- JVM在编译Bootstrap.java 的过程中,在将源代码编译成二进制机器码的同时,会判断其中的每一个方法的三个信息
- 在运行时会使用到的局部变量的数量(作用是:当JVM为方法创建栈帧的时候,在栈帧中为该方法创建一个局部变量表,来存储方法指令在运算时的局部变量值)
- 其机器指令执行时所需要的最大的操作数栈的大小(当JVM为方法创建栈帧的时候,在栈帧中为方法创建一个操作数栈,保证方法内指令可以完成工作)
- 方法的参数的数量
- 首先JVM会先将这个Bootstrap.class 信息加载到 内存中的方法区(Method Area)中
- 方法区与堆的区别


