关于JVM(Java虚拟机) Java Virtual Machine
JVM特性
平台无关性
Java虚拟机是实现Java平台无关性的关键,引入JVM后,Java语言在不同平台上运行时不需要重新编译。JVM在执行字节码时,把字节码解释成具体平台上的机器指令执行。
JVM的位置
JVM的体系结构
栈、本地方法栈、程序计数器肯定不会有垃圾回收
程序计数器
唯一一个在JVM中没有任何OOM情况的区域
非常小的内存空间
每个线程都是有一个程序计数器的,是线程私有的,相当于一个指针
小总结
Java文件经过编译后变成.class字节码文件
字节码文件通过类加载器被搬运到JVM中
JVM主要的五部分:
方法区,堆为线程共享区域,有线程安全问题
栈、本地方法栈、程序计数器是独享区域,不存在线程安全问题
JVM调优99%是在调方法区和堆,大部分是堆
JVM、JRE、JDK三者关系
JDK是常用的开发包,用于编译和调试Java程序的
JRE是Java运行环境,写好的程序必须在JRE才能运行
JVM是虚拟机,将字节码解释成为特定的机器码进行运行(需要先编译为.class文件,否则JVM不认识)
类加载器(类装载器)ClassLoader
作用:加载Class文件,将编译后的.class文件加载内存当中
类加载器只负责class文件的加载,是否能够运行则由Execution Engine来决定
Car car1 = new Car(); //名字在栈里面,引用的对象在堆里面
类加载器的加载顺序
启动类(根)加载器Bootstrap classLoader 由C/C++实现,主要负责加载核心的类库 rt.jar
扩展类加载器Extension ClassLoader 主要负责加载jre/lib/ext目录下的一些扩展的jar
应用程序加载器Application ClassLoader 主要负责加载应用程序的主函数类
自定义的类加载器Custom ClassLoader
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());//11632....
System.out.println(car2.hashCode());//195672...
System.out.println(car3.hashCode());//356573...
Class extends Car> aClass1 = car1.getClass();
Class extends Car> aClass2 = car1.getClass();
Class extends Car> aClass3 = car1.getClass();
System.out.println(aClass1.hashCode());//460141958
System.out.println(aClass2.hashCode());//460141958
System.out.println(aClass3.hashCode());//460141958
ClassLoader classLoader = aClass1.getClassLoader();
System.out.println(classLoader);//AppClassLoader
System.out.println(classLoader.getParent());//ExtClassLoader /jre/lib/ext
System.out.println(classLoader.getParent().getParent());//null 1.不存在 2.Java程序获取不到(底层是C或者C++写的) rt.jar
/*
为什么会有调用native方法?
因为JVM的下一层是操作系统,JVM无法获取,所以要调用native方法
*/
}
}
package java.lang;
public class String{//与根加载器的类同名、同方法
public String toString(){
return "Hello";
}
public static void main(String[] args){
String s = new String();
s.toString();
}
}
//运行会报错说String方法里面没有main方法
//原因如下:
/*
双亲委派机制:安全
运行时:APP-->EXT-->BOOT(rt.jar)
寻找该类,BOOT中有String方法,所以运行BOOT中的而不是上面写的
如果没有,则一层一层向下寻找
BOOT-->EXT-->APP的顺序
1. 类加载器收到类加载的请求
2. 将这个请求向上委托给父类加载器,一直向上,直到启动类加载器
3. 启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器
否则,抛出异常,通知子加载器进行加载 Class Not Found~
4. 重复步骤3
*/
双亲委派机制
优点:避免重复加载+避免核心类被篡改
当一个类加载器收到一个类加载的请求,会先将该请求委派给父类加载器去加载,直到Bootstrap ClassLoader,只有当父类加载器反馈无法完成这个类的加载请求时,子类加载器才尝试加载。
在JVM中判断两个类是否是同一个类取决于类加载器和类本身,类加载器不同,那么加载的两个类一定不相同。
沙箱安全机制
Java安全模型的核心就是Java沙箱sandbox。沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。
运行时数据区
本地方法栈
native:凡是带了native关键字的,说明java的作用范围达不到了,回去调用底层C语言的库
会进入本地方法栈,调用本地方法本地接口(JNI:Java native interface)
JNI的作用:扩展Java的作用,融合不同的编程语言为Java所用,因为底层是C、C++写的
在内存区域中专门开辟了一块标记区域:Native Method Stack,登记native方法,在最终执行的时候,通过JNI加载本地方法库中的方法
程序计数器 Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),再执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计
如果执行的是native方法,那这个指针就不工作了
方法区Method Area
方法区被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享空间。
静态变量static、常量final、类信息Class(构造方法、接口定义)、运行时的常量池存在方法区中
BUT,实例变量存在堆内存中,和方法区无关
创建对象内存分析👇
public class Pet{
public String name; //name:null
public int age;//age:0
public void shout(){
System.out.println("shout at you!");
}
}
public class Application{
public static void main(String[] args){
Pet dog = new Pet();
dog.name = "Jack";
dog.age = 3;
dog.shout();
Pet cat = new Pet();
}
}
系统启动一个JVM进程,找到.class的二进制文件,将类信息加载到运行时数据区的方法区内
JVM找到主程序入口,执行main方法
创建一个Pet对象,但是方法区没有Pet类的信息,JVM此时加载Pet类,把信息放到方法区中
在堆中为一个新的Pet实例分配内存,然后初始化Pet实例,此时实例持有指向方法区中的Pet类的类型信息的引用
根据引用找到dog对象,根据dog对象持有的引用定位到方法区中Pet类的类型信息的方法表,获得方法的字节码地址
执行shout()方法
对象实例初始化时会去方法区中找类信息,完成后再到栈那里去运行方法
栈
栈也叫栈内存,负责Java程序的运行,是在线程创建时创建的,生命周期和线程的生命周期一致,同时消亡,线程结束了栈就释放了,所以栈不存在垃圾回收
栈管运行,堆管存储
为什么main()最先运行,最后结束?
栈先进后出
递归为什么可能出现栈溢出?
public class Test{
public void a(){
b();
}
public void b(){
a();
}
}
//在栈里面,a()先压入栈,a中调用b,b()被压入栈,b调用a,a()被压入栈。。。
//StackOverFlowError!
栈内存,主管程序的运行,生命周期和线程同步
线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收
8大基本类型+对象引用+实例的方法都在栈中分配内存
栈运行原理:栈帧(每执行一个方法,就产生一个栈帧)
程序正在执行的方法,一定在栈的顶部
栈+堆+方法区的交互关系
堆Heap
一个JVM只有一个堆内存,堆内存的大小是可以调节的,堆被整个JVM的所有线程共享
JVM内存划分为堆内存(年轻代Eden,Survivor和老年代)和非堆内存(永久代)
非堆内存就是方法区
JDK1.8中已经移除永久代,替代品是元空间MetaSpace,metaSpace是不存在于JVM中的,使用的是本地内存
类加载器读取了类文件后,一般会把什么东西放入堆中?
类、方法、常量、变量、保存所有引用类型的真实对象
堆内存还要细分为三个区域:
新生代Young/New (伊甸园区+幸存区0区+幸存区1区)
老年代Old
永生代Perm
GC垃圾回收,主要是在伊甸园区(Eden Space)和老年代
伊甸园:轻GC
老年代:重GC(Full GC)
OOM:OutOfMemory是堆内存满了
新生代
对象诞生和成长的地方,甚至死亡
伊甸园区:所有的对象都是在伊甸园区new出来的
幸存区(0,1区):伊甸园区满了就会触发轻GC,还持有引用的对象存活下来,进入幸存0区
幸存0区满了触发Minor GC,将幸存对象移动到1区,并将from和to的指针交换
当一个对象经历了15次(默认值,可以改,最大为15)GC还没有死,就会进入老年区
如果老年区也满了,就会触发重GC
老年代在Full GC之后还无法进行操作,就是OOM了
永久代
这个区域常驻内存的,用来存放JDK自身携带的Class对象,Interface元数据,java运行时的一些环境
不存在垃圾回收!关闭虚拟机就会释放这个区域的内存
jdk1.6之前:永久代,常量池在方法区中
jdk1.7 :永久代,逐渐退化,去永久代,常量池在堆中
Jdk1.8之后:无永久代,常量池在元空间中
什么情况会在永久区出现OOM?
一个启动类加载了大量的第三方jar包
Tomcat部署了太多的应用
大量动态生成的反射类不断被加载
元空间是堆里面一个特殊的部分,为了和堆区分开,也叫非堆
方法区是元空间的一小部分,常量池是方法区中的一部分
元空间:逻辑上存在,物理上不存在
Q:遇到OOM怎么办
OOM是堆内存满了
尝试把堆内存扩大看结果
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
如果还满,那就是有垃圾代码或者死循环代码
GC:垃圾回收
作用区域:只有堆和方法区
JVM在进行GC时,并不是对所有区域统一回收,大部分时候回收都是新生代
轻GC:伊甸园区,幸存区
重GC:老年区
判断无用的类要符合以下三个条件
该类的所有实例都已经回收,Java堆中不存在该类的任何实例
加载该类的ClassLoader已经被回收
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法
GC算法:引用计数法
计数器本身也有消耗
对象多的话,方法并不高效,并且不能解决循环引用的问题
所以用的比较少
GC算法:复制算法
幸存区0区和1区由于经常互换位置,也叫from区和to区,谁空谁是to
新生区主要用复制算法
从其中一个幸存区复制到另一个幸存区,保证有一个幸存区是空的
Eden区活下来的也复制到幸存区to区
好处:没有内存的碎片
坏处:浪费内存空间(有一半空间永远是空的)
复制算法最佳使用场景:对象存活度较低的地方,即新生区
GC算法:标记清除算法
缺点:两次扫描,浪费时间,会产生内存碎片
优点:不需要额外的空间
GC算法:标记压缩
再优化:多了一次扫描,多了一个移动成本,但是减少了内存碎片
在标记清除压缩方法基础上调优:
先多几次标记清除,然后再进行压缩
GC算法总结:
内存效率:复制算法 > 标记清除算法 > 标记压缩算法 (时间复杂度)
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法
Q:有没有最优算法?
A:没有最好的算法,只有最合适的算法----->
GC:分代收集算法
年轻代:
存活率低
复制算法
老年代:
区域大
存活率高
标记清除+标记压缩混合实现 (JVM调优:清多少次再压缩,内存碎片到一定量再压缩)
关于JVM调优
线程共享数据区大小= 新生代大小+ 老年代大小+ 持久代大小(一般固定为64M)
如果老年代过小,会增多Full GC,推荐为Java堆的3/8
推荐Eden : Survivor0 : Survivor1 = 8 : 1 : 1