JDK = JRE + Java编译工具(如javac,java,javap等)
JVM组成
- 类加载子系统
- JVM内存
- 执行引擎
JVM内存结构
=======
程序计数器
作用:记录下一次要执行的JVM指令的内存地址
运作:当一个线程启动时,JVM就会为该线程创建一个私有的程序计数器内存,线程运行中执行Java代码,在JVM底层来看都是执行其对应的Java字节码中的JVM指令,而每个JVM指令都有一个行号(即该指令在内存中的地址),线程将要执行的下一条指令内存地址存入其私有的程序计数器中,当线程抢夺到CPU时间片时,JVM执行引擎中的解释器就会去该线程的程序计数器中获取要解释的指令,并解释为操作系统级别的机器码,交给CPU执行。当线程失去CPU时间片时,线程会继续将下一个要执行的指令的地址存入其私有的程序计数器,等到再次获取CPU时间片后,可以接着执行。
特点:
- 线程私有(随着线程的启动而创建,随着线程的死亡而销毁)
- 不存在内存溢出(只存储下一条执行指令的地址值)
- 内存小,速度快,物理实现是寄存器
注意:对于非本地方法,即Java方法,程序计数器记录的是下一条将要执行的JVM指令的地址,对于本地方法,即Native方法,程序计数器记录的是空。
虚拟机栈
虚拟机栈的组成单位是:栈帧。
栈帧的组成是:局部变量表,操作数栈,方法返回地址,动态链接。
虚拟机栈:JVM会每个线程创建一个虚拟机栈内存。即虚拟机栈是线程运行过程中需要的内存。虚拟机栈是线程私有的。
栈帧:线程中每次方法调用对应虚拟机栈有一个栈帧入栈,每次方法调用结束都对应虚拟机栈一个栈帧出栈。即栈帧是方法运行过程中需要的内存。
注意:线程只能有一个活动栈帧,就是当前正在执行的方法,即虚拟机栈顶的栈帧。
局部变量表:方法运行中方法参数,局部变量所需要的内存,存储单位称为槽位。
操作数栈:方法运行中方法参数,局部变量运算所需要的内存。
返回地址:方法出口。
动态链接:是找到正确的方法入口,然后才有后来的进栈出栈的执行。
问题0:虚拟机栈是否会出现内存溢出?
会,虚拟机栈是一种栈结构,它有着入栈出栈两种行为。当入栈的栈帧数目大于虚拟机栈的深度时,就会导致java.lang.StackOverFlowError。现实例子比如方法的递归调用,但是递归方向是未知的。还有一种情况是单个栈帧内存过大,直接超出了虚拟机栈剩余内存大小,比如一个方法中定义了过多的变量,此时会报错java.lang.StackOverFlowError。
还有一种情况,就是创建过多的线程,导致没有足够的内存给新线程创建虚拟机栈,此时会报错java.lang.OutOfMemoryError。
问题1:虚拟机栈内存是否越大越好?
JVM可以通过-Xss参数来设置单个线程的虚拟机栈的内存大小,如果单个线程的栈内存设置的越大,则总线程数越少。另外当创建的线程过多时,可能出现OOM。
问题2:虚拟机栈内存是否需要被垃圾回收?
不需要,因为虚拟机栈的生命周期就是线程的生命周期。当线程结束时,虚拟机栈内存被JVM自动回收。相较于堆中对象,虚拟机栈中的数据生命周期更短且更容易判断是否存活。
问题3:方法内的局部变量是否线程安全?
线程安全场景出现的前提是:1.多线程环境 2.多线程共享操作同一个数据
一个方法被多线程执行,
如果方法内的局部变量来自于外部传入(即方法参数),则可能该局部变量线程不安全,
如果方法内的局部变量会作为方法返回值返回,则可能局部变量线程不安全,
如果方法内的局部变量,既不从外部传入,也不传出给外部,则局部变量线程安全。
当然前面三个判断是以局部变量是引用类型为前提的,如果方法内局部变量是基本类型,则一定线程安全。
本地方法栈
和虚拟机栈没有本质区别。
只是虚拟机栈是给Java方法用的,本地方法栈是给Native方法用的。
本地方法栈也会出现StackOverFlowError和OOM。
堆
定义:所有的对象实例和数组实例都存在堆中。
特点:
- 线程共享,堆中对象都需要考虑线程安全的问题
- 堆内存是GC管理的内存区域,可以实现垃圾对象内存回收。
- 堆内存是实现分代的,如经典分代将堆分为两块:新生代,老年代
- 堆内存可以划分出线程私有的TLAB,即本地线程分配缓冲区,提高内存分配效率,减少内存分配冲突。
- 堆内存在物理上可以是不连续的内存组成,但是逻辑上是连续的。
- 堆内存既可以是固定大小的,也可以可扩展大小的。可以通过虚拟机参数控制:
- -Xms 堆初始内存大小
- -Xmx 堆最大内存大小
注意:当堆内存不足以分配时,就会发生堆内存溢出,即报错java.lang.OutOfMemoryError:Java heap space
方法区
定义:是线程共享的内存区域。方法区是堆的逻辑部分,但是也叫非堆。
作用:存储加载的类型信息,常量,静态变量,即时编译器编译代码的缓存
实现:方法区只是一种概念,对于Hotspot虚拟机,它的具体实现分为两阶段
- JDK8:元空间,元空间处于本地内存中
- JDK8以前:永久代,永久代处于JVM内存中
设置方法区内存大小的虚拟机参数变化:
JDK8:-XX:MaxMetaspaceSize=
JDK8以前:-XX:MaxPermSize=
方法区内存溢出报错变化:
JDK8:java.lang.OutOfMemoryError:Metaspace
JDK8以前:java.lang.OutOfMemoryError:PermGen space
问题:是否会存在方法区溢出的场景?
会,大量加载类到方法区,当前流行的Spring,Mybatis框架都使用了cjLib,即底层使用ClassWriter原理自动生成二进制类。
运行时常量池
常量池和运行时常量池的关系:
常量池是class文件,即二进制字节码文件中的Constant Pool信息,当字节码被类加载器加载到方法区后,它的Constant Pool信息就会放入运行时常量池,并把里面的符号地址变为真实地址。
串池和运行时常量池的关系:
运行时常量池中的字符串仅仅是符号,而不是对象。只有对应字符串符号第一次被使用时,才会变成字符串对象,并存入串池。
串池特点:
- 字符串对象的延迟加载特性:运行时常量池中的字符串仅仅是符号,只有运行时常量池中的字符串符号第一次被使用时才会变成字符串对象。
- 字符串对象的去重特性:当需要使用字符串时,会先使用字符串符号去串池中查找是否由对应的字符串对象,如果有,则直接引用串池中的字符串对象。
- 可以使用String::intern方法,主动将串池中还没有的字符串对象放入串池
- 字节串+号拼接优化:
对于字符串变量+拼接:底层new StringBuilder().append(“x”).append(“y”).toString();
对于字符串常量+拼接:编译期优化,则直接替换为拼接结果字符串对象。
串池位置:
JDK6:永久代
JDK7:堆
问题:如何验证串池位置
我们可以利用String::intern()方法往串池中加入大量字符串对象,撑爆内存,使之抛出OOM,对于永久代内存溢出报错:java.lang.OutOfMemoryError:PermGen space,对于堆内存溢出报错:java.lang.OutOfMemoryError:Java heap space,所以可以通过内存溢出报错来辨别串池位置。
问题:为什么要将串池位置从永久代换到堆中?
因为Java程序运行中会产生大量的字符串对象,如果不及时回收字符串对象内存,很有可能造成内存溢出,而永久代只有在Full GC时才会进行回收,而堆内存新生代会经常触发Minor GC,代价更小,频率更高。
问题:串池位置的变化对String::intern()方法返回结果的影响?
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
65)]
[外链图片转存中…(img-CfufOrjL-1715549145066)]
[外链图片转存中…(img-QpGAKXku-1715549145066)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!