JVM的结构
详细的JVM架构
3、类加载器
作用:加载class文件
层级关系:应用程序加载器–>扩展类加载器–>启动类(根)加载器–>虚拟机自带的加载器
我们可以通过对Java中的架包中进行修改,达到公司的一些目的。这是可以做到的。
双亲委派机制
使用代码查看,上图中的关系。
public class Car {
public static void main(String[] args) {
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
System.out.println(car1.hashCode());
System.out.println(car2.hashCode());
System.out.println(car3.hashCode());
Class<? extends Car> aClass1 = car1.getClass();
Class<? extends Car> aClass2 = car2.getClass();
Class<? extends Car> aClass3 = car3.getClass();
System.out.println(aClass1.hashCode());
System.out.println(aClass2.hashCode());
System.out.println(aClass3.hashCode());
ClassLoader classLoader = aClass1.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
System.out.println(classLoader.getParent().getParent());
}
}
结果:
1067040082
1325547227
980546781
603742814
603742814
603742814
jdk.internal.loader.ClassLoaders$AppClassLoader@15db9742
jdk.internal.loader.ClassLoaders$PlatformClassLoader@7adf9f5f
null
个人理解:(理解还不透彻,可能有一些错误。不过感觉方向大致是对的)
我们定义好一个类时,通过编译器变为.class文件,该文件需要classloader才能装入虚拟机中进行运行。但是此时,双亲委派机制要检查该文件中的类时候已经被加载过。并且从最外层,也就是Appclassloader开始验证,这个类是否被加载过。依次递交到BootStrapClassLoader,如果以上的任意一环已经加载过,那么不再向上委派。如果委派到最上层依旧没被加载过,那么就就地开始加载。又开始依次向下去验证是否自己可以记载此类。
这么做的好处是,避免系统级别的原生代码被修改。举个例子,我们可以自定义一个String类,在加载这个类的时候,我们就开始向上委派,直到BootstrapclassLoader,发现这个类被BootstrapclassLoader加载过了,那么我们就直接使用这个被加载过的类进行实例化。就不会再走我们自定义的String类的中方法逻辑(表面上我们依旧可以定义String类,但是实际上底层的String已经被加载)。这样避免了修改底层代码。保证了一定的安全。
步骤总结
1、类加载器收到类加载的请求!
2、将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器!
3、启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常。通知子加载器进行加载。
4、重复步骤3.
有趣的记录:java当初叫C++ - -,哈哈,去掉了指针。自动化了回收机制。
有趣的记录:Java中的native关键字,调用的操作系统级别的本地方法,一般是由C/C++写的
沙箱安全机制
nativa, 方法区
native:凡是带了native关键字的,说明Java的作用范围达不到了,会去调用底层C语言的库!!
注意事项:
1、会进入本地方法栈
2、待用本地方法本地接口 JNI
3、JNI的作用,扩展Java的使用,融合不同的编程语言为Java所用。比如最初的C,C++
4、Java诞生的时候,C,C++横行,要想立足,必须调用C,C++的程序
5、它在内存区域中,专门开辟了一个标记区域:native Method Stack,登记native 方法
6、在最终执行的时候,加载本地方法库中的方法通过JNI
7、Java程序驱动打印机,管理系统,掌握即可。在企业级应用中较为少见。
PC寄存器
程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向像一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
方法区(Method Area)
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关(主要包含:static,final,Class模板,常量池)
举个例子:
当我们创建一个类的时候,会在方法区中加载类的信息。此时如果对其属性进行了字面量的赋值初始化,其对应的值就会在方法区中的常量池中进行存储。一旦,实例化对象后,进行了重新的赋值。那么属性的值就会存储在堆空间中。
栈(一种数据结构)
程序=数据结构+算法:持续学习;
程序=框架+业务逻辑:吃饭~;
情景
我们的程序运行,我们的main()总是最先执行,最后结束。其实就是栈的应用,系统先将main()方法压入空栈,之后依旧压入其他的方法。等其他方法运行完后,弹出栈,main方法才被弹出。其中经常遇到“栈溢出”的问题,这可能就是方法栈被错误程序,混乱调用方法将栈压满了。
栈:栈内存,主管程序的运行,生命周期与线程同步;线程结束,栈内存也就释放。
对于栈来说,不存在垃圾回收问题。 一旦线程结束,栈就over。
栈存储:8大基本类型+对象的引用+实力的方法
栈运行的原理: 在栈结构的每一个“栈帧”中的结构:
程序正在执行的方法,一定在栈的顶部
堆(heap)
注意,不同的JVM中,堆的结构是不一样的。但是我们平时学习,使用的都是sun公司的。
SUN公司:Java HotSpot™ 64-Bit Server VM (build 13+33, mixed mode, sharing)
BEA : JRockit
IBM : J9 VM
一个JVM只有一个堆内存,堆内存的大小是可以调节的。
类加载器读了类文件后,一般会把什么东西放到堆中?类,方法,常量,变量等,保存我们所有引用类型的真实对象;
堆内存中还要细分为三个区域:
- 新生区
- 养老区
- 永久区
GC垃圾回收,主要是在新生区(伊甸园区)和养老区
假设内存满了,就会报OOM(java.lang.OutOfMemoryError:java heap space),堆内存不够~~
在JDK8以后,永久存储区改了个名字(原空间)
新生区
- 类:诞生和成长的地方,甚至是死亡;
真理:经过研究,99%的对象都是临时对象!
永久区
这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或者类信息,这个区域不存在垃圾回收!关闭VM虚拟机就会释放这个区域的内存!
一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,就会出现OOM;
- jdk1.6之前:永久代(永久存储区),常量池在方法区中;
- jdk1.7:永久代,但是慢慢退化了,去永久代,常量池在堆中;
- jdk1.8之后:无永久代,常量池在元空间。方法区也在持久代中,也就是永久存储区。
图解:
持久代(元空间在堆空间中),但是由于其方法去又被很多新生对象所共享,所以又叫非堆。
再细分:
注意:这个方法区在逻辑上存在,但是在物理上不存在。(因为新生代和老年代的空间加起来就已经等于了堆空间总和,所以这个元空间事实上又不在堆空间中。)
堆内存调优的问题:
假如以后报JVM的OOM问题,
-我们可以先通过编辑器将JVM的堆内存扩大看看,如果还报OOM问题,那么就应该是我们的代码的问题。
-分析内存,看一下哪个地方出现了问题。(使用专业工具)
(1)能够看到代码第几行出错:使用内存快照分析工具,MAT(Eclipse早年集成好的插件),Jprofiler(测试插件)
(2)Debug,一行行分析代码!
MAT,Jprofiler的作用:
1、分析Dump内存文件,快速定位内存泄漏;
2、获得堆中的数据;
3、获得大的对象;
。。。。。。等等
Jprofiler安装
首先我们需要在idea中安装我们的Jprofiler插件,setting–>plugins–>搜索Jprofiler,如下图:
记得重启idea~
之后工具栏就会出现该图标。此时还需要安装软件与该插件进行配合使用!
安装后,我们还需要去Jprofiler官网下载该软件:(直接百度即可)
下载后,傻瓜式下一步安装。
由于Jprofiler12的注册码没有找到,破解也不方便。所以先用免费的测试10天,用于体验,怎么排除OOM的错误。
下一步,将IDEA与Jprofile进行绑定
安装步骤,也可根据狂神的网站进行安装:
链接: https://www.kuangstudy.com/bbs/1369899213336367105.
很详细!!以及简单的使用!
测试代码:
package com.chaoxi.java;
import java.util.ArrayList;
/**
* @author chaoxi
* @create 2021-08-11 10:34
* 无限的在动态数组中添加对象,肯定会报OOM错误~!!!!!
*/
public class Demo03 {
public static void main(String[] args) {
ArrayList<Demo03> list = new ArrayList<>();
int count = 0;
try {
while (true){
list.add(new Demo03());
count++;
}
} catch (Exception e) {
System.out.println("count"+count);
e.printStackTrace();
}
}
}
这种就是简单的代码不报错,但是就是错了的典型。
接下来,我们对JVM进行配置。让其,DUMP下对应的错误信息。
配置指令:-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
-Xms:设置初始化内存分配大小
-Xmx:设置最大分配内存
-XX:PrintGCDetails :打印GC的一些计数信息,垃圾回收信息
再次运行,就可以看到dump下的文件。
找到,在项目根目录下,dump的文件。双击打开。
即可看到,Jprofiler对错误的分析。
GC(垃圾回收)
JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收都是在新生代。
- 新生代
- 幸存区(from,to)
- 老年区
GC两种:轻GC(普通的GC),重GC(全局GC)
引用计数法
会给每个对象分配计数器,计数器本身还会占用内存。所以很消耗内存。Java一般不采用这种方式。
复制算法
过程:首先to区永远是空的。当一个对象在新生代中没有被GC,
那么它就会被移动到空的to区,同时from区也会把幸存对象复制到to区。
此时,新生代区就被清空,原来的from区就会变为to区,
原来的to区会变为from区。当一个对象存活超过我们设置的门限时,就会被送到养老区。
好处:没有内存的碎片!
唯一的坏处:浪费了内存空间,因为to区永远是空的。
但是,复制算法最佳使用场景:对象存活度较低的时候,适用于新生区。假如对象100%存活(极端情况),频繁的复制移动也会影响效率。
标记清除算法
理解:这样做的好处是不像复制算法一样,会浪费一些空间。我们只需要两次扫描。第一次标记存活对象,第二次进行清除即可。但是扫描很耗时间。
优点:不需要额外的空间!
缺点:两次扫描,严重浪费时间,会产生内存碎片(内存碎片即:使得存储对象非常分散)。
标记清除压缩算法
理解:为了防止内存碎片的产生,我们对清除后的内存再次进行扫描。将存活对象进行一端移动。
可优化方案:理论上可以先进行几次标记清除之后,再进行压缩。可以节约一些时间。
总结
GC—>分代收集算法
没有最好的算法,只有最合适的算法。
年轻代:
- 存活率低
- 使用复制算法较优
老年代:
- 区域大,存活率高
- 标记清除+标记清除压缩混合实现
补充:JMM=Java Memory Model(Java内存模型)