目录
1.JVM体系结构概述
2.堆体系结构概述
3.堆参数调优入门
4.总结
JVM与硬件无关
JVM位置
JVM是运行在操作系统之上的,它与硬件没有直接的交互
类装载器ClassLoader
负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
echo %JAVA_HOME%
echo %PATH%
echo %CLASSPATH%
一共有四种类加载器
虚拟机自带的加载器:
启动类加载器(Bootstrap)C++
扩展类加载器(Extension)Java
应用程序类加载器(AppClassLoader)Java 也叫系统类加载器,加载当前应用的classpath的所有类
用户自定义加载器 Java.lang.ClassLoader的子类,用户可以定制类的加载方式
双亲委派机制:自顶向下加载,保证了代码的安全性。防止恶意代码对源代码的修改。
自己定义了新的,会报错!
Code案例
sun.misc.Launcher
它是一个java虚拟机的入口应用
Execution Engine执行引擎负责解释命令,提交操作系统执行。
Native Interface本地接口
本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket通信,也可以使用Web Service等等,不多做介绍。
Native Method Stack本地方法栈
它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。
Thread类中只有start0()方法的声明,这是一个native方法
稳定方法
JNI Java Native Interface
调用操作系统的额外的函数库。
本地方法栈Native Method Stack
带着Native的方法全都到本地方法栈
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码( 用来存储指向下一条指令的地址,也即将要执行的指令代码 ),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
也叫作程序计数器Program Counter Register
不是用来做存储的,而是用来做计算的
Method Area 方法区
方法区是被 所有线程共享 ,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区属于共享区间。
静态变量+常量+类信息(构造方法/接口定义)+运行时常量池存在方法区中
But 实例变量存在堆内存中,和方法区无关
注:只要是所有线程共享的才可以回收和优化,如果是线程私有的不可以回收和优化。
静态变量 static
常量 final
类信息 构造方法、接口
常量池
总结:公共的不变的通通放到方法区,而各自独有的放到堆或栈。
Stack 栈是什么
栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的。
8种基本类型的变量+对象的引用变量+实例方法都是在函数的栈内存中分配。
栈存储什么?
栈帧中主要保存3 类数据:
本地变量(Local Variables):输入参数和输出参数以及方法内的变量;
栈操作(Operand Stack):记录出栈、入栈的操作;
栈帧数据(Frame Data):包括类文件、方法等等。
栈先进后出:main方法先入栈,在最底下;然后是各种方法压进去;如果test1方法中还调用了test2,那么test2执行完之后才执行test1.
如果自己调用自己的话,就会出现StackOverFlow错误。
栈运行原理:
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法A被调用时就产生了一个栈帧 F1,并被压入到栈中,
A方法又调用了 B方法,于是产生栈帧 F2 也被压入栈,
B方法又调用了 C方法,于是产生栈帧 F3 也被压入栈,
……
执行完毕后,先弹出F3栈帧,再弹出F2栈帧,再弹出F1栈帧……
遵循“先进后出”/“后进先出”原则。
图示在一个栈中有两个栈帧:
栈帧 2是最先被调用的方法,先入栈,
然后方法 2 又调用了方法1,栈帧 1处于栈顶的位置,
栈帧 2 处于栈底,执行完毕后,依次弹出栈帧 1和栈帧 2,
线程结束,栈释放。
每执行一个方法都会产生一个栈帧,保存到栈(后进先出)的顶部,顶部栈就是当前的方法,该方法执行完毕 后会自动将此栈帧出栈。
Exception in thread “main” java.lang.StackOverflowError
栈+堆+方法区的交互关系
HotSpot是使用指针的方式来访问对象:
Java堆中会存放访问类元数据的地址,
reference存储的就直接是对象的地址
Heap 堆
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分:
Young Generation Space 新生区 Young/New
Tenure generation space 养老区 Old/ Tenure
Permanent Space 永久区 Perm
Heap堆(Java7之前)
一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
堆内存逻辑上分为三部分:新生+养老+永久----------》Java8之后变成了“新生+养老+元空间”
新生区
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的类都是在伊甸区被new出来的。
幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收**(Minor GC)**,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,然后移动到 1 区。那如果1 区也满了呢?再移动到养老区。
若养老区也满了,那么这个时候将产生MajorGC(FullGC),进行养老区的内存清理。若养老区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
(1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
永久区
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。一般出现这种情况**,都是程序启动需要加载大量的第三方jar包**。例如:在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致Perm区被占满。
Jdk1.6及之前: 有永久代, 常量池1.6在方法区
Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池1.7在堆
Jdk1.8及之后: 无永久代,常量池1.8在元空间
熟悉三区结构后方可学习-JVM垃圾收集
实际而言,**方法区(Method Area)**和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然JVM规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。
对于HotSpot虚拟机,很多开发者习惯将方法区称之为**“永久代(Parmanent Gen)”** ,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口interface)的一个实现,jdk1.7的版本中,已经将原本放在永久代的字符串常量池移走。
方法区UserService接口
永久代UserServiceImpl实现类
常量池(Constant Pool)是方法区的一部分,Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,这部分内容将在类加载后进入方法区的运行时常量池中存放。
所以一般开发人员把方法区成为永久代!
一个是逻辑部分,一个是物理部分!
JVM垃圾收集(Java Garbage Collection )
上集,本次均以JDK1.8+HotSpot为例
Java8
JDK 1.8之后将最初的永久代取消了,由元空间取代。
public static void main(String[] args){
long maxMemory = Runtime.getRuntime().maxMemory() ;//返回 Java 虚拟机试图使用的最大内存量。
long totalMemory = Runtime.getRuntime().totalMemory() ;//返回 Java 虚拟机中的内存总量。
System.out.println("MAX_MEMORY = " + maxMemory + "(字节)、" + (maxMemory / (double)1024 / 1024) + "MB");
System.out.println("TOTAL_MEMORY = " + totalMemory + "(字节)、" + (totalMemory / (double)1024 / 1024) + "MB");
}
(堆内存调优简介02)
发现默认的情况下分配的内存是总内存的“1 / 4”、而初始化的内存为“1 / 64”
VM参数: -Xms1024m -Xmx1024m -XX:+PrintGCDetails
(堆内存调优简介04)
String str = "www.atguigu.com" ;
while(true)
{
str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999) ;
}
VM参数:-Xms8m -Xmx8m -XX:+PrintGCDetails
eclipse内存泄露工具!
用图形界面的方式来显示错在那里。
-XX:+HeapDumpOnOutOfMemoryError
OOM时导出堆到文件。
-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
报这个错误时,导出堆的报错文件
错误原因:new了很多对象把它塞满了!
引用计数法:了解但不使用。问题:双向循环引用的案例就不适用了。
上一步的from变成下一次的to
这个过程循环几次
复制算法有什么优缺点?
新生区:存活1区:存活2区=8:1:1
精确打击,定点清除。
因为老年代没有太多的垃圾回收了
优点:不需要额外的空间
缺点:会产生内存碎片、需要两次扫描比较耗时
请后面的同学往前坐。
优点:没有内存碎片
缺点:需要移动对象,耗时