我们首先要明白,什么是java虚拟机(JVM)?
Java虚拟机是一个可以执行Java字节码的虚拟机进程。因为Java源文件都会被编译器编译成.class的字节码文件,经过类加载器加载到虚拟机才能执行。
我们注意一下三者的区别:
- JVM内存结构,和Java虚拟机的运行时区域有关(JVM内存分区)。
- Java内存模型,和Java的并发编程有关。
- Java对象结构,和Java对象在虚拟机中的表现形式有关。
JVM内存结构:
java内存通常被划分为5个区域:程序计数器(Program Count Register)、本地方法栈(Native Stack)、方法区(Methon Area)、栈(Stack)、堆(Heap)。
程序计数器是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器(偏移地址),Java编译过程中产生的字节码有点类似编译原理的指令,程序计数器的内存空间存储的是当前执行的字节码的偏移地址,每一个线程都有一个独立的程序计数器(程序计数器的内存空间是线程私有的),因为当执行语句时,改变的是程序计数器的内存空间,因此它不会发生内存溢出 ,并且程序计数器是jvm虚拟机规范中唯一一个没有规定 OutOfMemoryError 异常 的区域;
栈:线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。 没有类信息,类信息是在方法区中
堆:对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。
方法区:属于共享内存区域,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
问:Java堆是java虚拟机所管理的内存中最大的一块,每个线程都拥有一块内存区域,所有的对象实例以及数组都在这里分配内存 对还是错?
答: 错误,Java堆确实是java虚拟机所管理的内存中最大的一块,但是堆是线程共享的,并不是每个线程都拥有一块内存区域。
问:Java虚拟机栈描述的是java方法执行的内存模型,每个方法被执行的时候都会创建一个栈帧,用于存储局部变量表、类信息、动态链接等信息
答: 错误,栈中没有类信息,类信息在方法区中。
函数的返回值保存在哪里?
调用函数时,函数的返回值存放的位置具体在哪里呢?按照概念来说,函数的返回值应该放在被调用函数运行结束之后,主调函数可以有效访问的地方,也就是说,函数返回值应当存放在主调函数开辟的栈空间
但是,计算机硬件的飞速发展使得CPU的通用寄存器字长在不断增长,个数也不断增多,因此在很多情况下,函数的返回值直接被存放在了CPU的通用寄存器中,而并非一定需要计算机的内存空间。
具体类型的返回值的存放位置如下所示:
- char(8bit):寄存器a1
- short(16bit):寄存器ax int(32bit):寄存器eax
- double(64bit):协处理器堆栈 指针、引用:寄存器eax 。
- 类的对象且体积超过64bit:主调函数会在函数栈上创建临时对象存放。
具体来说:
- 1、结构体大小不超过4字节,那么仍然使用EAX寄存器传递返回值。
- 2.结构体超过4字节但不等于8字节时,调用者将首先在栈上分配一块能容纳结构体的临时内存块,然后在传递完函数参数后将该临时内存块的首地址作为隐含的第一个参数最后(因为压栈顺序是从右到左)压栈,接下的动作同前所述。当被调用函数返回时,它会通过第一个隐含参数寻址到临时内存块并将返回值拷贝到其中,然后将保存有返回值内容的临时内存块的首址存进eax寄存器中
- 3、结构体大小刚好为8个字节时编译器不再于栈上分配内存,而直接同时使用EAX和EDX两个寄存器传递返回值,其中EAX保存低4字节数据,EDX保存高4字节数据。
Java内存模型:
Java内存模型(简称JMM)决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。JMM是围绕原子性、有序性、可见性展开。
JMM 与 Java 内存区域唯一相似点,都存在共享数据区域和私有数据区域,在 JMM 中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。
那 jvm如何实现线程?
1.1 使用内核线程实现
内核线程(Kernel-Level Thread, KLT)就是直接由操作系统内核支持的线程。
- 内核来完成线程切换
- 内核通过调度器Scheduler调度线程,并将线程的任务映射到各个CPU上
- 程序使用内核线程的高级接口,轻量级进程(Light Weight Process,LWP)–>(!!!名字是进程,实际是线程)
- 用户态和内核态切换消耗内核资源
1.2 使用用户线程实现
- 系统内核不能感知线程存在的实现
- 用户线程的建立、同步、销毁和调度完全在用户态中完成
- 所有线程操作需要用户程序自己处理,复杂度高
1.3 用户线程加轻量级进程混合实现
- 轻量级进程作为用户线程和内核线程之间的桥梁,降低整个线程被完全阻塞的风险
一、类加载器和双亲委派机制
我们首要知道JVM加载class文件的原理。
我们在IDEA中编写的Java源代码被编译器编译成.class的字节码文件后,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显式的加载所需要的类。
类加载方式,有两种:
(1)隐式装载,程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中,
(2)显式装载,通过class.forname()等方法,显式加载需要的类,利用反射(显式装载)来创建实例时,可绕过jvm的权限检查机制。
要注意的是,Java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到 jvm中,至于其他类,则在需要的时候才加载。这样可以节省内存开销。
JVM中提供了三层的ClassLoader:
- AppClassLoader:主要负责加载用户路径(classpath)上的类库。
- ExtClassLoader:主要负责加载JAVA_HOME\lib\jre\ext 目录下的一些扩展的jar。
- Bootstrap classLoader:主要负责加载JAVA_HOME\lib目录中的(主要是在rt.jar 包),java.lang.*等).
JVM通过双亲委派模型进行类的加载,此外也可以通过继承java.lang.ClassLoader实现自定义的类加载器。
加载过程:
当一个Student.class这样的文件要被加载时。不考虑我们自定义类加载器时,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的ClassLoader方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
为什么要设计这种机制?
这种设计有个好处是,一定程度上保证了安全。如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被Bootstrap classLoader加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。
比如自定义一个在rt.jar包下的String类。当委托到顶层Bootstrap classLoader的时候,根加载器会检查是否可以执行。发现rt.jar包中已经有了String类了,就会加载里面的这个String类,而不会加载我们自己写的这个String类,所以会报找不到main方法的错误。
package java.lang;
public class String {
public String toString(){
return "hello";
}
public static void main(String[] args) {
String s = new String();
System.out.println(s.getClass().getClassLoader