应用启停脚本
以SpringBoot 应用为例子
startup.sh
1 | #!/bin/sh |
shutdown.sh
1 | #!/bin/bash |
以SpringBoot 应用为例子
1 | #!/bin/sh |
1 | #!/bin/bash |
sun.misc.Unsafe
简介sun.misc.Unsafe
是 Java 中的一个类,位于 sun.misc
包下。它提供了一组低级别的、不安全的操作,允许开发人员绕过 Java 的安全机制和内存管理,直接操作内存、对象、线程等。这使得它在某些高性能、低级别的编程中非常有用,但也非常危险。
1 | long memoryAddress = unsafe.allocateMemory(size); |
1 | SomeClass instance = (SomeClass) unsafe.allocateInstance(SomeClass.class); |
1 | Field field = SomeClass.class.getDeclaredField("someField"); |
1 | boolean success = unsafe.compareAndSwapInt(someObject, offset, expectedValue, newValue); |
1 | unsafe.fullFence(); |
以下是一个简单示例,展示如何使用 sun.misc.Unsafe 来分配和访问本机内存:
1 | import sun.misc.Unsafe; |
使用 sun.misc.Unsafe
是危险的,因为它绕过了 Java 的类型安全检查和内存管理机制。如果使用不当,可能会导致严重的错误,如内存泄漏、数据损坏或程序崩溃。并且,由于它不是 Java 标准 API 的一部分,在不同的 Java 版本和运行环境中可能会有不同的表现,甚至可能不被支持。
http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
1 |
|
Fastdfs 问题分析记录
生产上存在上传文件到fastdfs中时一直卡死的问题
大概半个月出现一次,重启就能恢复
日志中没有任何错误
线程堆栈:
1 | "taskExecutor-5" #638 prio=5 os_prio=0 tid=0x00007f970800b000 nid=0x1be3 waiting on condition [0x00007f9632008000] |
只知道是在等一个CompletableFuture异步计算的返回结果,可能是再等待获取存储服务器地址,也可能是再等待上传结果
检查代码并进行测试,没有发现问题
猜测可能是获取不到连接导致的,连接没有归还到连接池中等,也可能是fastdfs一直没有响应,或者网络出现了什么问题等。
直到生产上再次出现该问题
将生产上的程序堆栈dump出来查看对比之前看的代码,发现是获取的连接数达到了最大值
1 | jmap -dump:format=b,file=文件名 [pid] |
而连接池中并没连接
OQL
1 | select x.deque from io.netty.channel.pool.FixedChannelPool x |
FixedChannelPool
类的 acquiredChannelCount
字段
一直增加到了最大的限制(100)导致的
获取到的连接一直没有释放
查代码,并且重点测试这个变量,一直无法重现该问题(怀疑人生_)
导出测试程序的dump文件和线上的进行比较
对比了一下测试版本和线上版本的dump文件,发现类不一致
线上版本没有这个变量
检查发现 调试版本 和 线上的版本 不一致
调试版本:netty-all-4.0.34.Final.jar
线上版本:netty-all-4.0.30.Final.jar
再次查看netty-all-4.0.30的代码,发现netty的连接池存在问题
检查代码发现连接归还到连接池中再次获取时存在问题
当连接用完之后,归还到连接池中,过了一段时间之后连接变成了 TIME_WAIT 状态(或断开),再去获取连接(此时将获取到一个异常的连接)
获取到连接之后会对连接的有效性进行检查
检查到连接异常之后会关掉这个连接并再次获取连接
此时会再调用FixedChannelPool类的acquire0
方法获取连接,这将导致acquiredChannelCount 变量再次 +1,这次获取连接将会+2,而在释放时只会-1,这将导致这个值一直增大,直到达到maxConnections 的限制,之后就再也获取不到连接了
释放连接时只会-1
升级netty的包到更高的版本
netty-all-4.0.30.Final.jar 包存在这个问题
netty-all-4.0.34.Final.jar 已经修复了这个问题
PS:
这个4.0.34版本还是可能会有问题
private int acquiredChannelCount;
这个变量是int类型,存在并发问题,可能多个线程同时修改这个变量,应该用AtomicInteger,但是以目前系统的并发量来看,完全可以忽略。
想要加密java程序是比较困难的,一般的做法就是代码混淆,增加代码阅读的难度,另外就是修改Class文件的字节码,然后再虚拟机加载类的时候进行解密。修改字节码的解密方法一般是修改JDK中的ClassLoad或者自定义一个ClassLoad来解密Class。有些为了不让别人看到解密算法有些还会用C来写解密程序,再用jni来调用。
一般用改ClassLoader来加密的这种方法都可以用 javaagent 获取到解密后class。
使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。
介绍地址:
https://www.ibm.com/developerworks/cn/java/j-lo-jse61/
如:class-agent
PreMainExecutor.java
1 | package com.wwh.agent; |
PrintClassFileAgent.java
1 | package com.wwh.agent; |
用于指定:Premain-Class
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
将上面的maven项目编译打包,将 class-agent-0.0.1-SNAPSHOT.jar 复制到目标位置。
用如下命令启动想要破解的程序:
1 | java -javaagent:class-agent-0.0.1-SNAPSHOT.jar -cp runner-0.0.1-SNAPSHOT.jar com.wwh.runner.EmptyRunner |
如果需要破解的程序本身就有使用javaagent,需要将上面的agent放到调用链的最后面。
1 | java -javaagent:agentA.jar -javaagent:class-agent-0.0.1-SNAPSHOT.jar XXProgram |
将在目录下保存所有加载的类的class文件
1 | E:\opt\logs\wwh\classFile>tree |
instrument 规范
https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html?is-external=true
Class VirtualMachine
Interface ClassFileTransformer
https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/ClassFileTransformer.html
代理需要编译成jar包的方式来运行,JAR文件manifest中的属性指定将加载哪个代理类来启动代理。
有下面两种方式可以启动一个agent
程序没有启动时可以通过在命令行上指定javaagent的方式来启动代理
1 | -javaagent:jarpath[=options] |
通过命令行的方式可以指定多个代理,并且支持参数。初始化Java虚拟机(JVM)之后,将按照指定代理的顺序调用每个premain方法,然后调用真正的应用程序main方法。每个premain方法必须返回,以便继续启动程序。
agent Jar包中的manifest文件必须包含Premain-Class, 指向代理的入口类,这个类中包含了一个公共静态的premain方法
premain 方法有两种签名,虚拟机会尝试先运行下面这个
1 | public static void premain(String agentArgs, Instrumentation inst); |
如果没有会尝试调用下面这个
1 | public static void premain(String agentArgs); |
当使用命令行选项启动代理时,不会调用agentmain方法。
代理类将由系统类加载器加载(参见ClassLoader.getSystemClassLoader)。这是类加载器,它通常加载包含应用程序主方法的类。premain方法将在与应用程序main方法相同的安全性和类加载器规则下运行。对于代理premain方法可以做什么,没有建模限制。application main可以做的任何事情,包括创建线程,都是合法的。
每个代理都通过agentArgs参数传递其代理选项。代理选项作为单个字符串传递,任何额外的解析都应该由代理本身执行。
程序已经启动后可以通过VirtualMachine 来加载启动代理,如下:
1 | VirtualMachine vm = VirtualMachine.attach("2177"); |
注意:
启动代理时先尝试运行
1 | public static void agentmain(String agentArgs, Instrumentation inst); |
找不到再尝试运行
1 | public static void agentmain(String agentArgs); |
agentmain方法不能阻塞,这个类同用可以拥有 premain 方法,不过并不会被调用
参数通过如下方式指定:
1 | vm.loadAgent(jar, options); |
代理JAR文件定义了以下清单属性:
代理JAR文件可同时具有清单中的Premain-Class和Agent-Class属性。当使用-javaagent选项在命令行上启动代理时,执行Premain-Class属性指定的
代理类,而Agent-Class属性将被忽略。类似地,如果代理在VM启动之后再启动,则执行Agent-Class属性指定的代理类,而忽略Premain-Class属性的值。
几个关键类和接口
表示一个java虚拟机
VirtualMachine表示已附加到的Java虚拟机。它所附加的Java虚拟机有时称为目标虚拟机或目标VM。应用程序(通常是managemet控制台或分析器之类的工具)使用虚拟机将代理加载到目标VM中。例如,用Java语言编写的分析器工具可能附加到正在运行的应用程序,并加载其分析器代理来分析正在运行的应用程序。
通过调用带有标识目标虚拟机的标识符的attach方法来获得虚拟机。标识符依赖于实现,但在每个Java虚拟机都在自己的操作系统进程中运行的环境中,标识符通常是进程标识符(或pid)。另外,通过使用从list方法返回的虚拟机描述符列表中获得的VirtualMachineDescriptor调用attach方法来获得虚拟机实例。一旦获得对虚拟机的引用,就使用loadAgent、loadAgentLibrary和loadAgentPath方法将代理加载到目标虚拟机中。loadAgent方法用于加载用Java语言编写并部署在JAR文件中的代理。loadAgentLibrary和loadAgentPath方法用于加载部署在动态库或静态链接到VM并使用JVM工具接口的代理。
除了加载代理之外,虚拟机还提供对目标VM中的系统属性的读访问。这在某些环境中非常有用,比如java。home、os.name或os。arch用于构造将加载到目标VM的代理的路径。
一个启动jmx的例子
1 | // attach to target VM |
虚拟机对于多个并发线程的使用是安全的。
代理提供此接口的实现,以便转换类文件。转换发生在JVM定义类之前。
一个代理提供者需要实现:ClassFileTransformer 接口,来转变class文件,这个接口有一个方法
1 | byte[] |
此方法的实现可以转换提供的类文件并返回一个新的替换类文件
一旦向addTransformer注册了一个transformer,每个新的类定义和每个类重定义都会调用这个transformer。每个类重新转换时也将调用具有重新转换能力的转换器。
对新类定义的请求是用 ClassLoader.defineClass 或本地调用触发的。
对类的重定义请求是通过 Instrumentation.redefineClasses 或本地调用触发的。
对类重新转换的请求是通过 Instrumentation.retransformClasses 或本地调用触发的。
在处理请求期间,在类文件字节被验证或应用之前调用转换器。
当有多个转换器时,转换由链接转换调用组成。也就是说,一个转换调用返回的字节数组将成为下一个调用的输入(通过classfileBuffer参数)。
关于transform输入的classfileBuffer参数:
如果实现方法确定不需要转换,则返回null。否则,它应该创建一个新的byte[]数组,将输入classfileBuffer连同所有所需的转换复制到其中,并返回新数组。不能修改输入classfileBuffer。
在重新转换和重新定义的情况下,转换器必须支持重新定义语义:如果转换器在初始定义期间更改的类稍后被重新转换或重新定义,转换器必须确保第二个类输出类文件是第一个输出类文件的合法重新定义。
如果transformer抛出异常(它没有捕获异常),后续的transformer仍然会被调用,并且负载、重新定义或重新转换仍然会被尝试。因此,抛出异常的效果与返回null相同。为了防止在transformer代码中生成未检查异常时出现意外行为,transformer可以捕获Throwable。如果转换器认为classFileBuffer不代表一个有效格式化的类文件,它应该抛出一个IllegalClassFormatException;而这与返回null具有相同的效果。它有助于记录或调试格式错误。
参数说明:
返回:
格式良好的类文件缓冲区(转换的结果),如果没有执行转换,则为null。
该类提供测试Java编程语言代码所需的服务。插装是将字节码添加到方法中,以便收集工具使用的数据。由于这些更改纯粹是附加的,所以这些工具不会修改应用程序状态或行为。此类良性工具的例子包括监视代理、分析器、覆盖率分析器和事件日志记录器。
获取Instrumentation 接口实例有两种方法:
一旦代理获得一个Instrumentation 实例,代理可以在任何时候调用该实例上的方法。
1 | Instrumentation.addTransformer(new Transformer(), true); |
第二个参数表示是否可以重新转换已经定义好了的类
对于启动后再附加agent的方式,如果想要改变已经加载了的类,需要设置为true
并且注意修改manifest文件中的
1 | Can-Retransform-Classes: true |
否则会报错:
1 | adding retransformable transformers is not supported in this environment |
1 | <dependencies> |
META-INF/MANIFEST.MF
1 | Manifest-Version: 1.0 |
一个项目在开发环境,本地都能打包通过
在测试环境打包过不去
提示 error: cannot access xxxxxxx
开发和测试环境用了两套私服
测试环境有人手动上传了jar包,该jar包对应的pom文件,由nexus生成【>POM was created by Sonatype Nexus】,没有dependencies 节点
在nexus的 3rd party 中删除该jar,重新上传即可
如果通过手动上传的方式 GAV Definition 不能选择 GAV Parameters
应该选择:
GAV Definition: from pom
检查环境、检查配置,对比jar包等都没有问题
使用 mvn dependency:tree 命令,发现测试环境比开发环境少依赖了几个jar包
然后找的mavne 的 【localRepository】 目录中,按照路径找到对应jar包【根据提示缺少的类和依赖关系】的路径
如:
/data/repo/com/xxx/xxx/xxxxxxx/0.1.9.RELEASE
这个目录下有xxxx-version.jar和一个对应的xxxx-version.pom文件,问题就是这个pom文件没有dependencies信息
有问题pom文件如:
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
正常的应该是:
1 | <?xml version="1.0"?> |
1 |
|
实际测试情况:
1 | C:\Windows\SysWOW64\Macromed\Flash>del /F activex.vch |
删除目录测试:
1 |
|
先安装redis
1 | make |
找个地方创建两个文件,如:
1 | mkdir d6379 |
把redis.conf文件复制到这两个目录
进入d6379目录中,配置文件就改了
1 | daemonize yes |
启动第一个redis
1 | /usr/local/redis/bin/redis-server redis.conf |
进入d6380目录中,配置文件修改为
1 | port 6380 |
启动从节点
1 | /usr/local/redis/bin/redis-server redis.conf |
查看一下进程情况
1 | ps -ef | grep redis |
再开一个shell窗口,分别登入两个redis
登录主的
1 | $ /usr/local/redis/bin/redis-cli |
登录从的
1 | $ /usr/local/redis/bin/redis-cli -p 6380 |
分别查看他们的状态:
主的
1 | 127.0.0.1:6379> info Replication |
从的
1 | 127.0.0.1:6380> info Replication |
在主的上设置值
1 | 127.0.0.1:6379> set key1 vvv1 |
在从的上查看
1 | 127.0.0.1:6380> keys * |
可以看到从的已经自动从主的上复制了数据
再试一下在从的上写数据
1 | 127.0.0.1:6380> set key2 vvv2 |
SLAVEOF host port
SLAVEOF 命令用于在 Redis 运行时动态的修改复制(replication)功能的行为。
通过执行 SLAVEOF host port 命令,可以将当前服务器转变为指定服务器的从属服务器(slave server)。
如果当前服务器已经是某个主服务器(master server)的从属服务器,那么执行 SLAVEOF host port 将使当前服务器停止对旧主服务器的同步,丢弃旧数据集,转而开始对新主服务器进行同步。
另外,对一个从属服务器执行命令 SLAVEOF NO ONE 将使得这个从属服务器关闭复制功能,并从从属服务器转变回主服务器,原来同步所得的数据集不会被丢弃。
利用『 SLAVEOF NO ONE 不会丢弃同步所得数据集』这个特性,可以在主服务器失败的时候,将从属服务器用作新的主服务器,从而实现无间断运行。
将从服务器升级为主服务器,并且关闭复制功能
1 | 127.0.0.1:6380> SLAVEOF NO ONE |
此时可以写入数据
1 | 127.0.0.1:6380> set k5 'upgrade to master' |
将6379转成6380的从服务
1 | 127.0.0.1:6379> slaveof 127.0.0.1 6380 |
变成从的后会删除自己已有的数据,并将主服务器的数据复制过来
可以看到之前添加的数据也复制过来了
1 | 127.0.0.1:6379> keys * |
主节点下的从节点的信息都可以通过info命令查看
1 | 127.0.0.1:6380> info Replication |
生产环境栈溢出
1 | 3282788 at com.chedaia.task.address.util.ParkingPointUtil.getPreviewGpsPoint(ParkingPointUtil.java:410) |
递归调用次数太多导致
且启动脚本中设置了-Xss
1 | -XX:PermSize=128m -Xss256k |
1 | -XX:PermSize=128m -Xss2m |
1 | public static void main(String[] args) { |
结果:966
此时的栈设置为: -Xss128k
1 | 963 |
线程栈大小 | 递归次数约为 |
---|---|
-Xss128k | 966 |
-Xss256k | 2467 |
-Xss512k | 5447 |
-Xss1m | 11404 |
-Xss2m | 23323 |
-Xss3m | 35247 |
-Xss4m | 41306 |
当递归的方法中定义了一些局部变量时会占用线程栈的空间,从而影响到递归的次数
如将上面的方法稍微修改一下,增加3个局部变量:
1 | public static void recursion(int i) { |
在栈大小设置为: -Xss128k 时,结果是:623
1 | 620 |
https://www.oracle.com/technetwork/java/hotspotfaq-138619.html#threads_oom
您可能遇到线程的默认堆栈大小的问题。在Java SE 6中,Sparc的默认值是32位VM中的512k, 64位VM中的1024k。
在x86 Solaris/Linux上,32位虚拟机中是320k, 64位虚拟机中是1024k。
在Windows上,默认的线程堆栈大小是从二进制文件(java.exe)读取的。在Java SE 6中,这个值在32位VM中是320k,在64位VM中是1024k。
您可以通过使用-Xss选项运行来减小堆栈大小。例如:
1 | java -server -Xss64k |
注意,在某些版本的Windows上,操作系统可能使用非常粗的粒度来计算线程堆栈大小。如果请求的大小小于默认大小1K或更大,则将堆栈大小四舍五入到默认大小;否则,堆栈大小将四舍五入为1 MB的倍数。
64k是每个线程允许的最小堆栈空间量。
不显式设置-Xss或-XX:ThreadStackSize时,或者把-Xss或者-XX:ThreadStackSize设为0,就是使用“系统默认值”。
1 | # java -XX:+PrintFlagsFinal -version | grep ThreadStackSize |
测试环境
使用mysql-5.7.23-winx64.zip包解压缩安装
路径分别为:
解压安装路径随意指定,注意需要与my.ini文件中的路径一致
分别在两个目录中创建my.ini 文件
在同一台机器上测试,mysql实例使用不同端口
主数据库:3306 (默认)
从数据库:3307
内容如下:
1 | [mysqld] |
1 | [mysqld] |
分别初始化、启动两个数据库,并修改root密码
1 | mysqld --initialize --console |
具体见安装文档
登录主库
1 | >mysql -uroot -p |
主数据库创建用于同步的用户
1 | GRANT REPLICATION SLAVE ON *.* TO 'mstest'@'%' IDENTIFIED BY '123456'; |
登录从库,指定端口3307
1 | >mysql -uroot -p -P3307 |
配置从库连接到主库
1 | change master to master_host='127.0.0.1',master_port=3306, |
1 | mysql> show master status; |
1 |
|
1 | mysql> create database test; |
1 | select * from information_schema.processlist as p where p.command = 'Binlog Dump'; |