解读Jvm的内存结构与GC及jvm参数调优
作者:兮家小二
一、JVM 内存结构
1、对象主要存放在堆内存中;方法和属性主要存放在栈内存中。
2、栈是运行时单位,用来解决程序运行时的问题,堆是存储单位,解决数据存储的问题。
3、堆伴随着JVM的启动而创建。
结构图
1、类加载子系统
负责从文件系统或者网络加载Class信息,加载的信息存放在一块称之方法区的内存空间。
2、方法区(method)
存放类、常量、常量池信息、包括字符串字面量和数字常量等, 只要是 static 关键字修饰的都在方法区。
方法区的数据是所有线程共享的。
3、堆(heap)
几乎所有的对象实例都存放到Java堆中,堆空间是所有线程共享,比如new 出来的对象。
在堆中的分配的内存,由java虚拟机自动垃圾回收器来管理。
其中堆又分为了新生代和老年代和永久代 (jdk 1.8后取消永久代增加元空间)。
优点:是可以动态地分配内存大小,垃圾收集器会自动收走这些不再使用的数据。
缺点:由于要在运行时动态分配内存,存取速度较慢
4、栈(stack)
在多线程环境下,每个线程拥有一个栈和一个程序计数器,是线程私有的资源。
方法和属性主要存放在栈内存中。
java中的所有引用都在栈中,并指向堆。
引用是创建对象的时候创建的(new ),第二次创建发现栈中如果有该对象的引用就使用原引用,不会新建
优点:存取速度比堆要快,仅次于直接位于CPU中的寄存器,
缺点:存在栈中的数据大小与生存期必须是确定的,缺乏灵活性,生存期就是他与对象的关联的时间
5、本地方法栈
用于本地方法调用。Java虚拟机允许Java直接调用本地方法(通过使用C语言写)
6、pc寄存器(了解即可)
PC(Program Couneter)寄存器也是每个线程私有的空间,
Java虚拟机会为每个线程创建PC寄存器,在任意时刻,
一个Java线程总是在执行一个方法,这个方法称为当前方法,
如果当前方法不是本地方法,PC寄存器总会执行当前正在被执行的指令,
如果是本地方法,则PC寄存器值为Underfined,
寄存器存放如果当前执行环境指针、程序技术器、操作栈指针、计算的变量指针等信息。
7、执行引擎
虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。
8、垃圾收集器
垃圾收集系统是Java的核心,也是不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理
二、堆–> 新生代/新生代/永久代
1、新生代
主要是用来存放新生的对象。一般占据堆的1/3空间。
由于频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。
新生代又分为 Eden区、ServivorFrom、ServivorTo三个区。
Eden :Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收
ServivorTo :保留了一次MinorGC过程中的幸存者。
ServivorFrom :上一次GC的幸存者,作为这一次GC的被扫描者。
MinorGC 是采用复制算法来清理垃圾
2、老年代
主要是用来存放频繁使用的对象。一般占据堆的2/3空间。
老年代的对象比较稳定,所以MajorGC不会频繁执行。
在进行MajorGC前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。
当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC 是采用标记清除算法来清理垃圾
2.1、永久代(jdk1.7前)
指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,
Class在被加载的时候被放入永久区域. 它和存放实例的区域不同
GC不会在主程序运行期对永久区域进行清理,
永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。
2.2、元空间(jdk1.8后)
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
默认情况下,元空间的大小仅受本地内存限制。
类的元数据放入 native memory, 字符串池和类的静态变量放入java堆中. 这样可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制.
三、垃圾回收算法
垃圾回收机制采用算法一览图
1、引用算法
使用于新生代,新创建的对象,默认引用值为15
当引用值为 0时,该对象被垃圾回收机制回收
如果有地方引用,可达状态,则加1, 如果没有地方引用,不可达状态,则减1
引用说明,new对象的时候创建,在栈中,并指向堆中的对象
2、复制算法(MinorGC)
使用于新生代ds0,ds1中,引用算法的引用值到达了一定高度次数,就会晋升到ds0,ds1中
新的频繁使用的对象都会在一个区内,要么在ds0,要么在ds1,每次只会有一个区会有数据
假如现在数据在ds0中,如果ds0有存活的对象,把存活的对象赋值到ds1,然后清空ds0,新的频繁使用对象也会晋升在ds1,假如现在数据在ds1中,则反之
3、 标记清除算法(MajorGC)
标记清除算法
首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。
- MajorGC的耗时比较长,因为要扫描再回收。
- MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。----------- 当老年代也满了装不下的时候,就会抛出OOM(Out of Memory)异常。
- 标记压缩法在标记清除基础之上做了优化,把存活的对象压缩到内存一端,而后进行垃圾清理。(java中老年代使用的就是标记压缩法)
4、 java 手动回收GC
----Java技术使用finalize()方法在垃圾收集器将对象从内存中清除出去前,做必要的清理工作
----把对象赋值为null 在 System.gc(); 即可回收,但不保证100%
public class JVMDemo { public static void main(String[] args) { // 创建了堆内存 JVMDemo05 jvmDemo05 = new JVMDemo05(); // 赋值为空后去除该对象的引用 //jvmDemo05 = null; //手动垃圾回收,只有所有线程都无引用的对象才会会GC回收 System.gc(); } /** * obj 方法,垃圾回收前触发 */ protected void finalize() throws Throwable { System.out.println("gc在回收对象..."); } }
四、JVM 参数调优
堆的常用配置参数参考
-XX:+PrintGC
每次触发GC的时候打印相关日志-XX:+UseSerialGC
串行回收-XX:+PrintGCDetails
更详细的GC日志-Xms
堆初始值-Xmx
堆最大可用值-Xmn
新生代堆最大可用值-XX:SurvivorRatio
用来设置新生代中eden空间和from/to空间的比例.含以-XX:SurvivorRatio=eden/from=den/to
图文方式
1、设置最大堆内存
----- 1、堆内存默认大小4G,我们可以根据程序大小来指定,默认内存足够的情况下不会触发垃圾回收机制
----- 2、初始的堆大小与最大堆大小设置为相等,减少垃圾回收次数,如果内存一旦达到初始值,就会触发垃圾回收机制,频繁的触发垃圾回收机制影响线程及性能问题
参数:
-Xms
堆初始值-Xmx
堆最大可用值
-Xms512m -Xmx512m -XX:+PrintGCDetails -XX:+UseSerialGC -XX:+PrintCommandLineFlags
2、设置新生代与老年代优化参数
-Xms
堆初始值-Xmx
堆最大可用值-Xmn
新生代大小,一般设为整个堆的1/3到1/4左右-XX:SurvivorRatio
设置新生代中eden区和from/to空间的比例关系n/1
参数:
-Xms512m -Xmx512m -Xmn170m -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC
3、堆内存溢出处理
错误原因
java.lang.OutOfMemoryError Java heap space 堆内存溢出
解决办法
设置堆内存大小,根据实际大小设置
-Xms
堆初始值-Xmx
堆最大可用值-XX:+HeapDumpOnOutOfMemoryError
内存溢出抛出储存快照
示例:
-Xms512m –Xmx512m -XX:+HeapDumpOnOutOfMemoryError
4、栈内存溢出处理
错误原因
栈溢出 产生于递归调用,循环遍历是不会的,但是循环方法里面产生递归调用, 也会发生栈溢出。
java.lang.StackOverflowError 栈内存溢出
解决办法
设置线程最大调用深度
-Xss5m 设置最大调用深度,默认为1M大小 示例: -Xms512m –Xmx512m -Xss5m -XX:+HeapDumpOnOutOfMemoryError
5、Tomcat内存溢出
在catalina.sh 修改JVM堆内存大小
JAVA_OPTS="-server -Xms800m -Xmx800m -XX:PermSize=256m -XX:MaxPermSize=512m -XX:MaxNewSize=512m"
五、 JVM 参数调优二( GC- 垃圾回收器)
1、串行回收(Serial Collector)
------ 单线程执行回收操作,回收期间暂停所有应用线程的执行,client模式下的默认回收器,
jvm参数
-XX:+UseSerialGC 设置为串行回收
详细参数
-XX:+PrintGCDetails -Xmx512M -Xms512M -XX:+HeapDumpOnOutOfMemoryError -XX:+UseSerialGC -XX:PermSize=32M
2、并行回收(ParallelGC)
------ ParNew (新生代 )并行回收器在串行回收器基础上做了改进,他可以使用多个线程同时进行垃圾回收,对于计算能力强的计算机而言,可以有效的缩短垃圾回收所需的实际时间。
------ ParallelOldGC (老年代) 回收器也是一种多线程的回收器,和新生代的ParallelGC回收器一样,也是一种关往吞吐量的回收器,他使用了标记压缩算法进行实现。
jvm参数
----ParNew设置(新生代) XX:+UseParNewGC 设置为并行回收 XX:ParaleiGCThreads 设置ParNew回收器工作时的线程数量,建议指定为电脑核数*2 ----ParallelOldGC设置(老年代) -XX:+UseParallelOldGC 设置为并行回收 -XX:+ParallelCThread 设置垃圾收集时的线程教量, 建议指定为电脑核数*2
详细参数(UseParNewGC)
-XX:+PrintGCDetails -Xmx512M –Xms512M -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParNewGC -XX:PermSize=32M
详细参数(UseParallelGC)
-XX:+PrintGCDetails -Xmx512M -Xms256M -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -XX:+UseParallelOldGC -XX:ParallelGCThreads=8 -XX:PermSize=32M
G1回收器了解
G1回收器(Garbage-First)实在]dk1.7中提出的垃圾回收器,从长期目标来看是为了取代CMS回收器,G1回收器拥有独特的垃圾回收策略,G1属于分代垃圾回收器,区分新生代和老年代,依然有eden和from/to区,它并不要求整个eden区或者新生代、老年代的空间都连续,它使用了分区算法。
jvm调优总结
1、初始堆值和最大堆内存内存越大,吞吐量就越高。
2、最好使用并行收集器,因为并行手机器速度比串行吞吐量高,速度快。
3、设置堆内存新生代的比例和老年代的比例最好为1:2或者1:3。
4、减少GC对老年代的回收。
六、java 代码中查看大小
public class JvmDemo01 { public static void main(String[] args) throws InterruptedException { byte[] b1 = new byte[1 * 1024 * 1024]; System.out.println("分配了1m"); jvmInfo(); Thread.sleep(3000); byte[] b2 = new byte[4 * 1024 * 1024]; System.out.println("分配了4m"); Thread.sleep(3000); jvmInfo(); } static private String toM(long maxMemory) { float num = (float) maxMemory / (1024 * 1024); DecimalFormat df = new DecimalFormat("0.00");// 格式化小数 String s = df.format(num);// 返回的是String类型 return s; } static private void jvmInfo() { // 最大内存 long maxMemory = Runtime.getRuntime().maxMemory(); System.out.println("maxMemory:" + maxMemory + ",转换为M:" + toM(maxMemory)); // 当前空闲内存 long freeMemory = Runtime.getRuntime().freeMemory(); System.out.println("freeMemory:" +freeMemory+",转换为M:"+toM(freeMemory)); // 已经使用内存 long totalMemory = Runtime.getRuntime().totalMemory(); System.out.println("totalMemory:" +totalMemory+",转换为M"+toM(totalMemory)); } }
idea 设置参数
进入编辑找到对应的启动类,VM哪填写JVM参数就好了
eclipse 设置参数
设置
启动jar 项目设置参数
java -jar -Xms20m -Xmx20m xxxxx.jar
其他:压力测试工具 JMeter,可测试接口吞吐量(每秒的并发量)
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。