https://pdai.tech/md/java/jvm/java-jvm-class.html
https://www.wdbyte.com/java/jmx/
https://forum.butian.net/share/1232
https://www.cnblogs.com/lanxuezaipiao/p/3635556.html
https://blog.csdn.net/weixin\_40986713/article/details/131803534
整理学习前人知识,拾人牙慧
如阅读过程中有感到缺失或描述不清地方请看原始参考文章
class文件头魔数及版本号
常量池
访问标志
类索引、父索引、接口索引
字段表属性
方法表属性
属性表属性
文件开头的4个字节("cafe babe")称之为 魔数
后面0000为jdk编译器次版本号
后四位为主版本号
Classfile /E:/JavaCode/TestProj/out/production/TestProj/com/rhythm7/Main.class
//文件当前位置
Last modified 2018-4-7;
//最终修改时间
size 362 bytes
//文件大小
MD5 checksum 4aed8540b098992663b7ba08c65312de
//md5值
Compiled from "Main.java"
//编译自哪个文件
public class com.rhythm7.Main
//类的全限定名
minor version: 0
major version: 52
//编译器的主版本号和次版本号
flags: ACC_PUBLIC, ACC_SUPER
//类的访问标识
Constant pool:
//常量池(字面量+符号引用)
//符号引用:类和接口全限定名
//字段的名称和描述符号
//方法的名称和描述符
/*
JVM是在加载Class文件的时候才进行的动态链接
当虚拟机运行时,需要从常量池获得对应的符号引用
再在类创建或运行时解析并翻译到具体的内存地址中
*/
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V 方法定义
#2 = Fieldref #3.#19 // com/rhythm7/Main.m:I 声明int型变量m
#3 = Class #20 // com/rhythm7/Main
#4 = Class #21 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/rhythm7/Main;
#14 = Utf8 inc
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 Main.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 com/rhythm7/Main
#21 = Utf8 java/lang/Object
{
private int m;
descriptor: I
flags: ACC_PRIVATE
//方法表集合,对类内部的方法描述,私有变量m,类型int,返回int
public com.rhythm7.Main();
descriptor: ()V
flags: ACC_PUBLIC
//构造方法,属性public
Code:
stack=1,/*最大操作数栈*/
locals=1,//局部变量所需存储空间(单位slot-4字节)
args_size=1//(方法参数个数)
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
//方法体内容,0,1,4为字节码行号
LineNumberTable:
line 3: 0
//描述源码行号与字节码行号(字节码偏移量)之间的对应关系,可以使用 -g:none 或-g:lines选项来取消或要求生成这项信息,如果选择不生成LineNumberTable,当程序运行异常时将无法获取到发生异常的源码行号,也无法按照源码的行数来调试程序。
LocalVariableTable:
Start /*局部变量在哪一行开始可见*/ Length/*可见行数*/ Slot/*所在帧栈位置*/ Name /*变量名称*/ Signature/*类型签名*/
0 5 0 this
// 帧栈中局部变量与源码中定义的变量之间的关系.可以使用 -g:none 或 -g:vars来取消或生成这项信息,如果没有生成这项信息,那么当别人引用这个方法时,将无法获取到参数名称,取而代之的是arg0, arg1这样的占位符。
Lcom/rhythm7/Main;
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/rhythm7/Main;
}
SourceFile: "Main.java"
//类名
访问标识如下表
image-20230727143748956.png
字段类型如下表
image-20230727144315979.png
Javassist (JAVA programming ASSISTant) 是在 Java 中编辑字节码的类库;它使 Java 程序能够在运行时定义一个新类, 并在 JVM 加载时修改类文件
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();
反序列化gadget中使用,将我们打过去的字节码还原成类然后给classloader用于实例化
对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为,但是程序开始运行以后无法修改,只能用java agnet注入的方式修改
ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件
演示类
public class Base {
public void process(){
System.out.println("process");
}
}
ASM
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
public class Generator {
public static void main(String[] args) throws Exception {
//读取
ClassReader classReader = new ClassReader("meituan/bytecode/asm/Base");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//处理
ClassVisitor classVisitor = new MyClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
byte[] data = classWriter.toByteArray();
//输出
File f = new File("operation-server/target/classes/meituan/bytecode/asm/Base.class");
FileOutputStream fout = new FileOutputStream(f);
fout.write(data);
fout.close();
System.out.println("now generator cc success!!!!!");
}
}
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class MyClassVisitor extends ClassVisitor implements Opcodes {
public MyClassVisitor(ClassVisitor cv) {
super(ASM5, cv);
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
exceptions);
//Base类中有两个方法:无参构造以及process方法,这里不增强构造方法
if (!name.equals("<init>") && mv != null) {
mv = new MyMethodVisitor(mv);
}
return mv;
}
class MyMethodVisitor extends MethodVisitor implements Opcodes {
public MyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("start");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
|| opcode == Opcodes.ATHROW) {
//方法在返回之前,打印"end"
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("end");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
mv.visitInsn(opcode);
}
}
}
ClassReader:用于读取已经编译好的.class文件
ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件
各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等
ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中
ASM ByteCode Outline
安装后,右键选择“Show Bytecode Outline”,在新标签页中选择“ASMified”这个tab,就可以看到这个类中的代码对应的ASM写法
VMTI (JVM Tool Interface)是Java虚拟机对外提供的Native编程接口,通过JVMTI,外部进程可以获取到运行时JVM的诸多信息,比如线程、GC等。Agent是一个运行在目标JVM的特定程序,它的职责是负责从目标JVM中获取数据,然后将数据传递给外部进程。加载Agent的时机可以是目标JVM启动之时,也可以是在目标JVM运行时进行加载
JVMTI是一套Native接口,可以使用Java的Instrumentation接口(java.lang.instrument)来编写Agent
有权限启动jar,在目标JVM启动时加载
[1] public static void premain(String agentArgs, Instrumentation inst);
[2] public static void premain(String agentArgs);
在目标JVM运行时加载Agent
[1] public static void agentmain(String agentArgs, Instrumentation inst);
[2] public static void agentmain(String agentArgs);
Agent需要打包成一个jar包,在ManiFest属性中指定“Premain-Class”或者“Agent-Class”:
Premain-Class: class
Agent-Class: class
挂载到目标JVM
将编写的Agent打成jar包后,就可以挂载到目标JVM上去了。如果选择在目标JVM启动时加载Agent,则可以使用 “-javaagent:[=]“
使用com.sun.tools.attach.VirtualMachine进行动态挂载Agent
Instrumentation接口
http://itmyhome.com/java-api/java/lang/instrument/Instrumentation.html 接口文档
public interface Instrumentation
{
//添加ClassFileTransformer
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
//添加ClassFileTransformer
void addTransformer(ClassFileTransformer transformer);
//移除ClassFileTransformer
boolean removeTransformer(ClassFileTransformer transformer);
//是否可以被重新定义
boolean isRetransformClassesSupported();
//重新定义Class文件
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;
//是否可以修改Class文件
boolean isModifiableClass(Class<?> theClass);
//获取所有加载的Class
@SuppressWarnings("rawtypes")
Class[] getAllLoadedClasses();
//获取指定类加载器已经初始化的类
@SuppressWarnings("rawtypes")
Class[] getInitiatedClasses(ClassLoader loader);
//获取某个对象的大小
long getObjectSize(Object objectToSize);
//添加指定jar包到启动类加载器检索路径
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
//添加指定jar包到系统类加载检索路径
void appendToSystemClassLoaderSearch(JarFile jarfile);
//本地方法是否支持前缀
boolean isNativeMethodPrefixSupported();
//设置本地方法前缀,一般用于按前缀做匹配操作
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}
ClassFileTransformer接口
public interface ClassFileTransformer
{
byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException;
}
原文如下
https://www.yuque.com/tianxiadamutou/zcfd4v/tdvszq
首先利用 addTransformer 注册一个 transformer ,然后创建一个 ClassFileTransformer 抽象类的实现类,然后 override transform 方法
import java.lang.instrument.Instrumentation;
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation ins) {
ins.addTransformer(new DefineTransformer(),true);
}
}
在 transform 中定义自己的逻辑
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class DefineTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(className);
return classfileBuffer;
}
}
创建 jar 文件清单 agentmain.mf
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: AgentMain
打成jar包
jar cvfm AgentMain.jar agentmain.mf AgentMain.class DefineTransformer.class
attach进程
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class AgentMainDemo {
public static void main(String[] args) throws Exception{
String path = "AgentMain.jar的路径";
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor v:list){
System.out.println(v.displayName());
if (v.displayName().contains("AgentMainDemo")){
// 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
VirtualMachine vm = VirtualMachine.attach(v.id());
// 将我们的 agent.jar 发送给虚拟机
vm.loadAgent(path);
vm.detach();
}
}
}
}
所以需要我们上传jar包然后通过反序列化执行上面的代码,就可以获取到 jvm 的 pid 号之后,调用 loadAgent 方法将 agent.jar 注入进去
jvm-framework
线程私有:程序计数器、虚拟机栈、本地方法区
线程共享:堆、方法区, 堆外内存(Java7的永久代或JDK8的元空间、代码缓存)
JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟。
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。每个线程都分配了一个PC寄存器,每个线程都独立计算,不会互相影响.它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
可以通过参数-Xss
来设置线程的最大栈空间
栈不存在垃圾回收问题
Java 虚拟机规范允许 Java虚拟机栈的大小是动态的或者是固定不变的
如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 StackOverflowError 异常
如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个OutOfMemoryError异常
执行引擎运行的所有字节码指令只针对当前栈帧进行操作
如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前栈帧
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
执行方法->压栈->调用了其他方法->压栈执行->执行结束出栈->执行结束出栈
jvm-stack-frame
每个栈帧(Stack Frame)中存储着:
局部变量表(Local Variables)
操作数栈(Operand Stack)(或称为表达式栈)
动态链接(Dynamic Linking):指向运行时常量池的方法引用
方法返回地址(Return Address):方法正常退出或异常退出的地址
一些附加信息
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用
jvm-dynamic-linking
简单的讲,一个 Native Method 就是一个 Java 调用非 Java 代码的接口。我们知道的 Unsafe 类就有很多本地方法
与 Java 环境外交互:有时 Java 应用需要与 Java 外面的环境交互,这就是本地方法存在的原因。
与操作系统交互:JVM 支持 Java 语言本身和运行时库,但是有时仍需要依赖一些底层系统的支持。通过本地方法,我们可以实现用 Java 与实现了 jre 的底层系统交互, JVM 的一些部分就是 C 语言写的。
Sun's Java:Sun的解释器就是C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分都是用 Java 实现的,它也通过一些本地方法与外界交互。比如,类 java.lang.Thread
的 setPriority()
的方法是用Java 实现的,但它实现调用的是该类的本地方法 setPrioruty()
,该方法是C实现的,并被植入 JVM 内部。
本地方法栈也是线程私有的
本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区,它甚至可以直接使用本地处理器中的寄存器,直接从本地内存的堆中分配任意数量的内存
在 Hotspot JVM 中,直接将本地方法栈和虚拟机栈合二为一
虚拟机把堆内存逻辑上划分成三块区域(分代的唯一理由就是优化 GC 性能)
新生带(年轻代):新对象和没达到一定年龄的对象都在新生代
老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大
元空间(JDK1.8 之前叫永久代):像一些方法中的操作临时对象等,JDK1.8 之前是占用 JVM 内存,JDK1.8 之后直接使用物理内存
JDK7
-Xms
用来表示堆的起始内存,等价于 -XX:InitialHeapSize
默认情况下,初始堆内存大小为:电脑内存大小/64
-Xmx
用来表示堆的最大内存,等价于 -XX:MaxHeapSize
默认情况下,最大堆内存大小为:电脑内存大小/4
new 的对象先放在伊甸园区,此区有大小限制
当伊甸园的空间填满时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
然后将伊甸园中的剩余对象移动到幸存者 0 区
如果再次触发垃圾回收,此时上次幸存下来的放到幸存者 0 区,如果没有回收,就会放到幸存者 1 区
如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区
什么时候才会去养老区呢? 默认是 15 次回收标记
在养老区,相对悠闲。当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理
若养老区执行了 Major GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常
方法区(Method Area)与 Java 堆一样,是所有线程共享的内存区域。
方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。
虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫 Non-Heap(非堆),目的应该是与 Java 堆区分
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本/字段/方法/接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将类在加载后进入方法区的运行时常量池中存放。运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的是 String.intern()
方法。受方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryErro
r 异常
**方法区(method area)*只是 **JVM 规范**中定义的一个*概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,并没有规定如何去实现它,不同的厂商有不同的实现
永久代(PermGen)**是 **Hotspot** 虚拟机特有的概念, Java8 的时候又被**元空间取代了
永久代物理是堆的一部分,和新生代,老年代地址是连续的(受垃圾回收器管理),而元空间存在于本地内存(我们常说的堆外内存,不受垃圾回收器管理),这样就不受 JVM 限制了,也比较难发生OOM(都会有溢出异常)
java-memory-model-3 1.png
img
Java 程序的内存可见性保证按程序类型可以分为下列三类:
单线程程序。单线程程序不会出现内存可见性问题。编译器,runtime 和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。
正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是 JMM 关注的重点,JMM 通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。
未同步 / 未正确同步的多线程程序。JMM 为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)。
https://pdai.tech/md/java/jvm/java-jvm-jmm.html
详细内容在这里,建议用到条件竞争等涉及多线程内存读写相关的漏洞或者JVM性能优化相关的内容再看,不然看过也会忘。
使用可达性分析算法,通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收
虚拟机栈中引用的对象
本地方法栈中引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象
方法区的回收主要是对常量池的回收和对类的卸载
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
加载该类的 ClassLoader 已经被回收。
该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法
JVM 在进行 GC 时,并非每次都对堆内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。
针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:部分收集(Partial GC),整堆收集(Full GC)
部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:
目前只有 G1 GC 会有这种行为
目前,只有 CMS GC 会有单独收集老年代的行为
很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收
新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
老年代收集(Major GC/Old GC):只是老年代的垃圾收集
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC
Full GC 的触发条件
调用 System.gc()
老年代空间不足、Concurrent Mode Failure(执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足)
空间分配担保失败
垃圾收集器
加参数-XX:NativeMemoryTracking=detailJVM,使用命令 jcmd pid VM.native_memory detail
Thread Dump
Java 调试入门工具
jps
jstack
jinfo
jmap
jstat
jdb
CHLSDB
Java 调试进阶工具
btrace
Greys
Arthas
javOSize
JProfiler
其它工具
dmesg
JConsole
Visual VM
Visual GC
JProfile
Java 平台调试体系(Java Platform Debugger Architecture,JPDA)
层级由低到高分别是
java虚拟机工具接口(JVMTI)
Java 调试连接协议(JDWP)
Java 调试接口(JDI)
java -Xdebug -Xrunjdwp:transport=dt_shmem,address=debug,server=y,suspend=y com.xxx.Test
JDWP是用于规范调试器(Debugger)与目标 JVM 之间通信的协议
JDWP 只规定了具体的格式和布局,而不管你用什么协议来传输数据
连接建立之后,在发送其他数据包之前,连接双方需要进行握手:
握手过程包括以下步骤:
Debugger 端向目标 JVM 发送 14 个字节,也就是包括 14 个 ASCII 字符的字符串 "JDWP-Handshake"。
VM 端以相同的 14 个字节答复:JDWP-Handshake。
JDWP 是无状态的协议,JDWP 是异步的,命令包和应答包的 header 大小相等
命令包
length(4 bytes)
id(4 bytes)
flags(1 byte)
command set(1 byte)
command(1 byte)该字段用于标识命令集中的具体命令
0-63:发给目标 VM 的命令集
64-127:发送给调试器的命令集
128-256:JVM 提供商自己定义的命令和扩展。
调试器可以用命令包来从目标 VM 请求相关信息或者控制程序的执行
目标 VM 可以将自身的某些事件(例如断点或异常)用命令数据包的方式通知调试器
Header
data(长度不固定)
应答包
jdb等工具或脚本连接端口(未授权)然后通过反射调用java代码,执行命令
length(4 bytes)
id(4 bytes)应答包 id 值必须与对应的命令包 ID 相同,id 的取值允许 2^32 个数据包
flags(1 byte)用于修改命令的排队和处理方式,也用来标记源自 JVM 的数据包
error code(2 bytes)标识是否成功处理了对应的命令包。0 值表示成功,非零值表示错误
应答包仅用于对命令包进行响应,并且标明该命令是成功还是失败
应答包还可以携带命令中请求的数据(例如字段或变量的值)
Header
data(Variable)
Java代码和其他语言(尤其C/C++)写的代码进行交互,只要遵守调用约定即可
311340147974036.png
如果使用JNI技 术调用,我们首先需要使用C语言另外写一个.dll/.so共享库,使用SUN规定的数据结构替代C语言的数据结构,调用已有的 dll/so中公布的函 数。然后再在Java中载入这个库dll/so,最后编写Java native函数作为链接库中函数的代理。经过这些繁琐的步骤才能在Java中调用 本地代码
java创建类
package com.test;
public class GetPidJni {
public static native long getpid();
static {
System.loadLibrary("getpidjni");
}
public static void main(String[] args) {
System.out.println(getpid());
}
}
javac
编译代码 GetPidJNI.java
,然后用 javah
生成 JNI 头文件
$ mkdir -p target/classes
$ javac src/main/java/com/test/GetPidJni.java -d "target/classes"
$ javah -cp "target/classes" com.test.GetPidJni
头文件如下
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_test_GetPidJni */
#ifndef _Included_com_test_GetPidJni
#define _Included_com_test_GetPidJni
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_test_GetPidJni
* Method: getpid
* Signature: ()J
*/
JNIEXPORT jlong JNICALL Java_com_test_GetPidJni_getpid
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
实现头文件
#include "com_test_GetPidJni.h"
JNIEXPORT jlong JNICALL
Java_com_test_GetPidJni_getpid (JNIEnv * env, jclass c) {
return getpid();
}
编译 com_test_GetPidJni.c
,生成 libgetpidjni.dylib
$ gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin -dynamiclib -o libgetpidjni.dylib com_test_GetPidJni.c
运行 GetPidJni
类
$ java -Djava.library.path=`pwd` -cp "target/classes" com.test.GetPidJni
311340321101993.png
可以看到使用jna步骤减少了很多,最重要的是我们不需要重写我们的动态链接库文件,而是有直接调用的API
NA只需要我们写Java代码而不用写JNI或本地代码。功能相对于Windows的Platform/Invoke和Python的ctypes
JNA使用一个小型的JNI库插桩程序来动态调用本地代码
JNA包括一个已与许多本地函数映射的平台库,以及一组简化本地访问的公用接口
JNA把一个.dll/.so文件看做是一个Java接口
package com.sun.jna.examples;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;
public class HelloWorld {
public interface CLibrary extends Library {
//需要定义一个接口,继承自Library或StdCallLibrary
CLibrary INSTANCE = (CLibrary)
Native.loadLibrary((Platform.isWindows() ? "msvcrt" : "c"),
CLibrary.class);
//接口内部需要一个公共静态常量:INSTANCE,通过这个常量,就可以获得这个接口的实例.常量通过Native.loadLibrary()这个API函数获得.第一个参数是动态链接库dll/so的名称,第二个参数是本接口的Class类型。JNA通过这个Class类型,根据指定的.dll/.so文件,动态创建接口的实例
/*搜索动态链 接库路径的顺序是:先从当前类的当前文件夹找,如果没有找到,再在工程当前文件夹下面找win32/win64文件夹,找到后搜索对应的dll文件,如果 找不到再到WINDOWS下面去搜索,再找不到就会抛异常*/
void printf(String format, Object... args);
}
//接口中只需要定义你要用到的函数或者公共变量,不需要的可以不定义
public static void main(String[] args) {
CLibrary.INSTANCE.printf("Hello, World\n");
for (int i=0;i < args.length;i++) {
CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
}
}
}
Java中是没有char *指针类型的,因此const char *转到Java下就是String类型
JNA是不能完全替代JNI的
JNI技术,不仅可以实现Java访问C函数,也可以实现C语言调用Java代码,而JNA只能实现Java访问C函数。
添加依赖
<dependency>
<groupId>com.github.jnr</groupId>
<artifactId>jnr-ffi</artifactId>
<version>2.1.10</version>
</dependency>
创建本地库接口
import com.github.jnr.ffi.LibraryLoader;
import com.github.jnr.ffi.NativeLibrary;
public interface MyNativeLibrary extends NativeLibrary {
MyNativeLibrary INSTANCE = LibraryLoader.create(MyNativeLibrary.class).load("mylibrary");
int add(int a, int b);
}
加载本地类库
MyNativeLibrary nativeLibrary = MyNativeLibrary.INSTANCE;
调用本地函数
int result = nativeLibrary.add(10, 20);
System.out.println("Result: " + result);
处理异常
try {
int result = nativeLibrary.add(10, 20);
System.out.println("Result: " + result);
} catch (Throwable t) {
t.printStackTrace();
}
效率JNI>JNR>JNA
简单来说jmx是一个管理MBean的规范,各种组件实现了各自的JMX实现,可以用来监控管理我们的指定的java程序
首先定义一个MBean接口和实现他的类
package com.wdbyte.jmx;
/**
* @author https://www.wdbyte.com
*/
public interface MyMemoryMBean {
long getTotal();
void setTotal(long total);
long getUsed();
void setUsed(long used);
String doMemoryInfo();
}
public class MyMemory implements MyMemoryMBean {
private long total;
private long used;
@Override
public long getTotal() {
return total;
}
@Override
public void setTotal(long total) {
this.total = total;
}
@Override
public long getUsed() {
return used;
}
@Override
public void setUsed(long used) {
this.used = used;
}
@Override
public String doMemoryInfo() {
return String.format("使用内存: %dMB/%dMB", used, total);
}
}
然后向MBeanSearver注册
import java.lang.management.ManagementFactory;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
/**
* @author https://www.wdbyte.com
*/
public class MyMemoryManagement {
public static void main(String[] args) throws MalformedObjectNameException, NotCompliantMBeanException,
InstanceAlreadyExistsException, MBeanRegistrationException, InterruptedException {
// 获取 MBean Server
MBeanServer platformMBeanServer = ManagementFactory.getPlatformMBeanServer();
MyMemory myMemory = new MyMemory();
myMemory.setTotal(100L);
myMemory.setUsed(20L);
// 注册
ObjectName objectName = new ObjectName("com.wdbyte.jmx:type=myMemory");
platformMBeanServer.registerMBean(myMemory, objectName);
while (true) {
// 防止进行退出
Thread.sleep(3000);
System.out.println(myMemory.doMemoryInfo());
}
}
}
对于管理系统来讲,这些MBean中公开的方法,最终会被JMX转换为属性(Attribute)、监听(Listener)和调用(Invoke)的概念
资源接口
管理的资源
Object Name
VM 中的实例个数
ClassLoadingMXBean
类加载
java.lang:type= ClassLoading
1 个
CompilationMXBean
汇编系统
java.lang:type= Compilation
0 个或 1 个
GarbageCollectorMXBean
垃圾收集
java.lang:type= GarbageCollector, name=collectorName
1 个或更多
LoggingMXBean
日志系统
java.util.logging:type =Logging
1 个
MemoryManagerMXBean
内存池
java.lang: typeMemoryManager, name=managerName
1 个或更多
MemoryPoolMXBean
内存
java.lang: type= MemoryPool, name=poolName
1 个或更多
MemoryMXBean
内存系统
java.lang:type= Memory
1 个
OperatingSystemMXBean
操作系统
java.lang:type= OperatingSystem
1 个
RuntimeMXBean
运行时系统
java.lang:type= Runtime
1 个
ThreadMXBean
线程系统
java.lang:type= Threading
1 个
zabbix和jconsole都是通过jmx进行性能监控
特殊的MBean是MLet,可以通过getMBeanFromURL远程加载恶意Mbean
漏洞利用方式如下