一、jvm体系结构:
二、.class文件:
这个众所周知,这里就不废话了,.class文件就是javac编译之后产生的文件
三、类装载器- - - classloader:
1、作用:ClassLoader只负责class文件的加载,至于它是否可以运行,怎么运行,则由Execution Engine决定
2、分类:虚拟机自带的加载器、用户自定义的加载器
3、虚拟机自带的加载器:
分类:
- 启动类(bootstrap)加载器 由c++写的
- 扩展类(extension)加载器 由java写的
- 应用程序(app)加载器,也叫系统类加载器,加载当前应用的classpath的所有类 由java写的
代码实现:
1、写一个类(比如student),然后打成jar包,将这个jar包放入jdk的jre的lib的ext包中即可
2、然后进行代码实验:
Object o = new Object();
System.out.println(o.getClass().getClassLoader());
System.out.println("======================================");
Class clazz = Class.forName("Myjvm.MyStudent");
System.out.println(clazz.getClassLoader());
System.out.println("======================================");
ClassloaderTest classloaderTest = new ClassloaderTest();
System.out.println(classloaderTest.getClass().getClassLoader());
运行结果如下:
解释说明:
- 为什么第一个object的类加载器是null呢,因为object是java自带的类,java自带的类在加载的时候调用的是bootstrap类加载器,而只要你用的是bootstrap这个根加载器去加载的类,返回的加载器类型都是null
- 第二个是ExtClassLoader,只要是自己写的,不是java自带的,然后打成一个jar包的形式放到jdk/jre/lib/ext的目录下的类,调用的都是java扩展类加载器
- 第三个是AppClassLoader,只要是在classpath下的所有类都是调用的是应用程序加载器
三者关系: 其中sum.misc.lancher是一个java虚拟机的入口应用
结果:
- 启动类(bootstrap)加载器是扩展类(extension)加载器的父类
- 扩展类(extension)加载器是 应用程序(app)加载器的父类
类加载器的双亲委派机制:
即:一个类在被加载的时候它会先去委托父类加载器,如果父类加载器可以完成加载,那么就成功加载,如果父类加载器无法完成加载,才轮到自己去加载。
目的:主要是为了保证java的一种安全机制,叫做沙箱机制
沙箱机制:
我相信大家一定在初学java的时候自己定义过类似于String这样的类,虽然编译的时候不报错,但是运行时就是报错(在idea中索性都没有了运行的那按钮,eclipse还能运行)。。。
当时的你一定是一脸懵逼,这是因为双亲委派机制使得java在加载String这个类的时候默认加载的是java自带的String类,由bootstrap加载器进行加载,不会去加载我写的String类,这样一来就保证了java源代码的安全。我们再找到java自带的String这个类,打开rt这个jar包(注意,java自带的类基本都在rt这个jar中)
我们可以看到反编译出来的java自带的String类中并没有main方法
四、执行引擎:
在java中javac负责编译,java负责执行,执行引擎负(Execution Engine)责解释命令,提交操作系统执行。
五、JNI:
Java Native Interface:作用是融合不同的编程语言为Java所用,它的初衷是融合 C/C++程序
代码中的例子:我们可以打开我们最熟悉的一个类(Object)的源码
像这种都是本地方法,被native修饰的方法,只有方法的声明,没有方法体,说明java代码到此为止,后面就开始调底层操作系统,或者是其他函数库,它的具体做法是Native Method Stack中登记native方法,在Execution Engine执行时加载本地方法库。
注意:我们平时说的栈是java栈,我们的main等java方法放入的是java栈中,但是native方法放入的是native method stack中。
六、pc寄存器:
思考:有没有人想过,为什么程序能按逻辑执行下来,不出错?那是因为有寄存器的存在。
内容:每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
七、栈(Stack):
概要:栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就结束,生命周期和线程一致,是线程私有的。
经典错误:
Exception in thread “main” java.lang.StackOverflowError
分析说明:main方法进来会先放入栈里面,然后调用hello这个方法,hello这个方法又去调本身,然后就产生了死循环,递归调用。然后栈就被塞满了。
存储内容:基本类型,引用变量和我们写的实例方法(如main方法等,值得注意的是:栈的最底层是main方法)
八、方法区:
注意:在java7之前,方法区就是永久代
内容:方法区是线程共享的,通常用来保存装载的类的元结构信息。比如:运行时常量池+静态变量+常量+字段+方法字节码+在类/实例/接口初始化用到的特殊方法等。我们在new一个对象的时候需要有相同的属性,这些东西就去方法区拿,方法区就是存储这些东西的。
九、堆(Heap):
说明:一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息。
堆结构:
- 逻辑上分为三块:新生区+养老区+永久区(java8之后是元空间)
- 实际上堆内存只有两块,新生区和养老区,永久区又称为非堆内存
说明: 每次刚new出来的对象都会在新生区的伊甸区,但是伊甸区空间是有限的,当对象越来越多,就慢慢的进行轻量的gc,这个时候将会有一部分对象被清除,
幸存下来的将会去幸存0区,如果说又不停的new对象,幸存0区也满了,那就去幸存者1区,以此类推,如果幸存者1区也满了就去养老区,据说一般需要幸存15次左右才能到养老区,如果养老区也满了就回触发重量级的gc,清除对象来释放内存。
注意: 幸存者0区和1区也称为from区和to区,0区和1区是会来回交换的,即0区轻量gc后将幸存者移入1区,然后0区重新获取一片空的空间,变成幸存者1区,原来的1区将会变成0区
经典错误:内存溢出- - - OOM
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
public static void main(String[] s) {
String str = "mzd";
while (true) {
str += str;
}
}
设置jvm内存参数,并打印gc日志(现在配置内存为2M)
运行结果如图所示:
注意:
- -xms:表示jvm初始内存 默认是计算机物理内存的1/64
- -xmx:表示jvm最大内存 默认是计算机物理内存的1/4
- -XX:+PrintGCDetails:表示打印详细gc日志
public static void main(String[] s) {
// 最大内存
long maxmemory = Runtime.getRuntime().maxMemory();
// 当前总内存---返回的是java虚拟机现在已经从操作系统那里挖过来的内存大小,
// 也就是java虚拟机这个进程当时所占用的所有 内存
long totalmemoty = Runtime.getRuntime().totalMemory();
//java虚拟机内存100%的情况下是会稍微多获取操作系统一点的内存,但是又没有用上的内存,实际上就是 freeMemory()
long freememory = Runtime.getRuntime().freeMemory();
System.out.println(maxmemory / (double) 1024 / 1024 + "M");
System.out.println(totalmemoty / (double) 1024 / 1024 + "M");
System.out.println(freememory / (double) 1024 / 1024 + "M");
}
十、OOM调优之MAT:
研究中。。。敬请期待!!!