3. 本地方法栈
本地方法栈是java虚拟机在调用一些本地方法时需要给这些本地方法提供的内存空间,本地方法是指那些不是由java代码编写的方法,因为
我们的java代码是有一定的限制的,它有的时候不能直接跟我们的操作系统底层打交道,所以就需要一些用c或c++编写的本地方法来与操作系统更
底层的api来打交道,java代码可以间接地通过本地方法来调用到底层的一些功能,那这些本地方法运行的时候使用的内存就叫做本地方法栈,
那这样的本地方法多不多呢?答案是非常的多,不管是一些java的基础类库里还是在执行引擎里,它们都会去调用这些本地方法
Object类里面的clone()方法是做一个对象的克隆,可以看到它的方法声明就是native,这种native的方法它是没有方法实现的,
它的方法实现都是由c或c++语言编写的,我们的Java代码通过本地方法接口间接地调用真正的c和c++的它的实现,这样的方法很多,
比如Object类里的hashCode()方法就是本地方法,Object类的notify()方法和notifyAll()方法也都是本地方法,Object类
的wait()方法也是本地方法
本地方法栈的作用就是给本地方法的运行提供一个内存空间
4. 堆
前面我们讲的程序计数器、虚拟机栈、本地方法栈它们有一个共同的特点,它们
都是线程私有的,而从我们现在开始讲解的堆以及后面的方法区它们可以看成是
线程共享的区域,接下来我们看一下堆的定义。
4.1 定义
Heap 堆
特点
-
它是线程共享的,堆中对象都需要考虑线程安全的问题。 以前我们说的虚拟机栈它中的一些局部变量那都是线程私有的,只要你的局部变量
不逃逸出方法的作用范围,它就是线程安全的。
但是我们的堆不一样,因为堆中的对象一般来讲都是需要考虑线程安全问题的,因为它们都是线程共享的,当然也会有例外,后面
我们会讲到
-
垃圾回收机制 堆中不再被引用的对象就会当成垃圾进行一个回收以释放空闲的内存,不至于内存被创建的对象给撑爆。
4.2 堆内存溢出
import java.util.ArrayList;
import java.util.List;
public class Demo06 {
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String a = "hello";
while (true){
list.add(a);
a = a + a;
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.println(i);
}
}
}
结果如下:
那我们分析一下这个案例,看看它为什么产生了堆内存溢出。
a字符串不断递增,而a一直追加到list集合的尾部,不断地把hello字符串对象加入到list集合当中,而list集合它的生存范围从它被声明
开始,一直到catch块前面,它都是一个有效范围,所以它不能被垃圾回收掉,也就是程序还没有达到catch之前,那么它一直都是在它的有效
范围内,所以它就一直有效,而它里面的字符串对象由于被追加到了list集合的内部,所以这些字符串对象相当于有人在使用它们,它们也不能
被垃圾回收,所以随着字符串对象越来越多、越来越多,它就会把我们的堆空间占满了,这就是我们堆空间溢出的一个分析。
接下来我们讲一个参数: -Xmx8m
这个参数可以控制堆空间的最大值,现在我们将堆空间设置为8m
当我们设置堆空间为8m后,我们再次运行上面的程序
有的时候内存非常大的时候,它可能不太容易暴露这个内存溢出的问题,也许随着时间的累计,它最终会导致你的内存溢出,但是由于你的内存
比较大,可能你运行了很短的一段时间,你觉得它没问题,因为很短的时间堆内存仍然足够,所以排查这种堆内存溢出的问题,大家最好把你的
堆内存设的稍微小一些,这样可能尽早的暴露出可能引起的堆内存溢出的问题。
4.3 堆内存诊断
这里介绍一些堆内存诊断相关的工具,这些命令可以直接在命令行窗口上敲
- jps工具
- 查看当前系统中有哪些java进程,并且把他们的进程编号显示出来。
拿到java进程的id以后,我们就可以通过jmap工具查看这个java进程堆内存的使用情况
- jmap工具
- 查看堆内存的使用情况 jmap -heap 进程id
jmap只能查看某个时刻它的堆内存的占用情况,如果想要做一个连续的监测,去查看随着时间的流逝堆内存的占用情况的话,我们就得用一个
功能更复杂的jconsole工具来进行监测。
- jconsole工具
jconsole工具是图形界面的,而前两种工具都是命令行的方式,另外jconsole是一个多功能的监测工具,它不仅可以监测堆内存的占用,
它还可以监测线程、cpu这些都可以通过jconsole来监测,而且它也能实现这种连续的监测,下面我们通过一些案例具体来看看这几个工具
它们的使用。
我们看一下这个案例:
public class Demo07 {
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000);
byte[] array = new byte[1024 * 1024 * 10];
System.out.println("2...");
Thread.sleep(30000);
array = null;
System.gc();
System.out.println("3...");
Thread.sleep(1000000L);
}
}
首先,我们先把程序运行起来,然后使用jps工具查看我们运行的java程序的进程id
之后我们在程序输出1…的时候使用jmap -heap 线程id命令查看我们运行的java程序此时的堆内存使用情况
也就是 jmap -heap 110540命令
执行到2,也就是新数组10M在堆空间被分配了以后,我们再来监测一遍
我们再继续往下看,执行到3,也就是垃圾回收以后,我们再监测一遍
看完了jps和jmap工具的使用,下面我们来看一下jconsole工具的使用。
在命令行输入jconsole就可以出现jconsole工具的窗口
之后我们来动态监测一下java程序运行过程中的堆内存使用情况
首先我们来看一下,在输出1的时候,也就是程序刚开始运行还没有创建数组的时候,堆内存使用情况为二十几M
在程序输出2的时候此时已经新建了数组,此时堆内存的使用为四十几M,相较于输出1时的堆内存情况我们可以看到增长了十几M
最后在程序输出3的时候堆内存为6M多,这时我们已经进行过了垃圾回收,且此时由于数组引用为空,数组也被当成垃圾回收掉了
另外,jconsole窗口还可以查看内存的使用情况以及查看cpu的占用率
接下来我们再介绍一个案例,来学习jvisualvm
案例
我们运行下面的程序
import java.util.ArrayList;
import java.util.List;
public class Demo08 {
public static void main(String[] args) throws InterruptedException {
List<Student> students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());
}
Thread.sleep(1000000000L);
}
}
class Student{
private byte[] big = new byte[1024 * 1024];
}
首先我们通过jps工具来看看进程id,从下面的图中我们可以看到进程id是112088
使用jmap -heap 112088 来看看这个线程的内存使用情况
我们可以发现总共使用了200多M内存
接下来我们使用jconsole执行一次垃圾回收
jconsole工具的使用前面已经经过了,忘记的话可以去翻一下前面的知识啊
接下来我们再次使用jmap工具来看看内存使用情况,我们可以发现新生代内存确实被回收了不少,但是老年代却还是有很多
我们就很奇怪,为什么内存一直没有回收掉呢,是不是由于我们的编程失误导致一些对象始终被引用而无法释放它们的内存呢,接下来我们再介绍一个工具jvisualvm,它的意思是用可视化的方式来展示虚拟机内容
之后我们使用堆 Dump功能,它是抓取当前状态的快照,并对里面的内容进行详细的分析,这是jmap和jconsole所不具备的功能
之后我们查找前20个最占内存的对象
而且这些Student实例肯定是长期使用的,导致我们的垃圾回收没办法回收掉
接下来我们来看一下java源代码
实际环境下分析和排查的手段也是类似的,我们先通过堆 Dump这个功能把内存快照抓下来,抓下来以后我们分析对内存占用最大的那些对象,
就可以得出一些有用的结论,就可以发现到底是哪些对象占用的内存太大,那你再去分析和排查你的原始代码就可以了
|