JVM基础学习
# JVM 基础学习 第一节
JVM简介
java虚拟机平台上运行非Java语言编写的程序
java源代码->二进制字节码(.class文件) ->JVM-> 解释器->机器码->CPU
它只关心“字节码”文件
Java不是最强大的语言,但是JVM是最强大的虚拟机
虚拟机:虚拟的计算机,可分为系统虚拟机和程序虚拟机
java虚拟机就是二进制字节码的运行环境
特点:
1.一次编写,到处运行
2.自动内存的管理机制,垃圾回收功能
3.数组下标越界检查
4.多态
组成:类加载器、运行时数据区、执行引擎、本地方法库
JVM 内存区域
JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区
域【JAVA 堆、方法区】、直接内存。
OOM:Out Of Memory 内存用尽
内存泄露:申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用。
内存溢出:申请的内存超出了JVM能提供的内存大小,此时称之为溢出。
线程
线程 | 作用 |
---|---|
虚拟机线程(VM thread) | 等待JVM到达安全点操作出现,这些操作必须要在独立的线程里执行,因为当堆修改无法进行时,线程都需要JVM位于安全点。这些操作类型有:stop-the-world垃圾回收、线程栈dump、线程暂停、线程偏向锁解除 |
周期性任务线程 | 这线程负责定时器事件(也就是中断),用来调度周期性操作的执行 |
GC线程 | 这些线程支持 JVM 中不同的垃圾回收活动。 |
编译器线程 | 这些线程在运行时将字节码动态编译成本地平台相关的机器码。 |
信号分发线程 | 这个线程接收发送到 JVM 的信号并调用适当的 JVM 方法处理。 |
1.程序计数器
程序计数器(Program Counter register:寄存器)
作用:
记住下一条jvm指令的执行地址
特点:
线程私有的,每个线程都有自己的计数器
不会存在内存溢出
2.虚拟机栈(Java Virtual Machine Stacks)
1.每个线程运行时所需要的内存,称为虚拟机栈
2.每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
3.每个栈只能有一个活动栈帧,对应着当前正在执行的那个方法
问题辨析:
1、垃圾回收是否涉及栈帧?
不需要,方法在执行完成后会弹出栈,自动回收掉,不需要垃圾回收
2、栈内存分配越大越好吗?
不是,分配过多,可能运行效率变低
3、方法内的局部变量是否线程安全?
如果方法局部变量没有逃离方法的作用范围,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,要考虑变量的线程安全问题
栈内存溢出
栈帧过多导致栈内溢出
栈帧过大导致栈内存溢出
java.lang.StackOverflowError 栈内存溢出
-Xss256k 设置栈内存的大小
线程运行诊断
案例1:CPU占用过多
用top查看进程实时监测,定位那个进程对CPU的占用过高
ps H -eo pid,tid,%cpu|grep 进程id(用ps命令进一步定位是哪一个线程引起的CPU占用过高
jstack进程id
案例2:程序运行很长时间没有结果
发生死锁
3.本地方法栈
调用本地方法时所需要的内存,native关键字,使用C语言和C++语言实现的程序
4.堆(Heap)
通过new关键字,创建对象都会使用堆内存
特点:
它是线程共享的,所以堆中都要考虑线程的安全问题
有垃圾回收机制
堆内存溢出
堆内存溢出:java.lang.OutofMemoryError:java heap space
堆内存诊断
1.jps工具
查看当前系统中哪些java进程
2.jmap工具
查看堆内存占用的情况
3.jconsole工具
图形界面的,多功能的检测工具,
5.方法区
以前的永久代使用的堆内存,逻辑上方法区属于堆内存中,但在不同商家的JVM方法区实现的地方不一样
在java8之后,去除了永久代,使用了元空间,占用的内存是本地内存,操作系统的内存
方法区内存溢出
1.8以前导致内存永久溢出
演示永久代内存溢出:java.lang.OutofMemoryError:PermGen space
-XX:MaxPermSize=8m
1.8之后会导致元空间内存溢出
演示元空间内存溢出:java.lang.OutOfMemoryError:Metaspase
-XX:MaxMetaspaceSize=8m
运行时常量池
二进制字节码(类基本信息、常量池。类方法定义,包含了虚拟机指令)
常量池,就是一张表,虚拟机指令根据这张表找到要执行的类名,方法名、参数类型、字面量等信息
运行时常量池,常量池时*.class文件中的,当类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址
反编译命令:
javap -v Demo1.class
常量池中的信息,都会被加载到运行时常量池中,这时a b ab 都是常量池中的符号,还没有变为 java 字符串对象
ldc #2 会把a符号变为“a“字符串对象
ldc #3 会把a符号变为“b“字符串对象
ldc #4 会把a符号变为“ab“字符串对象
String s1="a";
String s2="b";
String s3="ab";在串池中
String s4=s1+s2;在堆中创建了一个新的对象 new stringbuilder().appand("a").appand("b").toString();new String("ab")
String s5="a"+"b";javac 在编译器期间就拼接成"ab",在串池中
s3==s4 //(false)
因为s3的"ab"是存在StringTable中的,而s4是新建对象,存放在堆中的,位置不同,比较返回的是false
String x="ab";
String s=new String("a")+new String("b")
Stirng s2=s.intern()将这个字符串对象尝试放入串池,如果没有则并不会放入,如果有则放入串池,会把串池中的对象返回
s2==x (true)
s==x (false)
在使用这些字符串时才会从常量池中加载到运行时常量池中
StringTable[“a”,“b”,“ab”] hashtable结构,不能扩容
6.StringTable的特性
常量池中的字符串仅是符号,第一次用到时才变为对象
利用串池的机制,来避免重复创建字符串对象
字符串变量拼接的原理是StringBuilder(1.8)
字符串拼接的原理是编译器优化
可以使用intern方法,主动将串池中还没有的字符串对象放入串池
1.8将这个字符串对象尝试放入串池,如果没有则并不会放入,如果有则放入串池,会把串池中的对象返回
1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把对象复制一份,放入串池,会把串池中的对象返回
面试题:
public class Demo1 {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b";
String s4 = s1 + s2;
String s5 = "ab";
String s6 = s4.intern();
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//true
System.out.println(s3 == s6);//true
String x2 = new String("c") + new String("d");
String x1 = "cd";
x2.intern();
//x2.intern();
//String x1 ="cd";
// System.out.println(x1 = x2);//true
System.out.println(x1 = x2);//false
//如果调换了位置,如果是1.6呢
//如果常量池中没有,则会在堆中复制一份到常量池中
}
}
7.StringTable的位置
在JDK8设置:-Xmx10m -XXUseGCOverheadLimit
在JDK6设置:-XX:MaxPermSize=10m
可证明StringTable在堆空间(1.8) 在永久代(1.6)
8.StringTable的性能调优
调整:-XX:StringTableSize=桶个数
考虑将字符串对象是否入池(使用intern方法)
直接内存
1.定义
常见于NIO操作,用于数据缓冲区
分配回收成本高,但读写性能高
不受JVM内存回收管理
2.分配和回收原理
使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存
JVM 运行时内存
Java堆从GC的角度还可以细分为:新生代(Eden**区、FromSurvivor**区和ToSurvivor**区)和老年
代。
1.新生代
是用来存放新生的对象。一般占据堆的1/3空间。由于频繁创建对象,所以新生代会频繁触发
MinorGC进行垃圾回收。新生代又分为Eden区、ServivorFrom、ServivorTo三个区。
Eden区
Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当Eden区内存不够的时候就会触发MinorGC,对新生代区进行 一次垃圾回收
Servivor From
上一次GC的幸存者,作为这一次GC的被扫描者。
ServivorTo
保留了一次MinorGC过程中的幸存者
MinorGC的过程(复制->清空->互换)
MinorGC采用复制算法。
1.eden、servicorFrom复制到ServicorTo,年龄+1
首先,把Eden和ServivorFrom区域中存活的对象复制到ServicorTo区域(如果有对象的年 龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果ServicorTo不够位置了就放到老年区);
2.清空eden、servicorFrom
然后,清空Eden和ServicorFrom中的对象;
3:ServicorTo和ServicorFrom互换
最后,ServicorTo和ServicorFrom互换,原ServicorTo成为下一次GC时的ServicorFrom 区。
2.老年代
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以MajorGC不会频繁执行。在进行MajorGC前一般都先进行 了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足 够大的连续空间分配给新创建的较大对象时也会提前触发一次MajorGC进行垃圾回收腾出空间。
MajorGC采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长,因为要扫描再回收。MajorGC会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出OOM(OutofMemory)异常。
3.永久代
指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被 放入永久区域,它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这 也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。
4.JAVA8与元数据
在Java8中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间 的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。类的元数据放入native memory,字符串池和类的静态变量放入java堆中,这样可以加载多少类的元数据就不再由 MaxPermSize控制,而由系统的实际可用空间来控制。