JVM & JMM

一、JVM运行流程
二、类加载过程
三、类的初始化时机
四、双亲委派模型
五、JVM运行时数据区
六、配置
七、JVM内存回收
八、垃圾收集器-GC
九、垃圾回收执行机制的分类
十、内存泄漏和内存溢出
十一、工具

如有雷同,请告知

一、JVM运行流程

JVM & JMM_第1张图片

二、类加载过程

  • 1、加载

取得类的二进制字节流,通过类的全限定名称(包名+类名)
把二进制字节流中静态存储结构转化为方法区数据结构
在内存中生成代表这个类的java.lang.Class对象,这里是放在堆中

  • 2、连接

2.1、验证
验证类的格式是否正确
文件格式的验证
元数据验证
字节码验证
符号引用验证

2.2、准备
为类的静态变量(static修饰)分配内存(方法区),并将其初始化成默认值

2.3、解析
常量池中的符号引用替换为直接引用

  • 3、初始化

如果这个类还没有被加载和链接,那就先加载和链接
如果类存在直接的父类,先初始化直接父类
如果类中存在初始化语句,那就依次执行初始化语句

注解::类的构造器 由编译器自动收集
static{}语句
static变量

三、类的初始化时机

主动使用

  • new,创建类的实例对象
  • 反射
  • 调用类的静态方法
  • 初始化类的子类,此时先要初始化父类
  • 访问类中的静态变量或者给静态变量赋值
  • Jvm启动的时候的启动类

四、双亲委派模型

JVM & JMM_第2张图片

五、JVM运行时数据区

JVM & JMM_第3张图片

  • 1、Program Counter Register(程序计数器)

作用

当前线程执行的字节码的行号指示器,通过改变此指示器来选取下一个需要执行的字节码指令

特征

在线程创建时创建
每个线程拥有一个
指向下一条指令的地址

  • 2、Method Area(Non-Heap)(方法区,有的也叫永久代)

线程共享
存储
类信息
常量
静态变量
方法字节码

  • 3、VM Stack (虚拟机栈)/ Native Method Stack(本地方法栈)

线程私有
方法在执行时会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接(用到某个类再加载到内存)、方法出口等信息
方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
局部变量表所需的内存空间在编译期间完成分配,而且分配多大的局部变量空间是完全确定的,在方法运行期间不会改变其大小
出栈后空间释放

  • 4、Heap(堆)

线程共享
存储对象或数组
Heap划分
JVM & JMM_第4张图片

JMM

JVM & JMM_第5张图片
Java 内存模型中的可见性、原子性和有序性

可见性:
  可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
  可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。
  在 Java 中 volatile、synchronized 和 final 实现可见性。
原子性:
  原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
  在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。
有序性:
  Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
JVM & JMM_第6张图片
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

AppMain.java
 public   class  AppMain                
//运行时, jvm 把appmain的信息都放入方法区
{
public   static   void  main(String[] args)  //main 方法本身放入方法区。
{
Sample test1 = new  Sample( " 测试1 " );   //test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面
Sample test2 = new  Sample( " 测试2 " );
test1.printName();
test2.printName();
}
} 
Sample.java
 public   class  Sample        //运行时, jvm 把appmain的信息都放入方法区
{
/** 范例名称 */
private  name;      //new Sample实例后, name 引用放入栈区里,  name 对象放入堆里
/** 构造方法 */
public  Sample(String name)
{
this .name = name;
}
/** 输出 */
public   void  printName()   //print方法本身放入 方法区里。
{
System.out.println(name);
}
} 

JVM & JMM_第7张图片

六、配置

Trace跟踪参数
-verbose:gc 打印GC日志信息
-XX:+PrintGCDetails 打印GC日志信息
-Xloggc:d:/gc.log GC日志目录
-XX:+PrintHeapAtGC 每次一次GC后,都打印堆信息
-XX:+TraceClassLoading 类加载信息
Stack内存分配参数
-Xss 决定方法调用的深度每个线程独有栈空间参数,局部变量分配在栈上一般几百K就够了,64位jvm默认1M
Heap内存分配参数
-Xmx 最大堆
–Xms 最小堆
-Xmn 新生代大小 (eden+2s)
–XX:NewRatio 年轻代(eden+2s):老年代
–XX:SurvivorRatio 2s:eden

七、JVM内存回收

  • 7.1、标记-清除算法

标记阶段

标记存活对象
清除阶段
统一回收所有未标记的对象

缺点

会产生内存碎片
如果空间内存碎片太多,当程序产生大对象无法在堆中找到连续空间大小存放的时候,会强制发生GC

  • 7.2、复制算法

原理

内存一分为二,每次只使用其中一块,当一块内存没有连续空间存储对象的时候,会把存活下来的对象复制到另外一块内存中,然后一次性清除之前的哪块空间
优缺点
没有内存碎片问题
代价就是讲内存减少了一半,空间利用率不高
不适用于存活对象较多的场景,比如老年代
而实际上我们并不需要按照1:1的比例来划分,因为大部分对象从创建到结束这个生命周期很短
HotSpot虚拟机默认Eden:Survivor=8:1

  • 7.3、标记-整理算法

原理

标记存活对象,然后把存活对象向一端移动
清理掉存活对象这端以外的所有空间

优缺点

适合用于存活对象较多的场合,如老年代
解决了空间碎片和效率问题:
将所有的存活对象压缩到内存的一端,然后清理边界外所有的空间

  • 7.4、分代收集算法

分代思想
堆划分为新生代和老年代
新生代中,能够存活的对象很少,可以使用复制算法
老年代中,对象存活率高,而且没有额外的空间用来做老年代的担保,可以使用标记清除或者标记整理算法

八、垃圾收集器-GC

Serial 新生代串行收集器 新(复制算法),老(标记整理)
ParNew 新生代并行收集器
Parallel Scavenge 新生代并行收集器
目标:尽可能缩GC时用户线程的停顿时间
在注重吞吐量或CPU资源敏感的场合,可以优先考虑Parallel Scavenge收集器 + Parallel Old收集器
Serial Old 老年代串行收集器
Parallel Old 老年代并行收集器
CMS 真正意义上的并发收集器(老年代收集器)
目标:最短的GC停顿时间
G1
JVM & JMM_第8张图片

  1. Serial/Serial Old
    它们都是一个单线程的收集器,在进行垃圾收集时,必须暂停所有的用户线程。它的优点是实现简单高效,但是缺点是会给用户带来停顿。Serial 是针对新生代的收集器,采用的是复制算法。Serial Old 是针对老年代的收集器,采用的是标记整理算法。
  2. ParNew
    ParNew 收集器是 Serial 收集器的多线程版本,使用多个线程进行垃圾收集,是针对新生代的收集器,采用的是复制算法。
  3. Parallel Scavenge / Parallel Old
    Parallel Scavenge 收集器是一个针对新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是复制算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。Parallel Old 是 Parallel Scavenge 收集器的老年代版本(并行收集器),使用多线程和标记复制算法。
  4. CMS(Concurrent Mark Sweep)
    它是一种以获取最短回收停顿时间为目标的收集器,它是一种针对老年代的并发收集器,采用的是标记清除算法。
  5. G1
    G1 收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多 CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型

并发:用户线程和GC线程可以同时执行,如果发生GC,用户线程依然可以执行
并行:用户线程和GC线程可以同时执行,如果发生GC 用户线程会暂停

九、垃圾回收执行机制的分类

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC 有两种类型:Scavenge GC 和 Full GC。
Scavenge GC
一般情况下,当新对象生成,并且在 Eden 申请空间失败时,就会触发 Scavenge GC,对 Eden 区域进行 GC,清除非存活对象,并且把尚且存活的对象移动到 Survivor 区,然后整理 Survivor 的两个区。这种方式的 GC 是对年轻代的 Eden 区进行,不会影响到老年代。因为大部分对象都是从 Eden 区开始的,同时 Eden 区不会分配的很大,所以 Eden 区的 GC 会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使 Eden 去能尽快空闲出来。
Full GC
对所有年代进行整理,包括新生代、老年代和持久代。Full GC 因为需要对整个内存进行回收,所以比 Scavenge GC 要慢,因此应该尽可能减少 Full GC 的次数。在对 JVM 调优的过程中,很大一部分工作就是对于 Full GC 的调节。有如下原因可能导致 Full GC:
年老代被写满
持久代被写满
System.gc() 被显示调用
上一次 GC 之后堆中的各域分配策略动态变化

十、内存泄漏和内存溢出

内存泄露
指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即被分配的对象可达但已无用。
内存溢出
指程序运行过程中无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。

内存泄露的几种场景
1、长生命周期的对象持有短生命周期对象的引用
这是内存泄露最常见的场景,也是代码设计中经常出现的问题。
例如:在全局静态map中缓存局部变量,且没有清空操作,随着时间的推移,这个map会越来越大,造成内存泄露。
2、修改hashset中对象的参数值,且参数是计算哈希值的字段
当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中删除当前对象,造成内存泄露。
3、机器的连接数和关闭时间设置
长时间开启非常耗费资源的连接,也会造成内存泄露。

内存溢出的几种情况
堆内存溢出(outOfMemoryError:java heap space)
在jvm规范中,堆中的内存是用来生成对象实例和数组的。
如果细分,堆内存还可以分为年轻代和年老代,年轻代包括一个eden区和两个survivor区。
当生成新对象时,内存的申请过程如下:
a、jvm先尝试在eden区分配新建对象所需的内存;
b、如果内存大小足够,申请结束,否则下一步;
c、jvm启动youngGC,试图将eden区中不活跃的对象释放掉,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
d、Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
e、 当OLD区空间不够时,JVM会在OLD区进行full GC;
f、full GC后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”:outOfMemoryError:java heap space
方法区内存溢出(outOfMemoryError:permgem space)
在jvm规范中,方法区主要存放的是类信息、常量、静态变量等。
所以如果程序加载的类过多,或者使用反射、cglib等这种动态代理生成类的技术,就可能导致该区发生内存溢出,一般该区发生内存溢出时的错误信息为:outOfMemoryError:permgem space
线程栈溢出(java.lang.StackOverflowError)
线程栈时线程独有的一块内存结构,所以线程栈发生问题必定是某个线程运行时产生的错误。
一般线程栈溢出是由于递归太深或方法调用层级过多导致的。
发生栈溢出的错误信息为:java.lang.StackOverflowError

为了避免内存泄露,在编写代码的过程中可以参考下面的建议
1、尽早释放无用对象的引用
2、使用字符串处理,避免使用String,应大量使用StringBuffer,每一个String对象都得独立占用内存一块区域
3、尽量少用静态变量,因为静态变量存放在永久代(方法区),永久代基本不参与垃圾回收
4、避免在循环中创建对象
5、开启大型文件或从数据库一次拿了太多的数据很容易造成内存溢出,所以在这些地方要大概计算一下数据量的最大值是多少,并且设定所需最小及最大的内存空间值。

十一、工具

11.1、JPS
Java Virtual Machine Process Status Tool
-q
指定jps只输出进程ID ,不输出类的短名称
-m
输出传递给Java进程(主函数)的参数
-l
输出主函数的完整路径
-v
显示传递给JVM的参数

11.2、jinfo
Configuration Info
-flag
打印指定JVM的参数值
-flag [+|-]
设置指定JVM参数的布尔值
-flag =
设置指定JVM参数的值

11.3、jmap
Memory Map
-histo
生成Java应用程序的堆快照和对象的统计信息
-dump
Dump堆详细信息,可以用于分析OOM导致的原因
-heap
输出堆信息

11.4、jstack
Stack Trace
打印线程dump,发现线程目前停留在那行代码
-l
打印线程锁信息
-F
强制dump,当jstack没有响应时使用

11.5、jstat
-options
class (类加载器)
compiler (JIT)
gc (GC堆状态)
gccapacity (各区大小)
gccause (最近一次GC统计和原因)
gcnew (新生代统计)
gcnewcapacity (新生代大小)
gcold (老年代统计)
gcoldcapacity (老年代大小)
gcpermcapacity (永久区大小)
gcutil (GC统计汇总)
printcompilation (HotSpot编译统计)

jstat -gc : 可以显示gc的信息,查看gc的次数,及时间
JVM & JMM_第9张图片
jstat -gcutil :统计gc信息
JVM & JMM_第10张图片

11.6、jconsole
JVM & JMM_第11张图片
11.7、jvisualvm(最强大)
JVM & JMM_第12张图片

十二、使用jvisualvm通过JMX的方式远程监控JVM的运行情况

使用jvisualvm通过JMX的方式远程监控JVM的运行情况,步骤如下
远程服务器的配置
在启动java程序时加上如下几个参数
-Djava.rmi.server.hostname=ip(可加可不加,建议加)
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.port=22222
例如:
java -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.port=22222 -jar spider-robot.jar
这样,程序就在22222端口上打开了jmx,客户端可以通过jvisualvm来进行连接,下面来说一个客户端的配置。

客户端的配置
在文件菜单下打开“添加JMX连接”
JVM & JMM_第13张图片

在弹出的窗口中添加连接信息
JVM & JMM_第14张图片

在连接一栏中填入主机和端口信息
这里的主机是要程序运行的机器,这里我们要监控192.168.1.4上的程序
端口是上面启动时-Dcom.sun.management.jmxremote.port参数指定的端口
由于我们将-Dcom.sun.management.jmxremote.authenticate设置为了false,所以无需用户名和密码
点击确定

双击生成的JMX连接
JVM & JMM_第15张图片
JVM & JMM_第16张图片

等待连接完成,ok了

汇总:

1、根据文档和stackoverflow上的讨论,JVM 1.5以后的版本应该使用类似下面的命令(老的还是可以使用的):
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1234
dt_socket:使用的通信方式
server:是主动连接调试器还是作为服务器等待调试器连接
suspend:是否在启动JVM时就暂停,并等待调试器连接
address:地址和端口,地址可以省略,两者用冒号分隔

你可能感兴趣的:(java)