java虚拟机和virtual_Java虚拟机

本文详细介绍了Java虚拟机JVM的特性,包括平台无关性、内存区域(栈、堆、方法区等)以及垃圾回收机制。讲解了类加载器的工作原理、双亲委派模型、JVM调优和内存溢出问题。此外,还讨论了JVM如何处理本地方法栈、程序计数器以及方法区中的常量池。
摘要由CSDN通过智能技术生成

关于JVM(Java虚拟机) Java Virtual Machine

JVM特性

平台无关性

Java虚拟机是实现Java平台无关性的关键,引入JVM后,Java语言在不同平台上运行时不需要重新编译。JVM在执行字节码时,把字节码解释成具体平台上的机器指令执行。

JVM的位置

4a2e900aec664fc5a46568a84fbbd7cd.png

JVM的体系结构

f89453124a65a2ec3cf063c538b9360b.png

栈、本地方法栈、程序计数器肯定不会有垃圾回收

程序计数器

唯一一个在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(); //名字在栈里面,引用的对象在堆里面

063904c499f7a2cee737e2cb2dec585b.png

类加载器的加载顺序

启动类(根)加载器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,只有当父类加载器反馈无法完成这个类的加载请求时,子类加载器才尝试加载。

294fedafc2bbbae437c1e891afab8a1e.png

在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();

}

}

3bd211356274f584e36f063c4156ef5b.png

系统启动一个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部署了太多的应用

大量动态生成的反射类不断被加载

0fd8175417c6c6ae671435d750db99a9.png

元空间是堆里面一个特殊的部分,为了和堆区分开,也叫非堆

方法区是元空间的一小部分,常量池是方法区中的一部分

元空间:逻辑上存在,物理上不存在

Q:遇到OOM怎么办

OOM是堆内存满了

尝试把堆内存扩大看结果

-Xms1024m -Xmx1024m -XX:+PrintGCDetails

如果还满,那就是有垃圾代码或者死循环代码

GC:垃圾回收

作用区域:只有堆和方法区

JVM在进行GC时,并不是对所有区域统一回收,大部分时候回收都是新生代

轻GC:伊甸园区,幸存区

重GC:老年区

判断无用的类要符合以下三个条件

该类的所有实例都已经回收,Java堆中不存在该类的任何实例

加载该类的ClassLoader已经被回收

该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法

GC算法:引用计数法

34e078c947b537429d44c0b762d210c7.png

计数器本身也有消耗

对象多的话,方法并不高效,并且不能解决循环引用的问题

所以用的比较少

GC算法:复制算法

幸存区0区和1区由于经常互换位置,也叫from区和to区,谁空谁是to

新生区主要用复制算法

从其中一个幸存区复制到另一个幸存区,保证有一个幸存区是空的

Eden区活下来的也复制到幸存区to区

234d37b710095e5e0d0af55176d1e0fc.png

好处:没有内存的碎片

坏处:浪费内存空间(有一半空间永远是空的)

复制算法最佳使用场景:对象存活度较低的地方,即新生区

GC算法:标记清除算法

1d0d98a2e27d9be5962bb58f0c1c91ee.png

缺点:两次扫描,浪费时间,会产生内存碎片

优点:不需要额外的空间

GC算法:标记压缩

再优化:多了一次扫描,多了一个移动成本,但是减少了内存碎片

6e4bb188ff598be15e9958d13b001217.png

在标记清除压缩方法基础上调优:

先多几次标记清除,然后再进行压缩

GC算法总结:

内存效率:复制算法 > 标记清除算法 > 标记压缩算法 (时间复杂度)

内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法

内存利用率:标记压缩算法 = 标记清除算法 > 复制算法

Q:有没有最优算法?

A:没有最好的算法,只有最合适的算法----->

GC:分代收集算法

年轻代:

存活率低

复制算法

老年代:

区域大

存活率高

标记清除+标记压缩混合实现 (JVM调优:清多少次再压缩,内存碎片到一定量再压缩)

关于JVM调优

线程共享数据区大小= 新生代大小+ 老年代大小+ 持久代大小(一般固定为64M)

如果老年代过小,会增多Full GC,推荐为Java堆的3/8

推荐Eden : Survivor0 : Survivor1 = 8 : 1 : 1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值