jvm的传唱
- 什么是jvm
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算
机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的(很官方化) - jvm、jre、jdk的关系
JVM 是 Java 程序能够运行的核心。但是需要注意,JVM 自己什么也干不了(绝世好键);JRE(Java Runtime Environment)JVM 标准加上实现的一大堆基础类库,就组成了Java 的运行时环境(拜剑山装);对于 JDK 来说,就更庞大了一些。除了 JRE,JDK 还提供了一些非常好用的小工具,比如 javac、java、jar 等(不惊云)。 - Java虚拟机的内存相关
1.8之前大家喜闻乐见的结构
实际1.8以后的结构
大家要注意在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory),至于为什么要变化也是整理了下原因仅供参考
-
字符串存在永久代中,容易出现性能问题和内存溢出。
-
类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太
大则容易导致老年代溢出 -
永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
虚拟机栈是线程私有的,主要用户存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M -Xss1m -Xss1024k -Xss1048576
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。
操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接,动态链接的作用:将符号引用转换成直接引用。
方法返回地址存放调用该方法的PC寄存器的值。无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行
本地方法栈(Native Method Stacks) 与虚拟机栈所发挥的作用是非常相似的, 其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码) 服务, 而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。
在Java虚拟机规范中,对本地方法栈这块区域,与Java虚拟机栈一样,规定了两种类型的异常: (1)StackOverFlowError :线程请求的栈深度>所允许的深度。 (2)OutOfMemoryError:本地方法栈扩展时无法申请到足够的内存。
Java堆(Java Heap) 是虚拟机所管理的内存中最大的一块。 Java堆是被所 有线程共享的一块内存区域, 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例, Java 世界里“几乎”所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此很多时候java堆也被称为“GC堆”。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor空间 控制堆内存的关键配置 使用示例: -Xmx20m -Xms5m 可以通过代码了解下Java使用内存情况
` public class HeapJavaTest {public static void main(String[] args) { //补充
//byte[] b=new byte[610241024];
System.out.print("Xmx=");
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
System.out.print("free mem=");
System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M");
System.out.print("total mem=");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
}
} `
其实JVM在分配内存过程中是动态的, 按需来分配的(通过释放注释中申请的4M的代码可以得知)
在JDK1.8之前堆主要分为年轻代、老年代、永久代,但是在1.8开始 上面我也提及了去掉了永久代,使用了元空间,而元空间使用的内存不是在堆中,是单独开辟了一块本地内存,所以我认为堆目前就只有两部分年轻代和老年代。其中新生代包含伊甸园和survior(2个) ,新生代和老年代默认占比 -XX:NewRatio=2 , 标识新生代占1 , 老年代占2 ,新生代占整个堆的1/3 ;修改占比 -XX:NewPatio=4 , 标识新生代占1 , 老年代占4 , 新生代占整个堆的1/5。Eden空间和另外两Survivor空间占比分别为8:1:1,可以通过操作选项 -XX:SurvivorRatio 调整这个空间比例。 比如 -XX:SurvivorRatio=6,JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
对于堆中新建对象比如new 大概流程描述可以如下
1.new的对象先放在伊甸园区。该区域有大小限制
2.当伊甸园区域填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园预期进行垃圾回收(Minor GC),将伊
甸园区域中不再被其他对象引用的额对象进行销毁,再加载新的对象放到伊甸园区
3.然后将伊甸园区中的剩余对象移动到幸存者0区
4.如果再次触发垃圾回收,此时上次幸存下来的放在幸存者0区的,如果没有回收,就会放到幸存者1区
5.如果再次经历垃圾回收,此时会重新返回幸存者0区,接着再去幸存者1区。
6.如果累计次数到达默认的15次,这会进入养老区。
可以通过设置参数,调整阈值 -XX:MaxTenuringThreshold=N
7.养老区内存不足是,会再次出发GC:Major GC 进行养老区的内存清理
8.如果养老区执行了Major GC后仍然没有办法进行对象的保存,就会报OOM异常.
在JDK1.7之前,HotSpot 虚拟机把方法区当成永久代来进行垃圾回收。而从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。 HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在,只不过取代永久代的是元空间
它和永久代有什么不同的?
存储位置不同:永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存。
存储内容不同:在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了。
与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟
机一样会抛出异常OutOfMemoryError:Metaspace -XX:MetaspaceSize:设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-xx:MetaspaceSize值为21MB。这就是初始的高水位线,一旦触及这个水位线,FullGC将会被触发并卸载没用的类(即这些类对应的类加载器不再存活)然后这个高水位线将会重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到FullGC多次调用。为了避免频繁地GC,建议将-XX:MetaspaceSize设置为一个相对较高的值。
jps #查看进程号 jinfo -flag MetaspaceSize 进程号 #查看Metaspace 最大分配内存空间 jinfo -flag MaxMetaspaceSize 进程号 #查看Metaspace最大空间
直接内存(Direct Memory) 并不是虚拟机运行时数据区的一部分,NIO的Buwer提供一个可以直接访问系统物理内存的类——DirectBuwer。DirectBuwer类继承自ByteBuwer,但和普通的ByteBuwer不同。普通的ByteBuwer仍在JVM堆上分配内存,其最大内存受到最大堆内存的 限制。而DirectBuwer直接分配在物理内存中,并不占用堆空间。在访问普通的ByteBuwer时,系统总是会使用一个“内核缓冲区”进行操作。而DirectBuwer所处的位置,就相当于这个“内核缓冲区”。因此,使用DirectBuwer是一种更加接近内存底层的方法,所以它的速度比普通的ByteBuwer更快。
类加载机制 类加载子系统负责从文件系统或是网络中加载.class文件,class文件在文件开头有特定的文件标识。 2、把加载后的class类信息存放于方法区,除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射); 3、ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定; 4、如果调用构造器实例化对象,则该对象存放在堆区
jvm支持两种类型的加载器,分别是引导类加载器和 自定义加载器 2、引导类加载器是由c/c++实现的,自定义加载器是由java实现的。 3、jvm规范定义自定义加载器是指派生于抽象类ClassLoder的类加载器
启动类加载器 1、这个类加载器使用c/c++实现,嵌套再jvm内部 2、它用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、 resource.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。 3、并不继承自java.lang.ClassLoader,没有父加载器
扩展类加载器 1、java语言编写,由sun.misc.Launcher$ExtClassLoader实现 2、从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的JAR 放在此目录下,也会自动由扩展类加载器加载;派生于 ClassLoader。 3、父类加载器为启动类加载器
系统类加载器 1、java语言编写,由 sun.misc.Lanucher$AppClassLoader 实现 2、该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载的,它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库;派生于 ClassLoader 3、父类加载器为扩展类加载器 4、通过 ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。
老生常谈双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即 ClassNotFoundException ),子加载器才会尝试自己去加载。