1、JVM 位置2、JVM 体系结构概览3、堆体系结构概述4、堆参数调优入门5、JVM 的配置和优化6、Tomcat 的配置和优化
熟悉 JVM 架构与 GC 垃圾回收机制以及相应的 JVM 调优,有过在 Linux 系统下的调优经验。
淘宝的周志明《深入理解 Java 虚拟机》中说 JVM 的优化,其中 99% 优化的是堆,1% 优化的是方法区。
内地女歌手照片--李嘉欣,贴在桌面上。
1、JVM 位置
JVM 是运行在操作系统之上的,它与硬件没有直接的交互
2、JVM 体系结构概览
详解如下:
类装载器 Class Loader
负责加载 class 文件,class 文件在文件开头有特定的文件标示,并且 Class Loader 只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。
虚拟机自带的加载器
启动类加载器(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 执行时加载本地方法库。
PC Register 程序寄存器
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记。
这块内存区域很小,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
如果执行的是一个 Native 方法,那这个计数器是空的。
Method Area 方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,此区属于共享区间
。
静态变量 + 常量 + 类信息(构造方法/接口定义) + 运行时常量池 存在方法区中
,但是实例变量存在堆内存中,和方法区无关
。
方法区主要存放的是:构造方法 + 接口的代码
Stack 栈是什么
栈也叫栈内存,主管 Java 程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题
,只要线程一结束该栈就 over,生命周期和线程一致,是线程私有的。8 种基本类型的变量 + 对象的引用变量 + 实例方法
都是在函数的栈内存中分配。
栈存储什么?
栈帧中主要保存 3 类数据:
本地变量(Local Variables):输入参数和输出参数以及方法内的变量。
栈操作(Operand Stack):记录出栈、入栈的操作。
栈帧数据(Frame Data):包括类文件、方法等等。
栈运行原理
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法 A 被调用时就产生了一个栈帧 F1,并被压入到栈中,A 方法又调用了 B 方法,于是产生栈帧 F2 也被压入栈,B 方法又调用了 C 方法,于是产生栈帧 F3 也被压入栈,…… 执行完毕后,先弹出 F3 栈帧,再弹出 F2 栈帧,再弹出 F1 栈帧,……
遵循 “先进后出”/“后进先出” 原则。
每个方法执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口
等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体 JVM 的实现有关,通常在 256K~756K 之间。
栈内存溢出异常:Exception in thread "main" java.lang.StackOverflowError
栈管运行,堆管存储。
三种 JVM
1、Sun 公司的 HotSpot(Oracle 收购)
2、BEA 公司的 JRockit(Oracle 收购)
3、IBM 公司的 J9 VM
3、堆体系结构概述
JVM 优化的是哪里?
Heap 堆
一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行,堆内存分为三部分:
- Young Generation Space 新生区 Young/New
- Tenure generation space 养老区 Old/Tenure
- Permanent Space 永久区 Perm
Heap 堆(Java7 之前)
一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,保存所有引用类型的真实信息,以方便执行器执行。
堆内存逻辑上分为三部分:新生 + 养老 + 永久
新生区
新生区是类的诞生、成长、消亡的区域,一个类在这里产生、应用、最后被垃圾回收器收集、结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被 new 出来的。幸存区有两个:0 区(Survivor 0 space)和 1 区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收
(Minor GC
),将伊甸园区中的不再被其他对象所引用的对象进行销毁
。然后将伊甸园中的剩余对象移动到幸存 0 区。若幸存 0 区也满了,再对该区进行垃圾回收,然后移动到 1 区。那如果
1 区也满了呢?再移动到养老区。若养老区也满了,那么这个时候将产生 Major GC
(Full GC
),进行养老区的内存清理。若养老区执行了 Full GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常 “OutOfMemoryError”。
如果出现 java.lang.OutOfMemoryError: Java heap space
异常,说明 Java 虚拟机的堆内存不够。原因有二:
(1)Java 虚拟机的堆内存设置不够,可以通过参数 -Xms、-Xmx 来调整
。
(2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
。
如何 new 一个大对象? 答:byte[] byteArray = new byte[1 * 1024 * 1024 * 7000];
养老区
养老区用于保存从新生区筛选出来的 Java 对象,一般池对象都在这个区域活跃。
永久区
永久存储区是一个常驻内存区域,用于存放 JDK 自身所携带的 Class、Interface 的元数据,也就是说它存储的是运行环境必须的类信息
,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
如果出现 java.lang.OutOfMemoryError: PermGen space
,说明是 Java 虚拟机对永久代 Perm 内存设置不够。一般出现这种情况,都是程序启动需要加载大量的第三方 jar 包。例如:在一个 Tomcat 下部署了太多的应用。或者大量动态反射生成的类不断被加载,最终导致 Perm 区被占满。
Java 7 叫永久代,Java 8 叫元空间。
实际而言,方法区(Method Area)和堆一样,是各个线程共享的内存区域,它用于存储虚拟机加载的:类信息+普通常量+静态常量+编译器编译后的代码等等,虽然 JVM 规范将方法区描述为堆的一个逻辑部分,但它却还有一个别名叫做 Non-Heap(非堆),目的就是要和堆分开。
对于 HotSpot 虚拟机,很多开发者习惯将方法区称之为“永久代(Parmanent Gen)”,但严格本质上说两者不同,或者说使用永久代来实现方法区而已,永久代是方法区(相当于是一个接口 interface)的一个实现
,jdk1.7 的版本中,已经将原本放在永久代的字符串常量池移走
。
jdk 1.6 方法区就是永久代。常量池在方法区中。
jdk 1.7 中 常量池放在了堆中。
RUNTIME CONSTANT POOL 运行时常量池
常量池(Constant Pool)是方法区的一部分,Class 文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,常量池用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
熟悉三区结构后方可学习-JVM 垃圾收集
4、堆参数调优入门
JVM 垃圾收集(Java Garbage Collection)
以 JDK1.7 + HotSpot 为例
以 JDK1.8 + HotSpot 为例
堆内存调优简介 01
堆内存调优简介 02
发现默认的情况下分配的内存是总内存的“1 / 4”、而初始化的内存为“1 / 64”
调整 VM 参数并打印出来:-Xms1024m -Xmx1024m -XX:+PrintGCDetails
堆内存调优简介 03
java 7
java 8
堆内存调优简介 04
调整 VM 参数并打印出来:-Xms8m -Xmx8m -XX:+PrintGCDetails
java 7
java 8
MAT(Eclipse Memory Analyzer Tool)
官网访问地址:https://projects.eclipse.org/projects/tools.mat/downloads
安装方式1:离线 jar 包方式
1)Eclipse Memory Analyzer Windows 64 位下载地址:http://www.eclipse.org/downloads/download.php?file=/mat/1.8.1/rcp/MemoryAnalyzer-1.8.1.20180910-win32.win32.x86_64.zip
2)解压下载包:放到 eclipse 或 myeclipse 安装目录的 dropins 目录下。
3)启动 eclipse 或 myeclipse,打开 Window - > Perspective,看到 Memory Analyzer 证明安装成功。
安装方式2:联网插件安装方式
http://download.eclipse.org/mat/1.8.1/update-site/
使用 MAT 分析
启动 eclipse 或 myeclipse,打开 File - > Open heap dump,在弹出的对话框选择生成的 dump文件,就可以看到 MAT 给出了overview page。
面试题
5、JVM 的配置和优化
JVM 复习
GC 是什么
频繁收集 新生区,使用的算法是复制算法
较少收集 老年区,使用的算法是
基本不动 永久区 (jdk7)
GC 三大算法
GC 算法总体概述
复制算法:Minor GC(普通 GC)
新生代中使用的是 Minor GC(普通 GC),这种 GC 算法采用的是复制算法(Copying)
复制算法的原理如下图所示:
Minor GC(普通 GC)会把 Eden 中的所有活的对象都移到 Survivor 区域中,如果 Survivor 区域中放不下,那么剩下的活的对象就被移动到 Old Generation 中,也即一旦收集后,Eden 就变成空的了
。
复制算法的缺点:
标记清除算法/标记整理算法:Full GC 又叫 Major GC(全局 GC)
老年代一般是由标记清除或者是标记清除与标记整理的混合实现。
标记清除:Mark-Sweep
标记清除原理
标记清除算法的缺点:
标记整理:Mark-Compact
标记整理算法原理
标记整理算法的缺点:
小总结
面试题
6、Tomcat 的配置和优化
(1)点击【参数配置】选项,Eclipse 中的 Tomcat 一般在默认情况下内存偏小,运行一会儿就会抛出内存溢出错误,需要在 Tomcat 的 VM arguments 中添加如下参数:-Xms128M -Xmx512M -XX:PermSize=512m -XX:MaxPermSize=1024m,具体大小根据自己的电脑硬件。最后点击下面的 "OK" 按钮保存配置。
(2)45秒和15秒分别是tomcat启动和停止的超时时间,该长一些,防止工程较大启动慢造成工程启动不了。
(3)Eclipse默认将工程部署至eclipse的目录中,目录层次较深不易操作,这里改到tomcat自己的部署目录中。
注意:如果eclipse的tomcat已添加工程需要在eclipse中将tomcat下的工程全部移除后方可设置该项。
配置完成后,要点击【保存】按钮。
原文地址:https://www.cnblogs.com/chenmingjun/p/11029371.html