首先我们要知道java堆空间的产生过程: 即当通过java命令启动java进程的时候,就会为它分配内存,而分配内存的一部分就会用于创建堆空间,而当程序中创建对象的时候 就会从堆空间来分配内存,所以堆空间存放的主要是对象和数组;
而GC 其实说白了就是java虚拟机回收对象的机制,即回收无效对象的内存用于将来的分配。
JVM包含两个子系统和两个组件,两个子系统为Class loader(类装载)、Execution engine(执行引擎);两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
那么他们分别有啥作用呢?
首先看Class loader:根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。其实说白了就是类加载器,简单点-作用就是通过类加载器将编译好的class文件加载到运行时数据区
Execution engine(执行引擎): 执行class中的指令
Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
整体的过程:首先会通过编译器把Java代码转换成字节码源文件,之后类加载器会把字节码文件加载到内存中,即加载到运行时数据区,
但是其实字节码文件只是JVM的一套指令集规范,并不能直接交给底层操作系统来去执行,因此需要特定的命令解析器即执行引擎,将字节码翻译成底层的系统指令,再交由CPU去执行。
同时java代码中 可以调用其他语言的本地库接口,进行一些系统调用或者c函数的调用
jvm内存模型大致被划分为如下几个区域:
程序计数器:也就是当前线程所执行的字节码指令的行号指示器,说白了就是当前字节码执行到的位置,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令
java虚拟机栈:用于存储方法执行时的局部变量表、操作数栈、动态链接、方法出口等信息,也是线程私有的。每个方法在执行时都会创建一个栈帧,栈帧包含了方法的局部变量表、操作数栈等信息。
本地方法栈:与 Java 虚拟机栈类似,只不过虚拟机栈是服务 Java
方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;
堆:用于存储对象实例和数组,它也是jvm中最大的一块内存区域,同时也是线程共享的,而堆包括年轻代和老年代,以支持垃圾回收机制。
方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
以Hotspot为例,堆内存主要由GC模块进行管理和分配,可以分为新生代和老年代,而新生代又可以分为eden区,s1区和s2区,并且他们的比例默认为8:1:1, 同时新生代和老年代的占比如下图所示
在使用堆内内存(on-heap memory)的时候,完全遵守JVM虚拟机的内存管理机制,采用垃圾回收器(GC)统一进行内存管理,
GC会在某些特定的时间点进行一次彻底回收,也就是Full GC,GC会对所有分配的堆内内存进行扫描,在这个过程中会对JAVA应用程序的性能造成一定影响,还可能会产生Stop The World。
通常来说方法区是非堆内存,而jdk1.8之前的方法区实现是永久代,而jdk1.8之后的方法区实现叫元空间
堆外内存, 常常又叫做直接内存。 和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,
这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。
具体实现 可以 用java.nio.DirectByteBuffer对象进行堆外内存的管理和使用
那么堆外内存的优点是什么?
减少垃圾回收:因为垃圾回收会暂停其他的工作。
加快复制速度:堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。
方法区是《Java虚拟机规范》中定义的内存区域,用于存储类的结构信息(如类的字节码、常量池、字段和方法信息等),
而 Java 默认虚拟机 HotSpot 中,在 JDK 1.8 之前的版本中,是通过永久代来实现方法区的,但 JDK 1.8 之后,永久代被元空间(Metaspace)取代。
所以,总结来看,方法区是规范,而永久代(和元空间)是具体实现。
主要原因有以下几点,
其实主要是为了提高回收效率,便于及时的回收常量池内存,同时也是会缓解永久代空间不足的问题
首先我们要知道字符串常量池在1.7之前是放到永久代的,而永久代的回收效率是很低的,只有在fullGC的时候才会触发,而fullGC的触发也是需要再老年代的空间不足,或者永久代不足时才会进行触发,这样就导致了字符串常量池的回收效率并不高。但其实在真正的开发中,我们一般会有大量的字符串常量被创建,回收效率低反过来也会导致永久代空间不足
因此在jdk1.7之后字符串常量池被放到了堆空间中,便于及时的回收内存
类变量
类变量是用static修饰符修饰,定义在方法外的变量,随着java进程产生和销毁。在java8之前把静态变量存放于方法区,在java8时存放在堆中
成员变量
成员变量是定义在类中,但是没有static修饰符修饰的变量,随着类的实例产生和销毁,是类实例的一部分
由于是实例的一部分,在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中
局部变量
局部变量是定义在类的方法中的变量
在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中
具体区别可以看以下几点
1 存放的内容
堆存放的是对象的实例和数组,而栈存放的是局部变量,操作数栈
2 再从是否私有来看
堆对于整个应用程序来说都是共享的,而栈是线程独享,所以也是线程私有。他的生命周期和线程相同
3 物理结构
堆的物理地址分配对对象是不连续的。因此性能慢些。在GC的时候也要考虑到不连续的分配,所以有各种算法。
栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。
说到双亲委派模式 我们先聊下什么是类加载器 以及类加载器的分类
类加载器(Class Loader)是 Java 虚拟机(JVM)的重要组成部分,负责将字节码文件加载到内存中并转换为可执行的类
而类加载器大致可以分为四种
启动类加载器:加载 JDK 中 lib 目录中 Java 的核心类库,即$JAVA_HOME/lib目录。
扩展类加载器:加载 lib/ext 目录下的类;
应用程序类加载器:加载我们写的应用程序;
自定义类加载器:根据自己的需求定制类加载器。
而 双亲委派模型是 Java 类加载器的一种工作机制。
它是指当一个类加载器需要加载一个类时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最 终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
双亲委派模型的优点是啥?
避免重复加载类:比如 A 类和 B 类都有一个父类 C 类,那么当 A 启动时就会将 C 类加载起来,那么在 B 类进行加载时就不需要在重复加载 C 类了。
提高安全性:使用双亲委派模型也可以保证了 Java 的核心 API 不被篡改,如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类,而有些 Object 类又是用户自己提供的因此安全性就不能得到保证了。
那么如何打破双亲委派机制呢?
1.自定义类加载,重写loadclass方法
因为双亲委派的机制都是通过这个方法实现的,这个方法可以指定类通过什么类加载器来进行加载,所有如果改写他的加载规则,相当于打破双亲委派机制
2.使用线程上下文类
双亲委派模型的第二次“破坏”是由这个模型自身的缺陷所导致的,双亲委派很好的解决了各个类加载器的基础类统一问题,基础类之所以“基础”,是因为他们总被用户代码所调用,但是如果基础类又要重新调用用户代码,那咋办?
比如说JNDI是java的标准服务,它的代码是由启动类加载器进行加载的,但是jndi的作用就是进行资源的集中管理和查找,它需要调用由开发人员开发在classpath下的类代码,但是启动类加载器不会进行加载。
所以引入线程上下类加载器,通过java.lang.Thread类的setContextClassLoader()方法进行设置。如果创建线程是还未设置,它会从父线程继承一个,如果在应用程序全局范围内没有设置,那么这个线程上下类加载器就是应用程序类加载器。
那么这样JNDI服务使用这个线程上下类加载器去加载所需的spi代码,也就是父类加载器请求子类加载器去完成类加载的动作,这个实际是打通了双亲委派的逆向层次结构。