JDK的体系架构:
JVM的内存模型:
从代码层面进入:
* JVM虚拟机组成部分:
* 1.类加载器
* 2.运行时数据区
* 3.执行引擎
* 4.本地库接口
*
* 最主要部分就是运行时数据区
* 运行时数据区分为:
* 1.堆区
* 2.方法区
* 3.栈区
* 4.本地方法栈
* 5.程序计数器
*
* 栈区:由图可知,我们的栈区内部存放由以下组成:
* 1.局部变量表: 实则就是我们compute()方法中的 a,b,c等等变量值
* 2.操作数栈 实则就是我们用来通过一系列计算使用的工具
* 3.动态连接 实则就是我们调用方法时存储的连接地址
* 4.方法出口 实则就是我们记录下当前引用连接调用之后下一步指向
* 总结:譬如Math类,在执行Main方法后,会在堆区中生成类信息等,在调用compute()方法时,会在我们的方法区中存储静态变量等信息,而静态类信息也会存在在方法区
* 但是方法区仅会存储new User()指向堆区User类信息的地址,这个就是方法区和堆区的关联关系,然后运行代码其中栈区中会开辟一块a的变量内存,b的变量内存,然后通过操作数栈+程序计数器
* 去将我们的c计算出来,程序计数器可以理解为java.class反编译回来的一些数字引用,也就是标识位置的数据,计算出来之后就会存储进入我们的栈区,
*
*
* @author fm
* @date 2022/5/14
*/
public class Math {
public static final int initData = 666;
public static User user = new User();
public int compute() { //一个方法对应一块栈帧内存区域
int a = 1;
int b = 2;
int c = (a + b) * 10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
int compute = math.compute();
System.out.println(compute);
}
}
分析:
JVM组成:
首先通过类加载器(ClassLoader)把 Java 代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器 执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接(Native Interface)来实现整个程序的功能。
JVM虚拟机组成部分:
1.类加载器
2.运行时数据区
3.执行引擎
4.本地库接口
一:类加载器的分类:
类加载器的作用:将class文件加载进JVM的方法区,并在方法区中创建一个java.lang.Class对象作为外界访问这个类的接口。” 实现这一动作的称为类加载器。
1.引导类加载器:
每次执行 java 命令时都会使用该加载器为虚拟机加载核心类。 该加载器是由 native code实现,而不是Java代码, 加载类的路径为/jre/lib。 特别的 /jre/lib/rt.jar 中包含了 sun.misc.Launcher 类, 而 sun.misc.Launcher$ExtClassLoader 和 sun.misc.Launcher$AppClassLoader 都是 sun.misc.Launcher的内部类,所以拓展类加载器和系统类加载器都是由启动类加载器加载的。
2.扩展类加载器:
用于加载拓展库中的类。拓展库路径为/jre/lib/ext/。 实现类为: sun.misc.Launcher$ExtClassLoader。
3.应用程序类加载器:
负责加载用户classpath下的class文件,又叫系统加载器,其父类是Extension。 它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类, 是用户自定义加载器的默认父加载器。 实现类为: sun.misc.Launcher$AppClassLoader
4.自定义类加载器:
自定义类加载器只需要继承 java.lang.ClassLoader 类, 该类有两个核心方法,一个是loadClass(String, boolean), 实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法, 所以我们自定义类加载器主要是重写findClass方法。 ====具体看上一篇源码内容
类加载的生命周期:
类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。
类加载过程详解:
1.加载:
基本概念: 该过程完成查找并加载类的class文件。
该class文件可以来自本地磁盘或者网络等。
虚拟机类加载的第一个阶段,在这个阶段中,虚拟机要完成3件事情:
获取类的二进制字节流
将字节流的静态数据结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区中这个类数据结构的访问入口
而获取类的二进制流又有很多种方式:
从压缩包中获取,比如 JAR包、EAR、WAR包等
从网络中获取,比如红极一时的Applet技术
从运行过程中动态生成,最出名的便是动态代理技术,在java.lang.reflect.Proxy 中,
就是用了ProxyGenerator.generateProxyClass 来为特定接口生成形式为“$Proxy”的代理类的二进制流
从其它文件生成,如JSP文件生成Class 类
2.验证:
基本概念: 确保类型的正确性,比如class文件的格式是否正确、 是否符合语法规定、字节码是否可以被JVM安全执行等 验证总体上分为4个阶段: 文件格式验证、元数据验证、 字节码验证、符号引用验证。
3.准备:
基本概念: 为类的静态变量分配内存,并赋初值。 基本类型的变量赋值为初始值,比如int类型的赋值为0,引用类型赋值为null。
4.解析:
基本概念: 将符号引用转为直接引用。 比如方法中调用了其他方法, 方法名可以理解为符号引用,而直接引用就是使用指针直接引用方法。 ”解析“阶段是虚拟机将常量池内的符号引用替换为直接引用的过程, 主要针对 类或接口、字段、类方法、接口方法、方法类型、方法句柄 和 调用限定符 7类符号引用过程。
初始化:
基本概念: 初始化,则是为标记为常量值的字段赋值的过程。 换句话说,只对static修饰的变量或静态代码块进行初始化。 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。 如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
二:运行时数据区 (Runtime Data Area)
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间;jvm运行时数据区分为以下部分:
注意:方法区 与 Java堆 是所有线程共享的。
Java虚拟机栈、本地方法栈、程序计数器 是线程私有的。
说明: jdk1.8+ 方法区改名叫元数据区(Metaspace)。
方法区:
1.方法区在jdk1.7中称之为永久代
2.方法区在jdk1.8中改名称之为元数据
方法区是线程共享的区域:存储已被虚拟机加载的类元信息、方法元信息、常量池。方法区无法满足内存分配需求时,抛出OutOfMemoryError。JVM规范并没对这个区域实现垃圾收集,因为这个区域回收主要针对的是类信息、方法信息、常量池的信息回收;
常量池: 是方法区的一部分。Java语言不要求常量只能在编译期产生,换言之,在运行期间也能将新的常量放入。
jdk1.7 方法区内存大小设置: -XX:PermSize -XX:MaxPermSize
jdk1.8 元数据区内存大小设置: -XX:MetaspaceSize -XX:MaxMetaspaceSize
方法区中内存默认大小为21m
代码演示(元空间内存溢出):
public class MetaspaceTest {
public static void main(final String[] args) {
while (true){
// 创建字节码增强器对象
Enhancer enhancer = new Enhancer();
// 设置父类
enhancer.setSuperclass(MetaspaceTest.class);
// 设置是否使用缓存
enhancer.setUseCache(false);
// 设置回调对象(方法拦截器)
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(o,args);
}
});
enhancer.create();
}
}
}
输出:内存溢出
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:237) at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377) at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285) at cn.itcast.jvm.MetaspaceTest.main(MetaspaceTest.java:26) Process finished with exit code 1
cglib动态代理可以动态创建代理类,这些代理类的Class会动态的加载入内存中,存入到方法区。所以当我们把方法区内存调小后便可能会产生方法区内存溢出,1.8之前的JDK我们可以称方法区为永久代 :PermSpace 1.8之后方法区改为 MetaSpace 元空间。
Java堆:
Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。在 Java 中,堆被划分成两个不同的区域:新生代 (Young)、老年代 (Old)。新生代 (Young) 又被划分为三个区域:Eden、From Survivor、To Survivor。 这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
Java Heap(堆大小) = 新生代 + 老年代 【新生代 与 老年代 的比例的值为 1:2】
新生代 = Eden + Survivor(S0) + Survivor(S1) 【Eden:S0:S1 = 8:1:1】
Eden:伊甸园 S0(Survivor 0):幸存者0 S1(Survivor 1):幸存者1
常说的“栈内存”、“堆内存”,其中前者指的是虚拟机栈, 后者说的就是Java堆了。Java堆是被线程共享的。在虚拟机启动时被创建。 Java堆是Java虚拟机所管理的内存中最大的一块。 Java堆的作用是存放对象实例,Java堆可以处于物理上不连续的内存空间中,只要求逻辑上连续即可。 堆空间内存大小设置: -Xms20m Java堆初始化内存 -Xmx20m Java堆最大内存 -Xmn15m 新生代内存大小 -XX:SurvivorRatio=8 新生代Eden/Survivor空间的初始比例 -XX:NewRatio=2 Old区 和 Yong区 的内存比例
查看默认新生代与老年代内存比例:java -XX:+PrintFlagsFinal -version | grep NewRatio
代码演示(堆内存溢出):
public class HeapTest {
public static void main(String[] args) throws Exception {
// 创建List集合
List list = new ArrayList();
// 循环往集合中添加元素
while (true){
Thread.sleep(5);
list.add(new byte[1024]);
}
}
}
输入打印:内存溢出
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at cn.kirin.jvm.HeapTest.main(HeapTest.java:16) Process finished with exit code 1
java虚拟机栈:
栈内部信息:
1.局部变量表 :存储代码中的变量值
2.操作栈 :结合cpu寄存器也就是程序计数器计算值
3.动态连接: 方法引用,接口引用,类型引用等等改为直接引用
4.方法出口(方法返回地址):方法执行完成后指向下一步的地址
1.Java虚拟机栈是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)。
2.Java虚拟机栈描述的是Java方法执行的内存模型: 每个方法执行的同时会创建一个栈帧。对于我们来说,主要关注的stack栈内存,就是虚拟机栈中局部变量表部分。
3.栈帧存储: 局部变量表、操作数栈、动态链接、方法返回地址。每一个方法从调用到结束,就对应这一个栈帧在虚拟机栈中的进栈和出栈过程。
4.局部变量表: 8种基本数据类型、对象引用(reference类型)、returnAddress类型(一条字节码地址)。
1.如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
2.如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
栈的内存大小设置: -Xss128k 为jvm启动的每个线程分配的内存大小,默认1m。
线程请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError:
循环调用
public class StackOverflowTest {
// 定义变量,记录栈的深度
private int stackLength = 1;
// 定义方法 2.此处开辟一个栈内存空间
public void stackLeak(){
stackLength++;
//3.此处也开辟一个栈内存空间再此调用,循环往复
stackLeak();
}
public static void main(String[] args) {
// 创建对象
StackOverflowTest stackOverflow = new StackOverflowTest();
try {
// 调用方法 1.这里调用方法开辟一个栈内存空间
stackOverflow.stackLeak();
} catch (Throwable e) {
System.out.println("当前栈深度:" + stackOverflow.stackLength);
e.printStackTrace();
}
}
}
打印结果:栈深度不够
当前栈深度:995
java.lang.StackOverflowError
at cn.kirin.jvm.StackOverflowTest.stackLeak(StackOverflowTest.java:10)
at cn.kirin.jvm.StackOverflowTest.stackLeak(StackOverflowTest.java:11)
at cn.kirin.jvm.StackOverflowTest.stackLeak(StackOverflowTest.java:11)
方法递归调用,造成深度过深,产生异常
虚拟机栈扩展时无法申请到足够的内存,抛出OutOfMemoryError:(代码谨慎使用,会引起电脑卡死)**
public class StackOutOfMemoryTest {
private void doStop(){
while (true){
}
}
public void stackLeakByThread(){
while(true){
new Thread(new Runnable() {
public void run() {
doStop();
}
}).start();
}
}
public static void main(String[] args) {
StackOutOfMemoryTest stack = new StackOutOfMemoryTest();
stack.stackLeakByThread();
}
}
打印结果:栈内存溢出
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
单线程下栈的内存出现问题了,都是报StackOverflow的异常,只有在多线程的情况下,当新创建线程无法在分配到新的栈内存资源时,会报内存溢出。
java本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。本地方法栈也会抛出 StackOverflowError、OutOfMemoryError。
java程序计数器:
-
线程私有。可看作是当前线程所执行的字节码的行号记录器,字节码解释器的工作是通过改变这个计数值来读取下一条要执行的字节码指令。
-
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,也就是说,在同一时刻一个处理器内核只会执行一条线程。为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。这就是一开始说的“线程私有”。如果线程正在执行的方法是Java方法,计数器记录的是虚拟机字节码的指令地址;如果是Native方法,计数器值为空。
-
程序计数器是唯一一个在Java虚拟机规范中没有规定OOM(OutOfMemoryError)情况的区域。



