JVM 基础知识
1、 jvm运行时内存结构
1、JVM基础构成
1.1 jvm 整体结构说明(基于jdk1.8)
1.2 JVM 是什么
jdk=jvm +sdk ,jvm 是java的运行环境。jvm会遇见OOM 内存溢出问题。Out of Memery 内存溢出 一般都是堆内存溢出。通过的解决方法:合理设置jvm内存空间。
1.3 类加载器-class loader
作用:作用 : 类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。
一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。
ClassLoader的分类:
-
Bootstrap ClassLoader :引导类加载器。由C++写的,由JVM启动,最底层的类加载器。
启动类加载器,负责加载java基础类,对应的文件是%JRE_HOME/lib/ 目录下的rt.jar、resources.jar、charsets.jar和class等
-
Extension ClassLoader:扩展类加载器。Java类,继承自URLClassLoader 。
负责加载jdk自带的扩展类。对应的文件是 %JRE_HOME/lib/ext 目录下的jar和class等
-
App ClassLoader:**应用加载器。**Java类,继承自URLClassLoader 系统类加载器,
负载加载自己写的java代码。对应的文件是应用程序classpath目录下的所有jar和class等
-
自定义加载器: 可以自定义加载器,不常用。
1.4 ClassLoader 加载机制--双亲委派机制
- 什么是双亲委派机制
当ClassLoader要加载一个类时,会先委托它的父类加载器尝试加载,一直往上,如果最顶级的父类加载器没有找到该类,那么委托者则亲自到特定的地方加载。
//java底层实现源码
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) { // 首先,检测是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { //父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else { //父加载器为空则调用Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader
}
if (c == null) { // If still not found, then invoke findClass in order // to find the class.
long t1 = System.nanoTime(); //父加载器没有找到,则调用findclass
c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) { //调用resolveClass()
resolveClass(c);
}
return c;
}
}
-
双亲委派机制的作用:
1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。
1.5 垃圾回收系统
作用 : 负责收集垃圾, 销毁, 回收内存. 对java堆, 方法区, 直接内存进行垃圾回收.
1.6 执行引擎
作用 : 执行编译后的字节码指令.
主要的执行技术有 : 解释执行,即时编译执行,自适应优化执行, 芯片级直接执行
(1)解释:属于第一代JVM;
(2)即时编译:JIT属于第二代JVM;
(3)自适应优化:(目前Sun的HotspotJVM采用这种技术)吸取第一代JVM和第二代JVM的经验,采用两者结合的方式。开始对所有的代码都采取解释执行的方式,并监视代码执行情况,然后对那些 经常 调用的方法启动一个后台线程,将其编译为本地代码,并进行仔细优化。若方法不再频繁使用,则取消编译过的代码,仍对其进行解释执行;
(4)芯片级直接执行:内嵌在芯片上,用本地方法执行Java字节码。android 软件开发使用该方法。
2、线程私有区域
- java栈
作用 : 线程私有,它的生命周期与线程相同。存储局部变量, 动态链接, 方法出口等信息。栈的内部结构
- 本地方法栈
作用 : 用于本地方法调用, JDK源码中好多使用了Native关键字, 也就是调用底层C语言编写的方法.
注意: (Sun HotSpot虚拟机)直接就把本地方法栈和Java栈合二为一.
- PC寄存器(程序计数器)
作用 : 每个线程启动的时候,都会创建一个PC寄存器。保存下一条将要执行的指令地址.
3、线程共享区域
- 方法区
作用 : Java方法区和堆一样,方法区是一块所有线程共享的内存区域,保存系统的类信息。比如类的字段、方法、常量池等。方法区的大小决定系统可以保存多少个类。如果系统定义太多的类,导致方法区溢出。虚拟机同样会抛出内存溢出的错误。
- java堆
作用 : 堆内存用于存放由new创建的对象和数组。
在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名或者代号
- 堆,方法区,栈之间的相互关系
- 直接内存
作用 : 提高一些场景中的性能。
直接内存并不是JVM虚拟机运行时数据区的一部分。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制
配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常
- 直接内存(堆外内存)与堆内存比较
直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显
4、 堆中内部结构
JDK8开始取消永久代, 使用元数据区替代永久代, 元数据区不在堆中, 是堆外的一块直接内存.
4.1 进行垃圾回收的过程
根据垃圾回收机制的不同,Java堆有可能拥有不同的结构,最为常见的就是将整个Java堆分为新生代和老年代。其中新生代存放新生的对象或者年龄不大的对象,老年代则存放老年对象。
新生代分为den区、s0区、s1区,s0和s1也被称为from和to区域,他们是两块大小相等并且可以互相变换角色的空间。
绝大多数情况下,对象首先分配在eden区,在新生代回收后,如果对象还存活,则进入s0或s1区,之后每经过一次新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄后,则进入老年代。
5、栈中内部结构 
作用 : 线程私有,它的生命周期与线程相同。存储局部变量, 动态链接, 方法出口等信息.
栈中放置以下内容:栈中存放基本类型的原值和引用类型的地址值.
基本类型包括:byte,short,int,long,char,float,double,Boolean,returnAddress 引用类型包括:类类型,接口类型和数组。
栈的内部结构如下:
栈由一个一个栈帧组成, 栈帧中的结构又由局部变量表, 操作数栈, 帧数据区组成.
局部变量表 : 保存函数参数,局部变量(当前函数有效,函数执行结束它销毁)
操作数栈 : 存中间运算结果, 临时存储空间
帧数据区 : 保存访问常量池指针, 异常处理表
2、JVM对象模型
Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。而这个关于Java对象自身的存储模型称之为Java对象模型。
HotSpot虚拟机中(Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机),设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass对象,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。
3.堆参数调优入门
均以JDK1.8+HotSpot为例
jdk1.7:
jdk1.8:
3.1. 常用JVM参数
怎么对jvm进行调优?通过参数配置
| 参数 | 备注 |
|---|---|
| -Xms | 初始堆大小。只要启动,就占用的堆大小,默认是内存的1/64 |
| -Xmx | 最大堆大小。默认是内存的1/4 |
| -Xmn | 新生区堆大小 |
| -XX:+PrintGCDetails | 输出详细的GC处理日志 |
| -XX:MaxMetaspaceSize=128m | 设置元空间-方法区(模板区)大小 例如:128M |
java代码查看jvm堆的默认值大小:
Runtime.getRuntime().maxMemory() // 堆的最大值,默认是内存的1/4 Runtime.getRuntime().totalMemory() // 堆的当前总大小,默认是内存的1/64
3.2. 怎么设置JVM参数
idea运行时设置方式如下:
3.3. 查看堆内存详情
public class Demo2 {
public static void main(String[] args) {
System.out.print("最大堆大小:");
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M");
System.out.print("当前堆大小:");
System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
System.out.println("==================================================");
byte[] b = null;
for (int i = 0; i < 10; i++) {
b = new byte[1 * 1024 * 1024];
}
}
}
执行前配置参数:-Xmx50m -Xms30m -XX:+PrintGCDetails
执行:看到如下信息
新生代和老年代的堆大小之和是Runtime.getRuntime().totalMemory()
3.4. GC演示
public class HeapDemo {
public static void main(String args[]) {
System.out.println("=====================Begin=========================");
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");
System.out.println("==================First Allocated===================");
byte[] b1 = new byte[5 * 1024 * 1024];
System.out.println("5MB array allocated");
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");
System.out.println("=================Second Allocated===================");
byte[] b2 = new byte[10 * 1024 * 1024];
System.out.println("10MB array allocated");
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");
System.out.println("=====================OOM=========================");
System.out.println("OOM!!!");
System.gc();
byte[] b3 = new byte[40 * 1024 * 1024];
}
}
jvm参数设置成最大堆内存100M,当前堆内存10M:-Xmx100m -Xms10m -XX:+PrintGCDetails
再次运行,可以看到minor GC和full GC日志:
3.5. OOM演示
把上面案例中的jvm参数改成最大堆内存设置成50M,当前堆内存设置成10M,执行测试: -Xmx50m -Xms10m
=====================Begin========================= 最大堆大小:Xmx=44.5M 剩余堆大小:free mem=8.186859130859375M 当前堆大小:total mem=9.5M =================First Allocated===================== 5MB array allocated 剩余堆大小:free mem=3.1868438720703125M 当前堆大小:total mem=9.5M ================Second Allocated==================== 10MB array allocated 剩余堆大小:free mem=3.68682861328125M 当前堆大小:total mem=20.0M =====================OOM========================= OOM!!! Exception in thread "main" java.lang.OutOfMemoryError: Java heap space :free mem=3.1868438720703125M 当前堆大小:total mem=9.5M ================Second Allocated==================== 10MB array allocated 剩余堆大小:free mem=3.68682861328125M 当前堆大小:total mem=20.0M =====================OOM========================= OOM!!! Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at com.atguigu.demo.HeapDemo.main(HeapDemo.java:40)