java内存
内存泄露&内存溢出
内存泄露
定义:申请的内存没有被正确释放。
Java中也存在内存泄露。当被分配的对象可达但已无用(有其他变量引用当前待回收变量)
预防办法:
- 尽早释放无用变量。(使用临时变量的时候,让引用变量在退出活动域后自动设置为null)
- 进行大量字符串处理时,尽量避免使用String,而应使用StringBuffer。因为String每次操作都是生成新的对象,占用内存多。影响性能。
- 尽量少用静态变量。因为静态变量是全局的,GC不会回收。
- 不要在循环中创建变量
内存溢出
系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。比如,内存溢出指你申请了10个字节的空间,但是你在这个空间写入11或以上字节的数据,就是溢出。
- 堆溢出:
java.lang.OutOfMemoryError: Java heap space
可能原因:1 大对象的分配,or内存泄露. 2 用户数量或数据量突然激增并超过预期的阈值
finalize…
解决办法:
首先确认是内存泄露还是内存溢出。(分析导致溢出的对象是否是必要的,没有必要就说明是内存泄露),如果是内存泄露,则进一步查看对象到GC Roots的引用链,找到无法被收集的原因。
如果不是内存泄露,可以修改-Xms和-Xmx两个jvm堆参数,扩大内存。默认是64m。减少大对象的分配。再从代码上查看是否存在某些对象生命周期过长,存储结构设计不合理的现象。
- 虚拟机栈和本地方法栈溢出
StackOverflowError
可能原因:线程请求栈的深度大于虚拟机所允许的最大深度。比如递归没有终止条件。容易排查。
解决办法:-Xss参数调高,或者查看是否存在死循环。
3.方法区和运行常量池溢出
常量池被放到方法区中。// jdk7包括以后,字符串常量池已经挪到了java堆中。‘
存放类型相关的信息:类名,访问修饰符,常量池,字段描述,方法描述。
- metaspace元空间内存溢出
java.lang.OutOfMemoryError: Metaspace
。
原因:系统的代码非常多或引用的第三方包非常多或者通过动态代码生成类加载等方法
解决办法:优化参数配置,避免影响其他JVM进程: -XX:MetaspaceSize,初始空间大小。去掉没有用的jar包
总结:分配给栈的内存并不是越大越好,因为栈内存越大,线程多,留给堆的空间就不多了,容易抛出OOM。JVM的默认参数一般情况没有问题
线程私有内容
程序计数器
用来取指,计数器的值代表下一条执行的指令的地址
虚拟机栈
每次方法调用的数据都是通过栈传递的
java栈中保存的内容是栈帧.每一次函数调用都会有对应的栈帧被压入java栈,每一个函数调用结束之后,都有一个栈帧被弹出来。
(内存变量,操作栈、方法返回值)
每个方法被执行的时候,jvm都会同步创建一个栈帧存储局部变量表,方法出口、操作数等。每个方法区被执行完毕都对应一个栈帧在虚拟机栈从出栈到入栈的过程。
本地方法栈
为虚拟机使用到的native方法服务。
堆
程序员声明的对象。(存储的是对象的实例)
从分配内存的角度上看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区
但是从回收内存角度上看,也包括新生区(Eden, suvivor),老年区,元空间(meta space)
元空间:java运行时的环境或类信息。这个区域不存在垃圾回收!关闭JVM虚拟机就会释放这个区域的内存。
元空间
方法区
存放被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
包括:运行期的常量池
对象的创建
1. 类加载检查
虚拟机遇到new指令时,检查这个指令的参数是否能在常量池中定位到这个**类**的符号引用。
检查这个类是否被加载过、解析、初始化。没有就执行类加载过程。
2. 分配内存
为新生对象分配内存(在类加载完之后就能确定需要多少内存)
把一块确定大小的内存从Java堆中划分出来。
分配方式:“指针碰撞”、“空闲列表”。
指针碰撞:java堆中的内存规整:中间一个指针作为java堆的分界点。分配内存就是指针往空闲空间那边挪动一个对象大小相等的距离。
空闲列表:java堆中的内存不规整:虚拟机维护一个列表记录哪些内存块是可以用的。每次分配后更新列表的记录。
解决内存划分中的同步问题:
1. CAS内存失败重试
2. 每个线程在java堆中预先分配一小块内存,叫本地线程分配缓存TLAB。分配新的缓存区的时候,才需要同步锁定。虚拟机是否用过TLAB可以用-XX:+/-UserTLAB
3. 初始化零值
分配的内存空间需要都初始化0值。这样不赋初始值就能使用。
4. 设置对象头
设置对象头
例如,对象是哪个类的实例,如何找到类的元数据信息,对象的hash码,对象的GC分代年龄等信息。
5. 执行init方法
执行程序员设定的init方法
判断对象是否死亡
垃圾回收前需要判断。
方法:
引用计数法:被引用的次数是否变为0(很难解决对象之间相互循环引用的问题)
可达性分析法:通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的 路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
判断类是否可以被回收
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾回收
Serial收集器
CMS收集器
并发标记清楚收集器
和串行的一样,将堆结构化分成三个部分:年轻代、老年代、固定内存大小的永久代。
G1收集器
引入了region概念,每个region都是连续的虚拟内存范围。region中又分成了eden, suvivor, old。
region的大小可以在JVM启动的时候选择。大约两千个region。
相比于CMS:消除了碎片,简化收集器的各个部分
每个region区域进行单独垃圾回收。记录每个region垃圾回收的时间以及回收获得的空间,维护一个优先列表。每次优先回收价值最大的Region
执行流程:
- 并发全局标记:确定堆中对象的活跃度,标记。
对象一开始优先进入Eden中。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Young GC
经过一次垃圾回收后,存活下来的被放到suvivor0, suvivor1中,并且年龄+1
年龄达到一定的预置(默认为15)。
另外,大对象先进入老年区(因为大对象的复制会消耗很多)
Old GC
如果统计信息说,young gen待晋升(要移动到old gen)的数据比old gen空间大,则会出发full GC
垃圾收集的算法(其中两个)
标记-清除算法: 当内存区域满了的时候,将依旧存活的对象做标记,删除所有没有标记的对象
标记-复制: 将内存分成大小相同的两块。每次只使用其中一块。当其中的一块满了的时候,将存活的变量复制到另一块内存中,删除之前的整个内存。
对于新生代,每次收集会死亡大量的对象,所以适合用标记-复制,对于老生代,变量占内存大,复制代价高,并且存活率高。所以适合用标记-清除
类的生命周期
加载-》链接-》初始化-》使用-》卸载
类加载流程
类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。
-
加载。将class字节码文件,通过类加载器,装载进内存。这些.class文件包括本地路径下编译生成的.class文件,从jar包中的.class文件等。
-
链接:将Java类的二进制代码合并到JVM的运行状态中。
2.1 验证:保证加载进来的字节流符合jvm规范。
常量中是否有不被支持的常量(文件格式的验证)
类是否继承了被final修饰的类,类中的方法是否与父类冲突(元数据验证)
保证类型转换的合理性(字节码合理性)
2.2 准备:为类变量分配内存,赋予初值。(根据不同的变量类型的默认的初始值)
比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,
2.3 解析:常量池内的符号引用替换为直接引用
符号引用指的是字符串。直接引用指的是一个内存地址,或者一个偏移量。
-
初始化
只对static修饰的变量或语句进行初始化。
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
静态代码块、静态变量、构造函数
1. 装载类
2. 完成静态动作(包括静态代码和变量,它们的级别是相同的,按照代码中出现的顺序初始化)
3. 类装载后进行实例化。
4. 初始化类的非静态代码和函数(实际上是会被提取到类的构造器中被执行的)
5. 构造方法
静态成员属于整个类的,在类 *加载* 完成后,已经初始化完成。