浅谈JVM,分析jvm的体系结构,jdk1.8、1.7、1.6有什么区别。
文章目录
- JVM
- 一、思考
- 二、jvm探究
- 2.1、什么是jvm
- 2.2、jvm的位置
- 2.3、虚拟机
- 2.4、作图(本人心得)
- 三、jvm体系结构
- 3.1、体系结构图
- 3.2、类加载器
- 3.2.1、概念及作用
- 3.2.2、类的加载过程(生命周期)
- 3.2.3、加载器
- 3.2.4、双亲委派机制
- 3.2.5、沙箱安全机制
- 3.3、程序计数器
- image-20220410013938381
- 3.4、虚拟机栈
- 3.4.1、栈和队列
- 3.4.2、什么是栈
- 3.4.3、栈与栈帧
- 3.4.4、栈溢出(StackOverflowError)
- 3.5、本地方法栈
- 3.6、堆
- 3.6.1、什么是堆
- **3.6.2、堆溢出**
- 3.6.3、堆内存快照抓取
- 3.6.4、堆空间演变
- 3.7、方法区
- 3.7.1、什么是方法区
- 3.7.2、方法区溢出
- 3.7.3、jdk8和之前的区别
- 3.7.4、常量池
- 3.7.5、运行时常量池
- 3.7.6、字符串常量池
JVM
一、思考
- 什么是JVM
- JVM的位置,为什么有强大的跨平台性?
- JVM的体系结构
- JVM类加载器的类加载过程?什么是双亲委派机制?什么是沙箱安全机制?
- 为什么要用程序计数器(PC寄存器)?程序计数器存在内存溢出吗?
- 什么是OOM?什么是堆溢出?什么是栈溢出?
- 栈的生命周期?什么是栈帧?
- 内存快照怎么抓取?
- JVM调优主要针对的哪部分调优
- jdk1.8虚拟机和之前有什么变化
二、jvm探究
2.1、什么是jvm
- JVM 是 java虚拟机,是用来执行java字节码(二进制的形式)的虚拟计算机。
- 具体详细介绍可以看 百度百科
- 下面是百度百科对 JVM 的定义
2.2、jvm的位置
-
jvm是运行在操作系统之上的
-
Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。
原理:使用Java编译器编译Java程序时,生成的是与平台无关的字节码,这些字节码只面向JVM。不同平台的JVM都是不同的,但它们都提供了相同的接口,只需要在不同的操作系统上安装一个对应版本的虚拟机(JVM) 。
2.3、虚拟机
- 通过 Java -version 命令可以查看自己是什么虚拟机(常用的是HotSpot)
- HotSpot VM,是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。
- 还有其他几种虚拟机有兴趣可以自行了解
2.4、作图(本人心得)
- JVM相对抽象,不是口头就能描述清楚,通过图片,我们才能更直观,便于理解。
- 要想清楚的记住JVM的体系,一定要自己作图,只有自己多次画图,才能在脑海里形成清楚的记忆。
- 网上有很多详细的体系图,我们可以先画一些简单的简图,等到熟练以后再去看每一块详细的图,一步一步理解。
- 能自己动手尝试的东西有空一定要尝试,只有自己动手才能记得牢固,有时候看了一遍理解了,过个一两个月就忘记了。只有频繁的使用,才会形成深度记忆,只有真正的理解,才能永不遗忘。
- 作图:https://www.processon.com/ 或者本地画图软件
三、jvm体系结构
3.1、体系结构图
-
java文件经过编译器(编译器前端)编译为字节码文件–>经过类加载子系统(将字节码加载到内存当中,生成一个class对象,中间经过三步:加载—>链接—>初始化 具体的可以查看类加载器内部图)
-
在内存中,方法区和堆区是多个线程共享区。
-
Java虚拟机栈,本地方法栈,程序计数器每一个线程独有。
3.2、类加载器
3.2.1、概念及作用
概念:
- 类的加载指的是将类的.class文件中的 二进制数据读入到方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
- 类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError、ClassNotFoundException错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
作用:
- 将class文件加载到虚拟机的内存
3.2.2、类的加载过程(生命周期)
1、加载:查找并加载类的二进制数据加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取其定义的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
2、验证:确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证:
- 文件格式的验证:验证字节流是否符合Class文件格式的规范,例如是否以0xCAFEBABE开头,版本号是否合理
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。例如是否有父类,继承了final类?非抽象类实现了所有的抽象方法
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private、protected、public、default)是否可被当前类访问。
3、准备:正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配
- 类变量(static )和 全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的值。
- 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过。
- 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
- 局部变量不需初始化。
4、解析:虚拟机将常量池内的符号引用替换为直接引用的过程
5、**初始化:**类加载最后阶段,若该类具有超类,则对其进行初始化。只有当对类的主动使用的时候才会导致类的初始化,类的主动使用 包括以下六种:
- 创建类的实例,也就是new的方式
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射
- 初始化某个类的子类,则其父类也会被初始化
- Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
6、**使用:**使用类进行业务操作。
7、**卸载:**结束生命周期
- 执行System.exit()方法
- 程序正常执行结束
- 由于操作系统出现错误而导致Java虚拟机进程终止
3.2.3、加载器
1、启动类加载器(Bootstrap ClassLoader)
- 启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分
- 它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中
- 注意由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
2、扩展类加载器(Extension ClassLoader)
- 扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类
- 它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
- jdk8后废弃,使用平台类加载器(Platform ClassLoader)替换了原来的扩展类加载器(Extension ClassLoader)
3、系统类加载器(应用程序加载器)
- 应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader
- 负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径
4、自定义类加载器
- 用户根据自己业务需求自己定义的加载器
接下来我们通过代码看一下:
/**
* 类加载器测试类
*/
public class Demo01 {
public static void main(String[] args) {
Demo01 demo = new Demo01();
//输出应用程序加载器
System.out.println(demo.getClass().getClassLoader());//AppClassLoader
//输出扩展类加载器
System.out.println(demo.getClass().getClassLoader().getParent());//PlatformClassLoader
//输出启动类加载器
System.out.println(demo.getClass().getClassLoader().getParent().getParent());//null
}
}
jdk8输出:
jdk18输出:
- 首先输出的是应用程序加载器
- 然后是平台类加载器(jdk8以后扩展类加载器被丢弃)
原因:
1、在JDK8中的这个Extension ClassLoader,主要用于加载jre环境下的lib下的ext下的jar包。当想要扩展Java的功能的时候, 把jar包放到这个ext文件夹下。然而这样的做法并不安全,不提倡使用。
2、这种扩展机制被JDK9开始加入的“模块化开发”的天然的扩展能力所取代
- 最后启动类加载器输出了null,这是因为启动加载器使用C++语言实现,我们获取不到。
3.2.4、双亲委派机制
先看代码(在我们的src下建一个java.lang包)
package java.lang;
/**
* 自定义String类
* 放到自己建的 java.lang包下
*/
public class String {
static {
System.out.println("自定义String类");
}
public static void main(String[] args) {
System.out.println("自定义Sting类");
}
}
jdk8输出:
- 如果你的jdk版本过高,会显示: 程序包xxx不可见,程序包xxx已在模块 java.base,都是正常的。
并且会导致其他类也无法运行,删除String类即可。
新增测试类
/**
* 测试类,测试加载时是否会调用自定义String类的静态代码块
*/
public class Demo02 {
public static void main(String[] args) {
String s = new String();
System.out.println("Hello String");
}
}
运行:
发现我们自定义的String类并不会执行,说明类加载器加载的不是我们自定义的String,这也就解释了为什么找不到main方法,因为
java.lang下的String类是没有main方法的,这就涉及到了我们的双亲委派机制。
双亲委派机制:
- 如果一个类加载器收到类的加载请求,他并不会自己先去加载,而是把这个请求先委托给自己的父类去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终会到达最顶层的启动类加载器
- 如果父类加载器可以完成加载任务,就成功返回,倘若父类加载器无法完成此类的加载任务,子加载器才会尝试自己去加载,这就是双亲委派模型
通俗的说就是,自定义的类,但是系统类加载器不会直接加载,先向上传递给扩展类加载器,扩展类加载器在向上传递给启动类加载器,因为启动类加载器没有父类加载器,所以他就尝试自己加载,但是发现自己不能加载,然后他就向下传递给扩展类加载器,但是扩展类加载器发现自己也不能加载,就在向下传递,最终由系统类加载器进行加载(注意:扩展类加载器和启动类加载器有自己的类的加载目录,不在此目录中,这两个加载器就不会加载)
一句话总结:向上委托,向下加载。
工作原理图:
优势:
- 避免类的重复加载
- 使得java中的类随着他的类加载器一起具备了一种带有优先级的层次关系,会保证系统的类不会受到恶意的攻击
- 保护程序安全,防止核心API被随意篡改
3.2.5、沙箱安全机制
什么是沙箱?
Java安全模型的核心就是Java沙箱,沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在 虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
java中的安全模型:
- 在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱 (Sandbox) 机制。如下图所示:
JDK1.0安全模型
- 但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的 Java1.1 版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图所示
JDK1.1安全模型
- 在 Java1.2 版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示
JDK1.2安全模型
- 当前最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示
3.3、程序计数器
什么是程序计数器?
- 程序计数器(Program Counter Register)是一个记录着当前线程所执行的字节码的行号指示器.
- Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。它占用很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域
- 每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致。
- 不会存在内存溢出(OutOfMemoryError)
看一段代码:
/**
* 程序计数器
*/
public class Demo03 {
public static void main(String[] args) {
int a = 10;
int b = 20;
int c = a + b;
}
}
然后将代码进行编译成字节码文件
Code:
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: return
左边的指令地址就是程序计数器记录的地址
优点:
- CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
- JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
PC寄存器为什么被设定为私有的?
- 所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。
- 由于cpu时间片轮限制,多个线程在执行的过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某一个线程中的一条指令。这样必然导致经常的中断和恢复,如何保证在这个过程中不出现差错,为了能够准确的记录各个线程正在执行的当前字节码指令的地址,最好办法是为每一个线程都分配一个pc寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现干扰的情况。
3.4、虚拟机栈
3.4.1、栈和队列
- 栈:先进后出,后进先出
- 队列:先进先出(FIFO)
栈:就像一个弹夹,先压进去的子弹后出来
队列:就像一个管道,先进去的先出来
3.4.2、什么是栈
1、栈的概念
- 栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
2、生命周期
- 栈的生命周期和线程的生命周期一致,随着线程的启动而创建,随着线程的停止而销毁。当主线程结束后,整个的虚拟机的栈就全部销毁掉。
3、存放类型
- 8大基本类型 + 对象的引用 + 实例的方法
4、特点
- 访问速度快,仅次于程序计数器
- 线程私有
- 存在 OOM,不存在 GC
3.4.3、栈与栈帧
1、概念
- 栈:线程运行时需要的内存空间。
- 栈帧:每个方法运行时需要的内存,栈帧是栈的基本单位。每个线程只能有一个活动栈帧,对应当前执行的那个方法
例如下图:main(),test(),test1()都是一个栈帧,test1()是活动栈帧,也可以说是当前栈帧
2、运行原理
- 入栈时,栈帧1会通过子帧指向栈帧2
- 出栈时栈帧2会通过父帧指向栈帧1
3、栈帧演示
- 代码
/**
* 栈帧演示
*/
public class Demo04 {
public static void main(String[] args) {
test();
}
private static void test(){
test1(1,2);
}
private static int test1(int a, int b) {
int c = a + b;
return c;
}
}
- debug启动,一步一步观察
- 发现main()先入栈,然后是test(),最后是test1()
- 再走下去,test1()先出栈,然后是test(),最后是main()
4、总结
- 每个线程在创建的时候都会创建一个虚拟机栈,其内部都会保存一个个的栈帧,对应着一次次的方法调用
- 栈是线程私有的,会存在OOM,不存在GC
- 随着线程的启动而创建,随着线程的停止而销毁
- 先进后出
3.4.4、栈溢出(StackOverflowError)
1、栈溢出原因
- 栈帧过多
- 栈帧过大(不容易出现)
代码演示(递归调用)
- 采用递归,方法不停的调用方法,栈帧就会过多,导致栈溢出
/**
* 测试内存溢出
*/
public class Demo05 {
private static int count;
public static void main(String[] args) {
try {
test();
} catch (Throwable e) {//注意捕获异常,用Throwable,不要用Exception
e.printStackTrace();
System.out.println(count);//捕获异常,打印执行方法次数
}
}
private static void test(){
count++;
//递归调用
test();
}
}
输出:
- 可以看出发生了栈溢出(StackOverflowError),记住这个错误,error错误Exception无法捕获
- 我这里方法执行了32059次,我们可以自己设置栈大小
2、设置栈内存大小
-Xss256k//大小自己设定
- 我的idea版本没有,所以需要添加一个,正常的应该有VM options,直接输入就可以
- 可以看到把栈内存设置小了以后,4368次栈就溢出了
总结:
- 递归调用没有终止条件会导致栈溢出
- 有时候调用第三方接口也会产生栈溢出,互相调用产生了循环(套娃)。
- 如果虚拟机栈容量不可以动态扩展,当线程请求的栈深度大于虚拟机允许的最大容量,就会抛出 StackOverFlowError 异常;
- 如果虚拟机栈容量可以动态扩展,当栈无法申请到足够的内存会抛出 OutOfMemoryError 异常。
3.5、本地方法栈
1、概念
- 一个Native Method就是一个Java调用非Java代码的接口
- 本地方法接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序
- 标识符native可以与其他的Java标识符连用,但是abstract除外
2、特点
- 本地方法栈也是线程私有的
- 实现Java应用与Java外面的环境交互
- 标识符native
3、Native关键字
- 我们打开Object类(Alt+7),可以看到很多方法都是Native调用的
- 线程star()
代码
/**
* Native
* 线程 start()
*/
public class Demo06 {
public static void main(String[] args) {
new Thread(()->{
},"my threa").start();
}
}
点进去看start()方法,可以看到调用了start0(),然后start0()是native修饰
4、总结
- 本地方法栈也是线程私有的
- native关键字会进入本地方法栈,调用本地方法接口,调用其他语言的库
- java诞生的时候C和C++很火,想要有一席之地,必须调用C C++.
3.6、堆
3.6.1、什么是堆
1、概念
- 一个JVM实例只有一个堆内存,堆也是Java内存管理的核心区域,堆在JVM启动的时候创建,其空间大小也被创建,是JVM中最大的一块内存空间,所有线程共享Java堆,物理上不连续的逻辑上连续的内存空间,几乎所有的实例都在这里分配内存,在方法结束后,堆中的对象不会马上删除,仅仅在垃圾收集的时候被删除,堆是GC(垃圾收集器)执行垃圾回收的重点区域。
- 使用New关键字,创建的对象都会使用堆内存
2、特点
- 线程共享,要考虑线程安全问题
- 一个JVM实例只有一个堆内存
- 有GC垃圾回收
- 响性能主要因素之一,垃圾回收的重点区域
3.6.2、堆溢出
1、溢出原因
- 对象实例过多
代码
/**
* 测试堆溢出
* -Xmx8m
*/
public class Demo07 {
public static void main(String[] args) {
int count = 0;//计数
try {
ArrayList<String> list = new ArrayList<>();
String a = "Heap 堆内存";
/*
1、死循环做对象拼接
2、String是固定不变的,每次拼接都是一个新的对象
3、对象存在list集合中,由于不停的存放,所以list集合不能被垃圾回收
4、字符串对象存在集合里,集合还在,字符串对象也不能被垃圾回收
*/
while(true){
list.add(a);
a=a+a;
count++;
}
} catch (Throwable e) {//捕获最大的错误
e.printStackTrace();
System.out.println(count);
}
}
}
输出:
- 报错:内存溢出,堆空间不足(OutOfMemoryError Java heap space)
2、设置堆内存大小
-Xmx8m
- 只执行了16次
3.6.3、堆内存快照抓取
- jps工具:查看当前系统中有那些进程
- jmap工具:查看堆内存占用情况
- jconsole工具:图形界面的,多功能的监测工具,可以连续监测
代码
/*
* 堆内存监测
* 命令:
* 1、jps:查看当前系统中有那些进程
* 2、jmap -head +当前线程 例如:jmap -head 39120
*/
public class Demo08 {
public static void main(String[] args) throws InterruptedException{
System.out.println("开始");
Thread.sleep(30000);//睡眠30s,打命令
byte[] bytes = new byte[1024 * 1024 * 10];//10M
System.out.println("创建数组");
Thread.sleep(30000);
bytes = null;//清空对象
System.gc();//垃圾回收
System.out.println("结束");
Thread.sleep(1000000L);
}
}
输出:
1、jps jmap
- 命令
- 堆参数
- 占用情况:
- 可以观察堆内存的使用情况
2、jconsole工具
- 命令
- 选择连接
- 可视化界面
3.6.4、堆空间演变
对空间划分
版本 | 变化 |
---|---|
jdk1.7及之前 | 新生代(年轻代)老年代 永久代 |
jdk1.8及之后 | 新生代(年轻代)老年代 元空间(本地内存) |
3.7、方法区
3.7.1、什么是方法区
- Java方法区和堆一样,方法区是一块所有线程共享的内存区域
- 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中与Java堆区一样都是可以是不连续的。
- .方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展
3.7.2、方法区溢出
- 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:PermGen space(永久代)或者java.lang.OutOfMemoryError:Metaspace(元空间),比如加载了大量的第三方的jar包或者tomcat部署的工程过多,都可能导致方法区溢出,出现java.lang.OutOfMemoryError错误。
3.7.3、jdk8和之前的区别
1、方法区的演变
版本 | 变化 |
---|---|
jdk1.6及之前 | 有永久代(Permanent generation),静态变量存放在永久代 |
jdk1.7 | 字符串常量池、静态变量移出永久代,存放在堆中 |
jdk1.8及之后 | 去除了永久代,本地内存的元空间(Metaspace)取代 |
对比图:
2、jdk1.7中字符串常量池StringTable为什么从永久代移到堆中
- 永久代的回收效率很低,只有full Gc才会触发,(老年代或永久代空间不足会触发full Gc)导致StringTable回收效率不高,开发中会有大量字符串被创建,放到堆里能够及时回收内存。
3、为什么去掉永久代
- 永久代在jvm中,合适的大小难以确定(元空间分配在本地内存,无需考虑大小)
- 对永久代调优很困难
3.7.4、常量池
代码
/**
* 二进制字节码(类的基本信息,常量池,类方法定义,虚拟机指令)
*/
public class Demo09 {
public static void main(String[] args) {
//梦开始的地方,又回到最初的起点
System.out.println("Hello World");
}
}
查看字节码文件
- 常量池
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OrnjfI43-1651405760875)(https://gitee.com/hyl199605251513/typora-img/raw/master/img/image-20220412212520222.png)]
- 编译后代码
- 在常量池中找 #2 发现对应的 #21 #22
- #21 对应 #28 System #22 对应#29 out
- 同理可以找到我们 Hello World在常量池中对应的编码
Code:
stack=2, locals=1, args_size=1
// 从常量池中符号地址为 #2 的地方,先获取静态变量System.out
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// 从常量池中符号地址为 #3 的地方加载常量 hello world
3: ldc #3 // String hello world
// 从常量池中符号地址为 #3 的地方获取要执行的方法描述,并执行方法输出hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// main方法返回
8: return
3.7.5、运行时常量池
1、什么是运行时常量池
- 当类的字节码被加载到内存中后,他的常量池信息就会集中放入到一块内存,这块内存就称为运行时常量池,并且把里面的符号地址变为真实地址
- 运行时常量池具有动态性,即运行期间也可以向常量池中添加新的常量
代码
/**
* 运行时常量池
*/
public class Demo10 {
public static void main(String[] args) {
Student student = new Student();
student.name="凌晨";//存放运行时常量池
}
}
class Student{
String name;
}
3.7.6、字符串常量池
1、StringTable
/**
* 串池
* StringTable ["a","b","ab"] hashtable结构,不能扩容
*/
public class Demo11 {
public static void main(String[] args) {
/*
常量池中的信息,都会被加载到运行时常量池,这是 a b ab 都是常量池中的符号,还没有成为字符串对象
懒惰:只有走到当前代码,才会变为字符串对象
*/
String s1 = "a";//把a变为字符串对象“a”
String s2 = "b";//把b变为字符串对象"b"
String s3 = "ab";//把ab变为字符串对象"ab"
//new StringBuilder().append("a").append("b").toString(); new String("ab")
String s4 = s1 + s2 ;
System.out.println(s3 == s4);
String s5 = "a" + "b";
//javac 在编译期间优化,在编译阶段已经确定结果,不可改变
System.out.println(s3==s5);
}
}
- s1 + s2 等同于 new StringBuilder().append(“a”).append(“b”).toString()
- 等同于 new String(“ab”) 但是字符串常量池不创建ab对象,因为StringBuilder的toString()中的new String 是调用的不同的构造器
- s5 = “a” + "b"在编译期间优化,在编译阶段已经确定结果,不可改变 ,相当于“ab”
2、intern
- jdk1.8主动将串池中还没有的字符串对象放入串池 ,有则不放,然后返回串池中的对象
- jdk1.6 有则不放入,没有则对象复制一份放入串池,然后返回串池中的对象
/**
* intern
*/
public class Demo12 {
public static void main(String[] args) {
String s1 = new String("a") + new String("b");//创建了几个对象
//将字符串对象放入串池,如果有就返回,没有就放入
String s2 = s1.intern();//串池中没有“ab”,会把s1放入串池,然后返回串池中的对象
System.out.println(s2=="ab");//true
System.out.println(s1=="ab");//true
}
}
/**
* intern
*/
public class Demo12 {
public static void main(String[] args) {
String s3 = "ab";
String s1 = new String("a") + new String("b");//创建了几个对象
//将字符串对象放入串池,如果有就返回,没有就放入
String s2 = s1.intern();//串池有“ab”,不会会把s1放入串池,会返回串池中的对象
System.out.println(s2==s3);//true
System.out.println(s1==s3);//false
}
}
于“ab”
2、intern
- jdk1.8主动将串池中还没有的字符串对象放入串池 ,有则不放,然后返回串池中的对象
- jdk1.6 有则不放入,没有则对象复制一份放入串池,然后返回串池中的对象
/**
* intern
*/
public class Demo12 {
public static void main(String[] args) {
String s1 = new String("a") + new String("b");//创建了几个对象
//将字符串对象放入串池,如果有就返回,没有就放入
String s2 = s1.intern();//串池中没有“ab”,会把s1放入串池,然后返回串池中的对象
System.out.println(s2=="ab");//true
System.out.println(s1=="ab");//true
}
}
/**
* intern
*/
public class Demo12 {
public static void main(String[] args) {
String s3 = "ab";
String s1 = new String("a") + new String("b");//创建了几个对象
//将字符串对象放入串池,如果有就返回,没有就放入
String s2 = s1.intern();//串池有“ab”,不会会把s1放入串池,会返回串池中的对象
System.out.println(s2==s3);//true
System.out.println(s1==s3);//false
}
}
凌晨三点不下班: 环境配置不用刻意的记忆,只要理解为什么需要配置就可以了
凌晨三点不下班: 谢谢你的肯定,祝你工作顺利
Boge_Xie: 不好意思哈,这里没错,是path不对,win10识别不了
Boge_Xie: 引用「变量值:.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar;」 这里最后面不用加分号吧
疯癫小呼: 很多教程只说了要修改环境变量,但是我不知道为什么要弄这个啊感谢博主解答了我的疑惑