解密java虚拟机
解密java虚拟机
1.基础概念
jdk:所有工具得集合,即一个工具包。
jre:java的运行环境,比如给jre一个.class文件或者jar就可以运行起来。
jvm:都知道机器只识别0101这种机器码,jvm的作用就是将.class文件转换成机器码。这样可以运行在不同的
(windows、linux等系统),形成了java语言跨平台的特点。
2.了解jvm的工作
jvm的工作可以简单解释为将.class文件通过类加载,加载到运行时数据区即内存中来,再交由执行引擎来转换成
字节码。由此可以看出运行时数据区是很重要的一部分,下部分介绍该内容。顺便写下反编译的命令:javap -v [.class文件] > [xx.txt ]
3.运行时数据区(内存 线程私有)
jvm运行时会把它的内存划分成不同的数据区域,主要分两类
线程私有:程序计数器、本地方法栈、虚拟机栈
线程共享:堆、方法区
因为java是多线程的所以每个线程都会拥有自己的程序计数器、本地方法栈、虚拟机栈,而共享一个堆和方
法区。问题:为什么要区分私有和共享呢?一个程序的完成是否可以简单理解为通过指令和数据来完成呢,
那么 此时线程私有的内存即就是存放的指令,共享的即存放的数据。下面解释下各个区域:
**栈**
先说栈的概念:栈即一种数据结构,特点是先进后出FILO,代码一显,其意自现了
其中A现入栈–>B入栈–>C入栈,此时C先出栈然后B出栈,最后C,很好的证明了FILO。
(1)程序计数器
每个线程都有自己的程序计数器,来指向当前线程正在执行的字节码指令的地址(行号)
具体执行指令的是我们的cpu,但是线程远比cpu多,此时需要线程来回切换。为了确保多线
程环境下程序的正常运行,需要一个程序计数器来确保正常切换。
(2)虚拟机栈
存储当前线程运行方法所需的数据、指令、返回地址。一个虚拟机栈包含多个栈帧,而每个
栈帧对应一个方法。栈帧可以划分为:局部变量表、操作数栈、动态连接、返回地址。
下面用一段代码和一张图来解释下
public class StackTest {
/**
*常量
*/
final String CH="常量";
/**
* 静态变量
*/
static String JT="静态变量";
/**
* 大哥钱包
*/
int bigmoney=10000;
/**
*大哥二弟 一块吃饭 这个吃饭方法即对应一个栈帧
*/
public void eat(){
//二弟钱包
int smallMoney=1000;
//吃饭
Object eatTogether=new Object();
eatTogether.hashCode();
//大哥出200
bigmoney=bigmoney-200;
//二弟出100
smallMoney=smallMoney-100;
}
public static void main(String[] args){
StackTest stackTest=new StackTest();
stackTest.eat();
}
}
我们将这段代码进行反编译下:javap -v .\StackTest.class > stackTest.txt
Classfile /C:/Users/qsy/Desktop/jvm_demo/target/classes/com/qin/jvm_demo/demo1/StackTest.class
Last modified 2019-5-15; size 915 bytes
MD5 checksum e2bdd750191edb46d502319744899fb8
Compiled from "StackTest.java"
public class com.qin.jvm_demo.demo1.StackTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#37 // java/lang/Object."<init>":()V
#2 = String #38 // 常量
#3 = Fieldref #7.#39 // com/qin/jvm_demo/demo1/StackTest.CH:Ljava/lang/String;
#4 = Fieldref #7.#40 // com/qin/jvm_demo/demo1/StackTest.bigmoney:I
#5 = Class #41 // java/lang/Object
#6 = Methodref #5.#42 // java/lang/Object.hashCode:()I
#7 = Class #43 // com/qin/jvm_demo/demo1/StackTest
#8 = Methodref #7.#37 // com/qin/jvm_demo/demo1/StackTest."<init>":()V
#9 = Methodref #7.#44 // com/qin/jvm_demo/demo1/StackTest.eat:()V
#10 = String #45 // 静态变量
#11 = Fieldref #7.#46 // com/qin/jvm_demo/demo1/StackTest.JT:Ljava/lang/String;
#12 = Utf8 CH
#13 = Utf8 Ljava/lang/String;
#14 = Utf8 ConstantValue
#15 = Utf8 JT
#16 = Utf8 bigmoney
#17 = Utf8 I
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lcom/qin/jvm_demo/demo1/StackTest;
#25 = Utf8 eat
#26 = Utf8 smallMoney
#27 = Utf8 eatTogether
#28 = Utf8 Ljava/lang/Object;
#29 = Utf8 main
#30 = Utf8 ([Ljava/lang/String;)V
#31 = Utf8 args
#32 = Utf8 [Ljava/lang/String;
#33 = Utf8 stackTest
#34 = Utf8 <clinit>
#35 = Utf8 SourceFile
#36 = Utf8 StackTest.java
#37 = NameAndType #18:#19 // "<init>":()V
#38 = Utf8 常量
#39 = NameAndType #12:#13 // CH:Ljava/lang/String;
#40 = NameAndType #16:#17 // bigmoney:I
#41 = Utf8 java/lang/Object
#42 = NameAndType #47:#48 // hashCode:()I
#43 = Utf8 com/qin/jvm_demo/demo1/StackTest
#44 = NameAndType #25:#19 // eat:()V
#45 = Utf8 静态变量
#46 = NameAndType #15:#13 // JT:Ljava/lang/String;
#47 = Utf8 hashCode
#48 = Utf8 ()I
{
final java.lang.String CH;
descriptor: Ljava/lang/String;
flags: ACC_FINAL
ConstantValue: String 常量
static java.lang.String JT;
descriptor: Ljava/lang/String;
flags: ACC_STATIC
int bigmoney;
descriptor: I
flags:
public com.qin.jvm_demo.demo1.StackTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String 常量
7: putfield #3 // Field CH:Ljava/lang/String;
10: aload_0
11: sipush 10000
14: putfield #4 // Field bigmoney:I
17: return
LineNumberTable:
line 3: 0
line 6: 4
line 11: 10
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 this Lcom/qin/jvm_demo/demo1/StackTest;
public void eat();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: sipush 1000 //将一个short型常量值推送至栈顶
3: istore_1 //将栈顶int型数值存入第二个局部变量
4: new #5 //创建一个对象,并且其引用进栈
7: dup //复制栈顶数值,并且复制值进栈
8: invokespecial #1 //调用超类构造方法、实例初始化方法、私有方法
11: astore_2
12: aload_2
13: invokevirtual #6 // Method java/lang/Object.hashCode:()I
16: pop
17: aload_0
18: aload_0
19: getfield #4 // Field bigmoney:I
22: sipush 200
25: isub
26: putfield #4 // Field bigmoney:I
29: iload_1
30: bipush 100
32: isub
33: istore_1
34: return
LineNumberTable:
line 16: 0
line 19: 4
line 20: 12
line 23: 17
line 26: 29
line 28: 34
LocalVariableTable:
Start Length Slot Name Signature
0 35 0 this Lcom/qin/jvm_demo/demo1/StackTest;
4 31 1 smallMoney I
12 23 2 eatTogether Ljava/lang/Object;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #7 // class com/qin/jvm_demo/demo1/StackTest
3: dup
4: invokespecial #8 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #9 // Method eat:()V
12: return
LineNumberTable:
line 33: 0
line 34: 8
line 35: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 args [Ljava/lang/String;
8 5 1 stackTest Lcom/qin/jvm_demo/demo1/StackTest;
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #10 // String 静态变量
2: putstatic #11 // Field JT:Ljava/lang/String;
5: return
LineNumberTable:
line 8: 0
}
SourceFile: "StackTest.java"
对照java反编译指令集我们可以大概的了解到一个方法的运行就是一个入栈出栈的过程,其中操作数栈即对应比如(
stack=3, locals=3, args_size=1
0: sipush 1000 //将一个short型常量值推送至栈顶
3: istore_1 //将栈顶int型数值存入第二个局部变量
4: new #5 //创建一个对象,并且其引用进栈
7: dup //复制栈顶数值,并且复制值进栈
8: invokespecial #1 //调用超类构造方法、实例初始化方法、私有方法
),其那边的数字即使程序计数器。
局部变量表对应的是:
LocalVariableTable:
Start Length Slot Name Signature
0 35 0 this Lcom/qin/jvm_demo/demo1/StackTest; //this当前对象
4 31 1 smallMoney I //方法内的局部变量
12 23 2 eatTogether Ljava/lang/Object; //该对象的引用地址 实际是存放在堆中
对应图解:
虚拟机栈设置大小参数默认为:-Xss1M
对于虚拟机栈容易抛出 java.lang.*Error,比如递归调用时(原因:递归调用时,每
个方法相当于一个栈帧,递归不断调用方法,则会不断的再虚拟机栈中执行入栈操作,当达到
虚拟机栈内存大小时,则会内存溢出),此时把-Xss值调大是否会避免异常呢?不会只是调用
方法的次数增多了。
如何优化呢?
将虚拟机栈进行调小,解决排查递归调用的代码。
4.运行时数据区(内存 线程共享)
(1)方法区:
类信息
常量
静态变量
即时编译期编译后的代码
(2)堆:(大小设置的参数 -Xmx 堆内存可被分配的最大上限 -Xms堆初始化内存分配的大小)
对象实例
数组
***JMM*********************************************************************************************
jvm的内存模型:
堆:
新生代:
Eden空间
From Survivor空间
To Survivor空间
老年代
方法区:
永久代(JDK<1.8)
元空间(JDK>=1.8)
JMM的对象内存分配
*对象优先再Eden分配 (垃圾回收期会判断对象是否存活,存活对象会在from、to中来回切换,并年龄增长
,默认年龄为15,当超过这个年龄直接进入老年代)
*长期存活的对象进入老年代
*大对象直接进入老年代 (堆中新生代和老年代默认内存比例3/1和3/2,而新生代中Eden、from、to的比例为
8:1:1,此时假设-Xmx为300M,新生代100M,Eden80M,From10M,to10M,突然来了来了一个对象120M
远超过80M,Eden装不下直接塞进了老年代)
*动态对象年龄判定
***JMM****************************************************************************************************
***对象的回收*********************************************************************************************
如何判断对象的存活?
*引用计数算法
*可达性分析
在java中可作为GC Root 的对象包括:
1.方法区:类静态属性引用的对象
2.方法区:常量引用的对象
3.虚拟机栈:本地变量表中的对象
4.本地方法栈:JNInative中引用的对象
JVM中的垃圾回收
*新生代:
Minor GC 对应复制回收算法
触发:Eden大小为80M,其中Object1=50M,Object=25M,现在有一个新对象10M,此时Eden内存大
小不足即有可能触发Minor GC。
复制回收算法:
把内存划分成等额两块,把存活对象复制到预留空间,再将需要回收的区域进行垃圾回收,速度快,但
是浪费空间
Eden:From:To为8:1:1因为90%的对象是不需要垃圾回收的,10%的对象需要垃圾回收,所以预留10%
作为复制回收的算法的另一半。
*老年代
Full GC 全盘扫描
标记清除算法
内存碎片
标记整理算法
*永久代
***对象的回收*********************************************************************************************
5.JVM常见问题处理
*保存堆栈快照日志
*分析内存泄露
*调整内存设置
*控制垃圾回收的频率
*选择合适的垃圾回收器
OOM\栈溢出 :
(1) 保存堆(内存泄漏:比如某一区域100m,某一对象在方法去引用某一常量,但是只使用了一次,可达性分
析算法会任务此对想有引用不可回收,所以每次可以使用的只有90m ,n多次垃圾回收后,抛出了内存泄漏)
、栈(死循环、递归)日志
//OutOfMemoryError :内存溢出
List list=new ArrayList();
while (true){
list.add(new Object());
}
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
//OutOfMemoryError :内存溢出 -Xmx5m -Xms5m 此时对的大小为5m
String[] array=new String[100*1000*1000];
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space